diff --git a/Explorer/Assets/DCL/AvatarRendering/AvatarShape/Rendering/TextureArray/TextureArrayConstants.cs b/Explorer/Assets/DCL/AvatarRendering/AvatarShape/Rendering/TextureArray/TextureArrayConstants.cs index 3d8ce59943..6a3ebedde8 100644 --- a/Explorer/Assets/DCL/AvatarRendering/AvatarShape/Rendering/TextureArray/TextureArrayConstants.cs +++ b/Explorer/Assets/DCL/AvatarRendering/AvatarShape/Rendering/TextureArray/TextureArrayConstants.cs @@ -16,6 +16,7 @@ public static class TextureArrayConstants public const TextureFormat DEFAULT_BASEMAP_TEXTURE_FORMAT = TextureFormat.BC7; public const TextureFormat DEFAULT_NORMALMAP_TEXTURE_FORMAT = TextureFormat.BC5; public const TextureFormat DEFAULT_EMISSIVEMAP_TEXTURE_FORMAT = TextureFormat.BC7; + public const TextureFormat DEFAULT_RAW_GLTF_TEXTURE_FORMAT = TextureFormat.RGBA32; // Some textures are less probably contained in the original material // so we can use a smaller starting array size for them diff --git a/Explorer/Assets/DCL/AvatarRendering/AvatarShape/Rendering/TextureArray/TextureArrayContainer.cs b/Explorer/Assets/DCL/AvatarRendering/AvatarShape/Rendering/TextureArray/TextureArrayContainer.cs index 6530d8ae66..5aa696386c 100644 --- a/Explorer/Assets/DCL/AvatarRendering/AvatarShape/Rendering/TextureArray/TextureArrayContainer.cs +++ b/Explorer/Assets/DCL/AvatarRendering/AvatarShape/Rendering/TextureArray/TextureArrayContainer.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using UnityEngine; using UnityEngine.Pool; +using static DCL.AvatarRendering.AvatarShape.Rendering.TextureArray.TextureArrayConstants; namespace DCL.AvatarRendering.AvatarShape.Rendering.TextureArray { @@ -37,10 +38,11 @@ public void ReleaseSlots(TextureArraySlot?[] slots) TextureArrayMapping mapping = mappings[i]; // Check if the texture is present in the original material var tex = originalMaterial.GetTexture(mapping.OriginalTextureID) as Texture2D; - if (tex && tex.format == mapping.Handler.GetTextureFormat()) + var handlerFormat = mapping.Handler.GetTextureFormat(); + if (tex && tex!.format == handlerFormat) results[i] = mapping.Handler.SetTexture(targetMaterial, tex, new Vector2Int(tex.width, tex.height)); - else - mapping.Handler.SetDefaultTexture(targetMaterial, mapping.DefaultFallbackResolution); + else if (tex == null || IsFormatValidForDefaultTex(handlerFormat)) + mapping.Handler.SetDefaultTexture(targetMaterial, mapping.DefaultFallbackResolution); } return results; @@ -53,14 +55,22 @@ public void ReleaseSlots(TextureArraySlot?[] slots) for (var i = 0; i < mappings.Count; i++) { TextureArrayMapping mapping = mappings[i]; + var handlerFormat = mapping.Handler.GetTextureFormat(); + bool foundTexture = textures.TryGetValue(mapping.OriginalTextureID, out var texture); + Texture2D tex = texture as Texture2D; - if (textures.TryGetValue(mapping.OriginalTextureID, out var texture)) - results[i] = mapping.Handler.SetTexture(targetMaterial, texture as Texture2D, new Vector2Int(texture.width, texture.height)); - else - mapping.Handler.SetDefaultTexture(targetMaterial, mapping.DefaultFallbackResolution, defaultSlotIndexUsed); + if (foundTexture && tex!.format == handlerFormat) + results[i] = mapping.Handler.SetTexture(targetMaterial, tex, new Vector2Int(tex.width, tex.height)); + else if (tex == null || IsFormatValidForDefaultTex(handlerFormat)) + mapping.Handler.SetDefaultTexture(targetMaterial, mapping.DefaultFallbackResolution, defaultSlotIndexUsed); } return results; } + + private bool IsFormatValidForDefaultTex(TextureFormat texFormat) => + texFormat == DEFAULT_BASEMAP_TEXTURE_FORMAT + || texFormat == DEFAULT_NORMALMAP_TEXTURE_FORMAT + || texFormat == DEFAULT_EMISSIVEMAP_TEXTURE_FORMAT; } } diff --git a/Explorer/Assets/DCL/AvatarRendering/AvatarShape/Rendering/TextureArray/TextureArrayContainerFactory.cs b/Explorer/Assets/DCL/AvatarRendering/AvatarShape/Rendering/TextureArray/TextureArrayContainerFactory.cs index 6a2d9bdf18..c8637396ec 100644 --- a/Explorer/Assets/DCL/AvatarRendering/AvatarShape/Rendering/TextureArray/TextureArrayContainerFactory.cs +++ b/Explorer/Assets/DCL/AvatarRendering/AvatarShape/Rendering/TextureArray/TextureArrayContainerFactory.cs @@ -38,6 +38,7 @@ private TextureArrayContainer CreateToon(IReadOnlyList defaultResolutions) return new TextureArrayContainer( new TextureArrayMapping[] { + // Asset Bundle Wearables (BC7/BC5) new (new TextureArrayHandler(MAIN_TEXTURE_ARRAY_SIZE, MAINTEX_ARR_SHADER_INDEX, MAINTEX_ARR_TEX_SHADER, defaultResolutions, DEFAULT_BASEMAP_TEXTURE_FORMAT, defaultTextures), MAINTEX_ORIGINAL_TEXTURE, MAIN_TEXTURE_RESOLUTION), new (new TextureArrayHandler(NORMAL_TEXTURE_ARRAY_SIZE, NORMAL_MAP_TEX_ARR_INDEX, NORMAL_MAP_TEX_ARR, defaultResolutions, DEFAULT_NORMALMAP_TEXTURE_FORMAT, defaultTextures), @@ -47,6 +48,14 @@ private TextureArrayContainer CreateToon(IReadOnlyList defaultResolutions) // new (new TextureArrayHandler(OTHER_TEXTURE_ARRAY_SIZE, METALLIC_GLOSS_MAP_ARR_SHADER_ID, METALLIC_GLOSS_MAP_ARR_TEX_SHADER_ID), METALLIC_GLOSS_MAP_ORIGINAL_TEXTURE_ID), // new (new TextureArrayHandler(OTHER_TEXTURE_ARRAY_SIZE, OCCLUSION_MAP_ARR_SHADER_ID, OCCLUSION_MAP_ARR_TEX_SHADER_ID), OCCLUSION_MAP_ORIGINAL_TEXTURE_ID), + + // Raw GLTF Wearables (RGBA32) + new (new TextureArrayHandler(MAIN_TEXTURE_ARRAY_SIZE, MAINTEX_ARR_SHADER_INDEX, MAINTEX_ARR_TEX_SHADER, defaultResolutions, DEFAULT_RAW_GLTF_TEXTURE_FORMAT), + MAINTEX_ORIGINAL_TEXTURE, MAIN_TEXTURE_RESOLUTION), + new (new TextureArrayHandler(NORMAL_TEXTURE_ARRAY_SIZE, NORMAL_MAP_TEX_ARR_INDEX, NORMAL_MAP_TEX_ARR, defaultResolutions, DEFAULT_RAW_GLTF_TEXTURE_FORMAT), + BUMP_MAP_ORIGINAL_TEXTURE_ID, NORMAL_TEXTURE_RESOLUTION), + new (new TextureArrayHandler(EMISSION_TEXTURE_ARRAY_SIZE, EMISSIVE_MAP_TEX_ARR_INDEX, EMISSIVE_MAP_TEX_ARR, defaultResolutions, DEFAULT_RAW_GLTF_TEXTURE_FORMAT), + EMISSION_MAP_ORIGINAL_TEXTURE_ID, EMISSION_TEXTURE_RESOLUTION), }); } @@ -57,6 +66,10 @@ private TextureArrayContainer CreateFacial(IReadOnlyList defaultResolutions { new (new TextureArrayHandler(FACIAL_FEATURES_TEXTURE_ARRAY_SIZE, MAINTEX_ARR_SHADER_INDEX, MAINTEX_ARR_TEX_SHADER, defaultResolutions, DEFAULT_BASEMAP_TEXTURE_FORMAT, defaultTextures), MAINTEX_ORIGINAL_TEXTURE, FACIAL_FEATURES_TEXTURE_RESOLUTION), new (new TextureArrayHandler(FACIAL_FEATURES_TEXTURE_ARRAY_SIZE, MASK_ARR_SHADER_ID, MASK_ARR_TEX_SHADER_ID, defaultResolutions, DEFAULT_BASEMAP_TEXTURE_FORMAT, defaultTextures), MASK_ORIGINAL_TEXTURE_ID, FACIAL_FEATURES_TEXTURE_RESOLUTION), + + // Raw Texture FF Wearables (RGBA32) + new (new TextureArrayHandler(FACIAL_FEATURES_TEXTURE_ARRAY_SIZE, MAINTEX_ARR_SHADER_INDEX, MAINTEX_ARR_TEX_SHADER, defaultResolutions, DEFAULT_RAW_GLTF_TEXTURE_FORMAT), MAINTEX_ORIGINAL_TEXTURE, FACIAL_FEATURES_TEXTURE_RESOLUTION), + new (new TextureArrayHandler(FACIAL_FEATURES_TEXTURE_ARRAY_SIZE, MASK_ARR_SHADER_ID, MASK_ARR_TEX_SHADER_ID, defaultResolutions, DEFAULT_RAW_GLTF_TEXTURE_FORMAT), MASK_ORIGINAL_TEXTURE_ID, FACIAL_FEATURES_TEXTURE_RESOLUTION), }); } @@ -82,7 +95,5 @@ public TextureArrayContainer CreateSceneLOD(string shaderName, IReadOnlyList defaultTextures; + private readonly IReadOnlyDictionary? defaultTextures; public TextureArrayHandler( int minArraySize, @@ -28,7 +28,7 @@ public TextureArrayHandler( int textureID, IReadOnlyList defaultResolutions, TextureFormat textureFormat, - IReadOnlyDictionary defaultTextures, + IReadOnlyDictionary? defaultTextures = null, int initialCapacityForEachResolution = PoolConstants.AVATARS_COUNT) { this.minArraySize = minArraySize; @@ -76,19 +76,21 @@ private TextureArraySlotHandler CreateHandler(TextureArrayResolutionDescriptor d handlersByResolution[resolution] = slotHandler; // When the handler is created initialize the default texture - for (int i = 0; i < defaultTextures.Count; ++i) + if (defaultTextures != null) { - if (defaultTextures.TryGetValue(new TextureArrayKey(textureID, resolution, i), out var defaultTexture)) + for (int i = 0; i < defaultTextures.Count; ++i) { - var defaultSlot = slotHandler.GetNextFreeSlot(); - Graphics.CopyTexture(defaultTexture, srcElement: 0, srcMip: 0, defaultSlot.TextureArray, dstElement: defaultSlot.UsedSlotIndex, dstMip: 0); + if (defaultTextures.TryGetValue(new TextureArrayKey(textureID, resolution, i), out var defaultTexture)) + { + var defaultSlot = slotHandler.GetNextFreeSlot(); + Graphics.CopyTexture(defaultTexture, srcElement: 0, srcMip: 0, defaultSlot.TextureArray, dstElement: defaultSlot.UsedSlotIndex, dstMip: 0); + } } } return slotHandler; } - private TextureArraySlotHandler CreateHandler(Vector2Int resolution) { //We are creating a considerably smaller array for non square resolutions. Shouldn't be a common case @@ -96,12 +98,15 @@ private TextureArraySlotHandler CreateHandler(Vector2Int resolution) handlersByResolution[resolution] = slotHandler; // When the handler is created initialize the default texture - for (int i = 0; i < defaultTextures.Count; ++i) + if (defaultTextures != null) { - if (defaultTextures.TryGetValue(new TextureArrayKey(textureID, resolution, i), out var defaultTexture)) + for (int i = 0; i < defaultTextures.Count; ++i) { - var defaultSlot = slotHandler.GetNextFreeSlot(); - Graphics.CopyTexture(defaultTexture, srcElement: 0, srcMip: 0, defaultSlot.TextureArray, dstElement: defaultSlot.UsedSlotIndex, dstMip: 0); + if (defaultTextures.TryGetValue(new TextureArrayKey(textureID, resolution, i), out var defaultTexture)) + { + var defaultSlot = slotHandler.GetNextFreeSlot(); + Graphics.CopyTexture(defaultTexture, srcElement: 0, srcMip: 0, defaultSlot.TextureArray, dstElement: defaultSlot.UsedSlotIndex, dstMip: 0); + } } } diff --git a/Explorer/Assets/DCL/AvatarRendering/Emotes/Systems/Load/LoadOwnedEmotesSystem.cs b/Explorer/Assets/DCL/AvatarRendering/Emotes/Systems/Load/LoadOwnedEmotesSystem.cs index 2f2a22a8fa..7c8770d49e 100644 --- a/Explorer/Assets/DCL/AvatarRendering/Emotes/Systems/Load/LoadOwnedEmotesSystem.cs +++ b/Explorer/Assets/DCL/AvatarRendering/Emotes/Systems/Load/LoadOwnedEmotesSystem.cs @@ -5,10 +5,12 @@ using Cysharp.Threading.Tasks; using DCL.AvatarRendering.Loading; using DCL.AvatarRendering.Loading.Systems.Abstract; +using DCL.AvatarRendering.Wearables.Helpers; using DCL.Diagnostics; using DCL.WebRequests; using ECS; using ECS.StreamableLoading.Cache; +using System; namespace DCL.AvatarRendering.Emotes.Load { @@ -21,12 +23,17 @@ public LoadOwnedEmotesSystem( IRealmData realmData, IWebRequestController webRequestController, IStreamableCache cache, - IEmoteStorage emoteStorage - ) : base(world, cache, emoteStorage, webRequestController, realmData) { } + IEmoteStorage emoteStorage, + string? builderContentURL = null + ) : base(world, cache, emoteStorage, webRequestController, realmData, builderContentURL) { } - protected override async UniTask>> ParsedResponseAsync(GenericDownloadHandlerUtils.Adapter adapter) => + protected override async UniTask>> ParseResponseAsync(GenericDownloadHandlerUtils.Adapter adapter) => await adapter.CreateFromJson(WRJsonParser.Unity); + protected override async UniTask>> ParseBuilderResponseAsync(GenericDownloadHandlerUtils.Adapter adapter) => + throw new NotImplementedException(); + // => await adapter.CreateFromJson(WRJsonParser.Newtonsoft); // TODO: Adapt for 'EmoteDTO' + protected override EmotesResolution AssetFromPreparedIntention(in GetOwnedEmotesFromRealmIntention intention) => new (intention.Result, intention.TotalAmount); diff --git a/Explorer/Assets/DCL/AvatarRendering/Loading/Assets/AttachmentAssetBase.cs b/Explorer/Assets/DCL/AvatarRendering/Loading/Assets/AttachmentAssetBase.cs index 6a5e32caa8..9619b54852 100644 --- a/Explorer/Assets/DCL/AvatarRendering/Loading/Assets/AttachmentAssetBase.cs +++ b/Explorer/Assets/DCL/AvatarRendering/Loading/Assets/AttachmentAssetBase.cs @@ -16,7 +16,7 @@ public class AttachmentTextureAsset : AttachmentAssetBase { public readonly Texture Texture; - public AttachmentTextureAsset(Texture texture, AssetBundleData assetBundleData) : base(assetBundleData) + public AttachmentTextureAsset(Texture texture, AssetBundleData? assetBundleData) : base(assetBundleData) { this.Texture = texture; } @@ -78,7 +78,7 @@ public string GetInstanceName() } /// - /// Represents an original wearable asset + /// Represents an original wearable asset (raw or asset bundle) /// public abstract class AttachmentAssetBase : IDisposable { diff --git a/Explorer/Assets/DCL/AvatarRendering/Loading/DTO/AvatarAttachmentDTO.cs b/Explorer/Assets/DCL/AvatarRendering/Loading/DTO/AvatarAttachmentDTO.cs index 412067d3f3..d337b1fd43 100644 --- a/Explorer/Assets/DCL/AvatarRendering/Loading/DTO/AvatarAttachmentDTO.cs +++ b/Explorer/Assets/DCL/AvatarRendering/Loading/DTO/AvatarAttachmentDTO.cs @@ -1,6 +1,5 @@ -#nullable disable - -using System; +using System; +using System.Collections.Generic; namespace DCL.AvatarRendering.Loading.DTO { @@ -22,6 +21,7 @@ public abstract class AvatarAttachmentDTO public long timestamp; public string version; public Content[] content; + public string? ContentDownloadUrl { get; protected set; } public abstract MetadataBase Metadata { get; } diff --git a/Explorer/Assets/DCL/AvatarRendering/Loading/ILambdaResponse.cs b/Explorer/Assets/DCL/AvatarRendering/Loading/ILambdaResponse.cs index 3deecd6faf..e38ed49cc7 100644 --- a/Explorer/Assets/DCL/AvatarRendering/Loading/ILambdaResponse.cs +++ b/Explorer/Assets/DCL/AvatarRendering/Loading/ILambdaResponse.cs @@ -1,3 +1,4 @@ +using DCL.AvatarRendering.Loading.DTO; using System.Collections.Generic; namespace DCL.AvatarRendering.Loading @@ -21,4 +22,16 @@ public interface ILambdaResponseElement IReadOnlyList IndividualData { get; } } + + public interface IBuilderLambdaResponse + { + IReadOnlyList WearablesCollection { get; } + } + + public interface IBuilderLambdaResponseElement + { + IReadOnlyDictionary Contents { get; } + + TElementDTO BuildWearableDTO(string contentDownloadUrl); + } } diff --git a/Explorer/Assets/DCL/AvatarRendering/Loading/Systems/Abstract/LoadElementsByIntentionSystem.cs b/Explorer/Assets/DCL/AvatarRendering/Loading/Systems/Abstract/LoadElementsByIntentionSystem.cs index e78d44e007..2ab8826bdb 100644 --- a/Explorer/Assets/DCL/AvatarRendering/Loading/Systems/Abstract/LoadElementsByIntentionSystem.cs +++ b/Explorer/Assets/DCL/AvatarRendering/Loading/Systems/Abstract/LoadElementsByIntentionSystem.cs @@ -25,18 +25,21 @@ public abstract class LoadElementsByIntentionSystem avatarElementStorage; private readonly IWebRequestController webRequestController; private readonly IRealmData realmData; + private readonly string? builderContentURL; protected LoadElementsByIntentionSystem( World world, IStreamableCache cache, IAvatarElementStorage avatarElementStorage, IWebRequestController webRequestController, - IRealmData realmData + IRealmData realmData, + string? builderContentURL = null ) : base(world, cache) { this.avatarElementStorage = avatarElementStorage; this.webRequestController = webRequestController; this.realmData = realmData; + this.builderContentURL = builderContentURL; } protected sealed override async UniTask> FlowInternalAsync(TIntention intention, @@ -44,20 +47,42 @@ protected sealed override async UniTask> FlowInt { await realmData.WaitConfiguredAsync(); - var lambdaResponse = - await ParsedResponseAsync( - webRequestController.GetAsync( - new CommonArguments( - BuildUrlFromIntention(in intention), - attemptsCount: intention.CommonArguments.Attempts - ), - ct, - GetReportCategory() - ) - ); - - await using (await ExecuteOnThreadPoolScope.NewScopeWithReturnOnMainThreadAsync()) - Load(ref intention, lambdaResponse); + URLAddress url = BuildUrlFromIntention(in intention); + + if (intention.CommonArguments.NeedsBuilderAPISigning) + { + var lambdaResponse = + await ParseBuilderResponseAsync( + webRequestController.SignedFetchGetAsync( + new CommonArguments( + url, + attemptsCount: intention.CommonArguments.Attempts + ), + string.Empty, + ct + ) + ); + + await using (await ExecuteOnThreadPoolScope.NewScopeWithReturnOnMainThreadAsync()) + LoadBuilderItem(ref intention, lambdaResponse); + } + else + { + var lambdaResponse = + await ParseResponseAsync( + webRequestController.GetAsync( + new CommonArguments( + url, + attemptsCount: intention.CommonArguments.Attempts + ), + ct, + GetReportCategory() + ) + ); + + await using (await ExecuteOnThreadPoolScope.NewScopeWithReturnOnMainThreadAsync()) + Load(ref intention, lambdaResponse); + } return new StreamableLoadingResult(AssetFromPreparedIntention(in intention)); } @@ -95,7 +120,22 @@ private void Load(ref TIntention intention, IAttachmentLambdaR } } - protected abstract UniTask>> ParsedResponseAsync(GenericDownloadHandlerUtils.Adapter adapter); + private void LoadBuilderItem(ref TIntention intention, IBuilderLambdaResponse> lambdaResponse) + { + if (string.IsNullOrEmpty(builderContentURL)) return; + + intention.SetTotal(lambdaResponse.WearablesCollection.Count); + + foreach (var element in lambdaResponse.WearablesCollection) + { + var wearable = avatarElementStorage.GetOrAddByDTO(element.BuildWearableDTO(builderContentURL), false); + intention.AppendToResult(wearable); + } + } + + protected abstract UniTask>> ParseResponseAsync(GenericDownloadHandlerUtils.Adapter adapter); + + protected abstract UniTask>> ParseBuilderResponseAsync(GenericDownloadHandlerUtils.Adapter adapter); protected abstract TAsset AssetFromPreparedIntention(in TIntention intention); diff --git a/Explorer/Assets/DCL/AvatarRendering/Thumbnails/Tests/ResolveWearableThumbnailSystemShould.cs b/Explorer/Assets/DCL/AvatarRendering/Thumbnails/Tests/ResolveWearableThumbnailSystemShould.cs index aaf4605cbd..b7f2e2e2a7 100644 --- a/Explorer/Assets/DCL/AvatarRendering/Thumbnails/Tests/ResolveWearableThumbnailSystemShould.cs +++ b/Explorer/Assets/DCL/AvatarRendering/Thumbnails/Tests/ResolveWearableThumbnailSystemShould.cs @@ -14,7 +14,7 @@ using ECS.TestSuite; using NUnit.Framework; using System.Collections.Generic; -using Promise = ECS.StreamableLoading.Common.AssetPromise; +using Promise = ECS.StreamableLoading.Common.AssetPromise; namespace DCL.AvatarRendering.Wearables.Tests { diff --git a/Explorer/Assets/DCL/AvatarRendering/Thumbnails/Utils/LoadThumbnailsUtils.cs b/Explorer/Assets/DCL/AvatarRendering/Thumbnails/Utils/LoadThumbnailsUtils.cs index 4c78c5a658..229d9a2e58 100644 --- a/Explorer/Assets/DCL/AvatarRendering/Thumbnails/Utils/LoadThumbnailsUtils.cs +++ b/Explorer/Assets/DCL/AvatarRendering/Thumbnails/Utils/LoadThumbnailsUtils.cs @@ -17,7 +17,7 @@ using System.Threading; using UnityEngine; using Utility; -using Promise = ECS.StreamableLoading.Common.AssetPromise; +using Promise = ECS.StreamableLoading.Common.AssetPromise; using AssetBundlePromise = ECS.StreamableLoading.Common.AssetPromise; namespace DCL.AvatarRendering.Thumbnails.Utils @@ -78,7 +78,8 @@ private static void CreateWearableThumbnailTexturePromise( using var urlBuilderScope = URL_BUILDER_POOL.AutoScope(); var urlBuilder = urlBuilderScope.Value; urlBuilder.Clear(); - urlBuilder.AppendDomain(realmData.Ipfs.ContentBaseUrl).AppendPath(thumbnailPath); + urlBuilder.AppendDomain(attachment.DTO.ContentDownloadUrl != null ? URLDomain.FromString(attachment.DTO.ContentDownloadUrl) : realmData.Ipfs.ContentBaseUrl) + .AppendPath(thumbnailPath); var promise = Promise.Create(world, new GetTextureIntention diff --git a/Explorer/Assets/DCL/AvatarRendering/Wearables/ApplicationParametersWearablesProvider.cs b/Explorer/Assets/DCL/AvatarRendering/Wearables/ApplicationParametersWearablesProvider.cs index b8881a0a85..0bfeb57789 100644 --- a/Explorer/Assets/DCL/AvatarRendering/Wearables/ApplicationParametersWearablesProvider.cs +++ b/Explorer/Assets/DCL/AvatarRendering/Wearables/ApplicationParametersWearablesProvider.cs @@ -2,6 +2,7 @@ using Cysharp.Threading.Tasks; using DCL.AvatarRendering.Loading.Components; using DCL.AvatarRendering.Wearables.Components; +using ECS.StreamableLoading.Common.Components; using Global.AppArgs; using System; using System.Collections.Generic; @@ -15,12 +16,13 @@ public class ApplicationParametersWearablesProvider : IWearablesProvider private readonly IAppArgs appArgs; private readonly IWearablesProvider source; private readonly List resultWearablesBuffer = new (); + private readonly string builderDTOsUrl; - public ApplicationParametersWearablesProvider(IAppArgs appArgs, - IWearablesProvider source) + public ApplicationParametersWearablesProvider(IAppArgs appArgs, IWearablesProvider source, string builderDTOsUrl) { this.appArgs = appArgs; this.source = source; + this.builderDTOsUrl = builderDTOsUrl; } public async UniTask<(IReadOnlyList results, int totalAmount)> GetAsync(int pageSize, int pageNumber, CancellationToken ct, @@ -29,41 +31,53 @@ public ApplicationParametersWearablesProvider(IAppArgs appArgs, string? category = null, IWearablesProvider.CollectionType collectionType = IWearablesProvider.CollectionType.All, string? name = null, - List? results = null) + List? results = null, + CommonLoadingArguments? loadingArguments = null) { - if (!appArgs.TryGetValue(AppArgsFlags.SELF_PREVIEW_WEARABLES, out string? wearablesCsv)) - return await source.GetAsync(pageSize, pageNumber, ct, sortingField, orderBy, category, collectionType, name, results); + if (appArgs.TryGetValue(AppArgsFlags.SELF_PREVIEW_WEARABLES, out string? wearablesCsv)) + { + URN[] pointers = wearablesCsv!.Split(',', StringSplitOptions.RemoveEmptyEntries) + .Select(s => new URN(s)) + .ToArray(); - URN[] pointers = wearablesCsv!.Split(',', StringSplitOptions.RemoveEmptyEntries) - .Select(s => new URN(s)) - .ToArray(); + (IReadOnlyCollection? maleWearables, IReadOnlyCollection? femaleWearables) = + await UniTask.WhenAll(RequestPointersAsync(pointers, BodyShape.MALE, ct), + RequestPointersAsync(pointers, BodyShape.FEMALE, ct)); - (IReadOnlyCollection? maleWearables, IReadOnlyCollection? femaleWearables) = - await UniTask.WhenAll(RequestPointersAsync(pointers, BodyShape.MALE, ct), - RequestPointersAsync(pointers, BodyShape.FEMALE, ct)); + results ??= new List(); - results ??= new List(); + lock (resultWearablesBuffer) + { + resultWearablesBuffer.Clear(); - lock (resultWearablesBuffer) - { - resultWearablesBuffer.Clear(); + if (maleWearables != null) + resultWearablesBuffer.AddRange(maleWearables); - if (maleWearables != null) - resultWearablesBuffer.AddRange(maleWearables); + if (femaleWearables != null) + resultWearablesBuffer.AddRange(femaleWearables); - if (femaleWearables != null) - resultWearablesBuffer.AddRange(femaleWearables); + int pageIndex = pageNumber - 1; + results.AddRange(resultWearablesBuffer.Skip(pageIndex * pageSize).Take(pageSize)); + return (results, resultWearablesBuffer.Count); + } + } - int pageIndex = pageNumber - 1; - results.AddRange(resultWearablesBuffer.Skip(pageIndex * pageSize).Take(pageSize)); - return (results, resultWearablesBuffer.Count); + if (appArgs.TryGetValue(AppArgsFlags.SELF_PREVIEW_BUILDER_COLLECTION, out string? collectionId)) + { + return await source.GetAsync(pageSize, pageNumber, ct, sortingField, orderBy, category, collectionType, name, results, + loadingArguments: new CommonLoadingArguments( + builderDTOsUrl.Replace("[COL-ID]", collectionId), + cancellationTokenSource: new CancellationTokenSource(), + needsBuilderAPISigning: true + )); } + + // Regular path without any "self-preview" element + return await source.GetAsync(pageSize, pageNumber, ct, sortingField, orderBy, category, collectionType, name, results); } public async UniTask?> RequestPointersAsync(IReadOnlyCollection pointers, - BodyShape bodyShape, - CancellationToken ct) => - await source.RequestPointersAsync(pointers, bodyShape, ct); - + BodyShape bodyShape, CancellationToken ct) + => await source.RequestPointersAsync(pointers, bodyShape, ct); } } diff --git a/Explorer/Assets/DCL/AvatarRendering/Wearables/ECSWearablesProvider.cs b/Explorer/Assets/DCL/AvatarRendering/Wearables/ECSWearablesProvider.cs index 1deed8519f..11db24dcc9 100644 --- a/Explorer/Assets/DCL/AvatarRendering/Wearables/ECSWearablesProvider.cs +++ b/Explorer/Assets/DCL/AvatarRendering/Wearables/ECSWearablesProvider.cs @@ -8,6 +8,7 @@ using DCL.Web3.Identities; using ECS.Prioritization.Components; using ECS.StreamableLoading.Common; +using ECS.StreamableLoading.Common.Components; using System.Collections.Generic; using System.Linq; using System.Threading; @@ -48,7 +49,7 @@ public ECSWearablesProvider( public async UniTask<(IReadOnlyList results, int totalAmount)> GetAsync(int pageSize, int pageNumber, CancellationToken ct, IWearablesProvider.SortingField sortingField = IWearablesProvider.SortingField.Date, IWearablesProvider.OrderBy orderBy = IWearablesProvider.OrderBy.Descending, string? category = null, IWearablesProvider.CollectionType collectionType = IWearablesProvider.CollectionType.All, - string? name = null, List? results = null) + string? name = null, List? results = null, CommonLoadingArguments? loadingArguments = null) { requestParameters.Clear(); requestParameters.Add((PAGE_NUMBER, pageNumber.ToString())); @@ -74,8 +75,12 @@ public ECSWearablesProvider( results ??= new List(); + var intention = new GetWearableByParamIntention(requestParameters, web3IdentityCache.Identity!.Address, results, 0); + if (loadingArguments.HasValue) + intention.CommonArguments = loadingArguments.Value; + var wearablesPromise = ParamPromise.Create(world!, - new GetWearableByParamIntention(requestParameters, web3IdentityCache.Identity!.Address, results, 0), + intention, PartitionComponent.TOP_PRIORITY); wearablesPromise = await wearablesPromise.ToUniTaskAsync(world!, cancellationToken: ct); diff --git a/Explorer/Assets/DCL/AvatarRendering/Wearables/Helpers/DTO/WearableDTO.cs b/Explorer/Assets/DCL/AvatarRendering/Wearables/Helpers/DTO/WearableDTO.cs index b1651eb4c4..dc77a26896 100644 --- a/Explorer/Assets/DCL/AvatarRendering/Wearables/Helpers/DTO/WearableDTO.cs +++ b/Explorer/Assets/DCL/AvatarRendering/Wearables/Helpers/DTO/WearableDTO.cs @@ -51,4 +51,48 @@ public class LambdaResponseElementDto : ILambdaResponseElement public IReadOnlyList IndividualData => individualData; } } + + [Serializable] + public class BuilderWearableDTO : WearableDTO + { + [Serializable] + public struct BuilderLambdaResponse : IBuilderLambdaResponse + { + public bool ok; + public List data; + + [JsonIgnore] + public IReadOnlyList WearablesCollection => data; + } + + [Serializable] + public class BuilderWearableMetadataDto : WearableMetadataDto, IBuilderLambdaResponseElement + { + public Dictionary contents; + public string type; + + [JsonIgnore] + public IReadOnlyDictionary Contents => contents; + + public BuilderWearableDTO BuildWearableDTO(string contentDownloadUrl) + { + Content[] parsedContent = new Content[contents.Count]; + int i = 0; + foreach ((string key, string value) in contents) + { + parsedContent[i] = new Content() { file = key, hash = value}; + i++; + } + + return new BuilderWearableDTO() + { + ContentDownloadUrl = contentDownloadUrl, + metadata = this, + id = this.id, + type = this.type, + content = parsedContent + }; + } + } + } } diff --git a/Explorer/Assets/DCL/AvatarRendering/Wearables/Helpers/WearablePolymorphicBehaviour.cs b/Explorer/Assets/DCL/AvatarRendering/Wearables/Helpers/WearablePolymorphicBehaviour.cs index 0f7ae999c0..6ba9ab16a5 100644 --- a/Explorer/Assets/DCL/AvatarRendering/Wearables/Helpers/WearablePolymorphicBehaviour.cs +++ b/Explorer/Assets/DCL/AvatarRendering/Wearables/Helpers/WearablePolymorphicBehaviour.cs @@ -11,6 +11,8 @@ using ECS.Prioritization.Components; using ECS.StreamableLoading.AssetBundles; using ECS.StreamableLoading.Common.Components; +using ECS.StreamableLoading.GLTF; +using ECS.StreamableLoading.Textures; using SceneRunner.Scene; using System; using System.Collections.Generic; @@ -19,6 +21,8 @@ using Utility; using AssetBundlePromise = ECS.StreamableLoading.Common.AssetPromise; using AssetBundleManifestPromise = ECS.StreamableLoading.Common.AssetPromise; +using RawGltfPromise = ECS.StreamableLoading.Common.AssetPromise; +using TexturePromise = ECS.StreamableLoading.Common.AssetPromise; using IAvatarAttachment = DCL.AvatarRendering.Loading.Components.IAvatarAttachment; namespace DCL.AvatarRendering.Wearables.Helpers @@ -45,17 +49,16 @@ public static bool CreateAssetBundleManifestPromise(this T component, World w } /// - /// Create a certain number of AssetBundlePromises based on the type of the wearable, + /// Create a certain number of Asset Promises based on the type of the wearable, /// if promises are already created does nothing and returns false /// - public static bool TryCreateAssetBundlePromise( + public static bool TryCreateAssetPromise( this IWearable wearable, in GetWearablesByPointersIntention intention, URLSubdirectory customStreamingSubdirectory, IPartitionComponent partitionComponent, World world, - ReportData reportData - ) + ReportData reportData) { SceneAssetBundleManifest? manifest = !EnumUtils.HasFlag(intention.PermittedSources, AssetSource.WEB) ? null : wearable.ManifestResult?.Asset; @@ -72,10 +75,9 @@ ReportData reportData partitionComponent, bodyShape, world, - reportData - ); + reportData); default: - return TryCreateSingleGameObjectAssetBundlePromise( + return TryCreateSingleGameObjectPromise( manifest, in intention, customStreamingSubdirectory, @@ -83,12 +85,11 @@ ReportData reportData partitionComponent, bodyShape, world, - reportData - ); + reportData); } } - public static bool TryCreateSingleGameObjectAssetBundlePromise( + public static bool TryCreateSingleGameObjectPromise( SceneAssetBundleManifest? sceneAssetBundleManifest, in GetWearablesByPointersIntention intention, URLSubdirectory customStreamingSubdirectory, @@ -96,8 +97,7 @@ public static bool TryCreateSingleGameObjectAssetBundlePromise( IPartitionComponent partitionComponent, BodyShape bodyShape, World world, - ReportData reportData - ) + ReportData reportData) { ref WearableAssets wearableAssets = ref InitializeResultsArray(wearable, bodyShape, 1); @@ -105,18 +105,17 @@ ReportData reportData } /// - /// Facial feature can consists of the main texture and the mask + /// Facial feature can consist of the main texture and the mask /// private static bool TryCreateFacialFeaturePromises( - SceneAssetBundleManifest sceneAssetBundleManifest, + SceneAssetBundleManifest? sceneAssetBundleManifest, in GetWearablesByPointersIntention intention, URLSubdirectory customStreamingSubdirectory, IWearable wearable, IPartitionComponent partitionComponent, BodyShape bodyShape, World world, - ReportData reportData - ) + ReportData reportData) { ref WearableAssets wearableAssets = ref InitializeResultsArray(wearable, bodyShape, 2); @@ -145,7 +144,7 @@ void SetByRef(BodyShape bs) } } - private static bool TryCreateMaskPromise(SceneAssetBundleManifest sceneAssetBundleManifest, + private static bool TryCreateMaskPromise(SceneAssetBundleManifest? sceneAssetBundleManifest, GetWearablesByPointersIntention intention, URLSubdirectory customStreamingSubdirectory, IWearable wearable, IPartitionComponent partitionComponent, ref WearableAssets wearableAssets, BodyShape bodyShape, World world) { @@ -177,7 +176,7 @@ private static bool TryCreateMaskPromise(SceneAssetBundleManifest sceneAssetBund private static bool TryCreateMainFilePromise( Type expectedObjectType, - SceneAssetBundleManifest sceneAssetBundleManifest, + SceneAssetBundleManifest? sceneAssetBundleManifest, GetWearablesByPointersIntention intention, URLSubdirectory customStreamingSubdirectory, T wearable, @@ -215,7 +214,7 @@ ReportData reportData private static void CreatePromise( Type expectedObjectType, - SceneAssetBundleManifest sceneAssetBundleManifest, + SceneAssetBundleManifest? sceneAssetBundleManifest, GetWearablesByPointersIntention intention, URLSubdirectory customStreamingSubdirectory, string hash, @@ -224,19 +223,72 @@ private static void CreatePromise( IPartitionComponent partitionComponent, World world) where T: IAvatarAttachment { - var promise = AssetBundlePromise.Create(world, - GetAssetBundleIntention.FromHash( - expectedObjectType, - hash + PlatformUtils.GetCurrentPlatform(), - permittedSources: intention.PermittedSources, - customEmbeddedSubDirectory: customStreamingSubdirectory, - manifest: sceneAssetBundleManifest, cancellationTokenSource: intention.CancellationTokenSource), - partitionComponent); + if (!string.IsNullOrEmpty(wearable.DTO.ContentDownloadUrl)) + { + foreach (AvatarAttachmentDTO.Content content in wearable.DTO.content) + { + CreateRawWearablePromise( + content, + intention, + wearable, + index, + partitionComponent, + world); + } + } + else + { + // An index is added to the promise to know to which slot of the WearableAssets it belongs to + var promise = AssetBundlePromise.Create(world, + GetAssetBundleIntention.FromHash( + expectedObjectType, + hash + PlatformUtils.GetCurrentPlatform(), + permittedSources: intention.PermittedSources, + customEmbeddedSubDirectory: customStreamingSubdirectory, + manifest: sceneAssetBundleManifest, cancellationTokenSource: intention.CancellationTokenSource), + partitionComponent); + world.Create(promise, wearable, intention.BodyShape, index); + } wearable.UpdateLoadingStatus(true); - world.Create(promise, wearable, intention.BodyShape, index); // Add an index to the promise so we know to which slot of the WearableAssets it belongs } + /// + /// Handle the creation of non-asset-bundle wearable promises, either a GLTFData promise for regular + /// wearables or a Texture2DData promise for Facial Feature wearables. + /// + private static void CreateRawWearablePromise( + AvatarAttachmentDTO.Content content, + GetWearablesByPointersIntention intention, + T wearable, + int index, + IPartitionComponent partitionComponent, + World world) where T: IAvatarAttachment + { + // An index is added to the promises to know to which slot of the WearableAssets it belongs to + + if (content.file.EndsWith(".glb")) // Wearables cannot be GLTF + { + var promise = RawGltfPromise.Create(world, GetGLTFIntention.Create(content.file, content.hash), partitionComponent); + world.Create(promise, wearable, intention.BodyShape, index); + } + else if (content.file.EndsWith(".png")) // Wearables documentation specifies PNG format + { + if (content.file.StartsWith("thumbnail")) return; + + // Texture + var promise = TexturePromise.Create(world, + new GetTextureIntention + { + // If cancellation token source was not provided a new one will be created + CommonArguments = new CommonLoadingArguments(URLAddress.FromString(wearable.DTO.ContentDownloadUrl+content.hash), cancellationTokenSource: intention.CancellationTokenSource), + }, + partitionComponent); + world.Create(promise, wearable, intention.BodyShape, index); + } + } + + // Asset Bundle Wearable public static StreamableLoadingResult ToWearableAsset(this StreamableLoadingResult result, IWearable wearable) { if (!result.Succeeded) return new StreamableLoadingResult(result.ReportData, result.Exception!); @@ -250,6 +302,23 @@ public static StreamableLoadingResult ToWearableAsset(this } } + // Raw GLTF Wearable + public static StreamableLoadingResult ToWearableAsset(this StreamableLoadingResult result, IWearable wearable) + { + if (!result.Succeeded) return new StreamableLoadingResult(result.ReportData, result.Exception!); + + return new StreamableLoadingResult(ToRegularAsset(result)); + } + + // Raw Facial Feature Wearable + public static StreamableLoadingResult ToWearableAsset(this StreamableLoadingResult result, IWearable wearable) + { + if (!result.Succeeded) return new StreamableLoadingResult(result.ReportData, result.Exception!); + + // Has to be RGBA32 to work with the avatar shader TextureArrays (Unity cannot compress BC7 in runtime) + return new StreamableLoadingResult(new AttachmentTextureAsset(TextureUtilities.EnsureRGBA32Format(result.Asset!.Asset), null)); + } + public static AttachmentRegularAsset ToRegularAsset(this StreamableLoadingResult result) { GameObject go = result.Asset!.GetMainAsset(); @@ -265,6 +334,21 @@ public static AttachmentRegularAsset ToRegularAsset(this StreamableLoadingResult return new AttachmentRegularAsset(go, rendererInfos, result.Asset); } + public static AttachmentRegularAsset ToRegularAsset(this StreamableLoadingResult result) + { + GameObject go = result.Asset!.containerGameObject; + + // collect all renderers + List rendererInfos = AttachmentRegularAsset.RENDERER_INFO_POOL.Get(); + + using PoolExtensions.Scope> pooledList = go.GetComponentsInChildrenIntoPooledList(); + + foreach (SkinnedMeshRenderer skinnedMeshRenderer in pooledList.Value) + rendererInfos.Add(new AttachmentRegularAsset.RendererInfo(skinnedMeshRenderer, skinnedMeshRenderer.sharedMaterial)); + + return new AttachmentRegularAsset(go, rendererInfos, null); + } + public static void AssignWearableAsset(this IWearable wearable, AttachmentRegularAsset attachmentRegularAsset, BodyShape bodyShape) { ref WearableAssets results = ref wearable.WearableAssetResults[bodyShape]; diff --git a/Explorer/Assets/DCL/AvatarRendering/Wearables/IWearablesProvider.cs b/Explorer/Assets/DCL/AvatarRendering/Wearables/IWearablesProvider.cs index e1e95f901e..c5db03a325 100644 --- a/Explorer/Assets/DCL/AvatarRendering/Wearables/IWearablesProvider.cs +++ b/Explorer/Assets/DCL/AvatarRendering/Wearables/IWearablesProvider.cs @@ -2,6 +2,7 @@ using Cysharp.Threading.Tasks; using DCL.AvatarRendering.Loading.Components; using DCL.AvatarRendering.Wearables.Components; +using ECS.StreamableLoading.Common.Components; using System; using System.Collections.Generic; using System.Threading; @@ -14,7 +15,8 @@ public interface IWearablesProvider SortingField sortingField = SortingField.Date, OrderBy orderBy = OrderBy.Descending, string? category = null, CollectionType collectionType = CollectionType.All, string? name = null, - List? results = null); + List? results = null, + CommonLoadingArguments? loadingArguments = null); UniTask?> RequestPointersAsync(IReadOnlyCollection pointers, BodyShape bodyShape, diff --git a/Explorer/Assets/DCL/AvatarRendering/Wearables/Systems/FinalizeWearableLoadingSystem.cs b/Explorer/Assets/DCL/AvatarRendering/Wearables/Systems/FinalizeWearableLoadingSystem.cs index adcf5107c6..c5685e3c90 100644 --- a/Explorer/Assets/DCL/AvatarRendering/Wearables/Systems/FinalizeWearableLoadingSystem.cs +++ b/Explorer/Assets/DCL/AvatarRendering/Wearables/Systems/FinalizeWearableLoadingSystem.cs @@ -19,6 +19,8 @@ using ECS.StreamableLoading.AssetBundles; using ECS.StreamableLoading.Common; using ECS.StreamableLoading.Common.Components; +using ECS.StreamableLoading.GLTF; +using ECS.StreamableLoading.Textures; using SceneRunner.Scene; using System; using System.Collections.Generic; @@ -28,6 +30,10 @@ using AssetBundlePromise = ECS.StreamableLoading.Common.AssetPromise; using StreamableResult = ECS.StreamableLoading.Common.Components.StreamableLoadingResult; +// Non-Asset-Bundle wearable promises +using RawGltfPromise = ECS.StreamableLoading.Common.AssetPromise; +using TexturePromise = ECS.StreamableLoading.Common.AssetPromise; + namespace DCL.AvatarRendering.Wearables.Systems { [UpdateInGroup(typeof(PresentationSystemGroup))] @@ -68,6 +74,9 @@ protected override void Update(float t) ResolveWearablePromiseQuery(World, defaultWearablesResolved); + FinalizeRawGltfWearableLoadingQuery(World, defaultWearablesResolved); + FinalizeRawFacialFeatureTexLoadingQuery(World, defaultWearablesResolved); + // Asset Bundles can be Resolved with Embedded Data FinalizeAssetBundleManifestLoadingQuery(World, defaultWearablesResolved); FinalizeAssetBundleLoadingQuery(World, defaultWearablesResolved); @@ -152,7 +161,7 @@ private void ResolveWearablePromise([Data] bool defaultWearablesResolved, in Ent IWearable visibleWearable = hideWearablesResolution.VisibleWearables[i]; if (visibleWearable.IsLoading) continue; - if (CreateAssetBundlePromiseIfRequired(visibleWearable, wearablesByPointersIntention, partitionComponent)) continue; + if (CreateAssetPromiseIfRequired(visibleWearable, wearablesByPointersIntention, partitionComponent)) continue; if (!visibleWearable.HasEssentialAssetsResolved(wearablesByPointersIntention.BodyShape)) continue; successfulResults++; @@ -272,6 +281,71 @@ int index } } + [Query] + private void FinalizeRawGltfWearableLoading( + [Data] bool defaultWearablesResolved, + Entity entity, + ref RawGltfPromise promise, + ref IWearable wearable, + in BodyShape bodyShape, + int index + ) + { + if (promise.LoadingIntention.CancellationTokenSource.IsCancellationRequested) + { + ResetWearableResultOnCancellation(wearable, in bodyShape, index); + promise.ForgetLoading(World); + World.Destroy(entity); + return; + } + + if (promise.TryConsume(World, out StreamableLoadingResult result)) + { + // every asset in the batch is mandatory => if at least one has already failed set the default wearables + if (result.Succeeded && !AnyAssetHasFailed(wearable, bodyShape)) + SetWearableResult(wearable, result, in bodyShape, index); + else + SetDefaultWearables(defaultWearablesResolved, wearable, in bodyShape); + + wearable.UpdateLoadingStatus(!AllAssetsAreLoaded(wearable, bodyShape)); + World.Destroy(entity); + } + } + + [Query] + [None(typeof(URLPath))] // thumbnails + private void FinalizeRawFacialFeatureTexLoading( + [Data] bool defaultWearablesResolved, + Entity entity, + ref TexturePromise promise, + ref IWearable wearable, + in BodyShape bodyShape, + int index + ) + { + if (wearable.Type != WearableType.FacialFeature) return; + + if (promise.LoadingIntention.CancellationTokenSource.IsCancellationRequested) + { + ResetWearableResultOnCancellation(wearable, in bodyShape, index); + promise.ForgetLoading(World); + World.Destroy(entity); + return; + } + + if (promise.TryConsume(World, out StreamableLoadingResult result)) + { + // every asset in the batch is mandatory => if at least one has already failed set the default wearables + if (result.Succeeded && !AnyAssetHasFailed(wearable, bodyShape)) + SetWearableResult(wearable, result, in bodyShape, index); + else + SetDefaultWearables(defaultWearablesResolved, wearable, in bodyShape); + + wearable.UpdateLoadingStatus(!AllAssetsAreLoaded(wearable, bodyShape)); + World.Destroy(entity); + } + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] private static bool AllAssetsAreLoaded(IWearable wearable, BodyShape bodyShape) { @@ -286,16 +360,18 @@ private static bool AllAssetsAreLoaded(IWearable wearable, BodyShape bodyShape) private static bool AnyAssetHasFailed(IWearable wearable, BodyShape bodyShape) => wearable.WearableAssetResults[bodyShape].ReplacedWithDefaults; - private bool CreateAssetBundlePromiseIfRequired(IWearable component, in GetWearablesByPointersIntention intention, IPartitionComponent partitionComponent) + private bool CreateAssetPromiseIfRequired(IWearable component, in GetWearablesByPointersIntention intention, IPartitionComponent partitionComponent) { + bool dtoHasContentDownloadUrl = !string.IsNullOrEmpty(component.DTO.ContentDownloadUrl); + // Do not repeat the promise if already failed once. Otherwise it will end up in an endless loading:true state - if (component.ManifestResult is { Succeeded: false }) return false; + if (!dtoHasContentDownloadUrl && component.ManifestResult is { Succeeded: false }) return false; - // Manifest is required for Web loading only - if (component.ManifestResult == null && EnumUtils.HasFlag(intention.PermittedSources, AssetSource.WEB)) + if (EnumUtils.HasFlag(intention.PermittedSources, AssetSource.WEB) // Manifest is required for Web loading only + && !dtoHasContentDownloadUrl && component.ManifestResult == null) return component.CreateAssetBundleManifestPromise(World, intention.BodyShape, intention.CancellationTokenSource, partitionComponent); - if (component.TryCreateAssetBundlePromise(in intention, customStreamingSubdirectory, partitionComponent, World, GetReportCategory())) + if (component.TryCreateAssetPromise(in intention, customStreamingSubdirectory, partitionComponent, World, GetReportCategory())) { component.UpdateLoadingStatus(true); return true; @@ -395,10 +471,20 @@ void ResetBodyShape(BodyShape bs) ResetBodyShape(bodyShape); } + // Asset Bundle Wearable private static void SetWearableResult(IWearable wearable, StreamableLoadingResult result, in BodyShape bodyShape, int index) - { - StreamableLoadingResult wearableResult = result.ToWearableAsset(wearable); + => SetWearableResult(wearable, result.ToWearableAsset(wearable), bodyShape, index); + + // Raw GLTF Wearable + private static void SetWearableResult(IWearable wearable, StreamableLoadingResult result, in BodyShape bodyShape, int index) + => SetWearableResult(wearable, result.ToWearableAsset(wearable), bodyShape, index); + // Raw Facial Feature Wearable + private static void SetWearableResult(IWearable wearable, StreamableLoadingResult result, in BodyShape bodyShape, int index) + => SetWearableResult(wearable, result.ToWearableAsset(wearable), bodyShape, index); + + private static void SetWearableResult(IWearable wearable, StreamableLoadingResult wearableResult, in BodyShape bodyShape, int index) + { if (wearable.IsUnisex() && wearable.HasSameModelsForAllGenders()) { SetByRef(BodyShape.MALE); diff --git a/Explorer/Assets/DCL/AvatarRendering/Wearables/Systems/Load/LoadWearablesByParamSystem.cs b/Explorer/Assets/DCL/AvatarRendering/Wearables/Systems/Load/LoadWearablesByParamSystem.cs index d87b329966..2090204344 100644 --- a/Explorer/Assets/DCL/AvatarRendering/Wearables/Systems/Load/LoadWearablesByParamSystem.cs +++ b/Explorer/Assets/DCL/AvatarRendering/Wearables/Systems/Load/LoadWearablesByParamSystem.cs @@ -12,6 +12,7 @@ using DCL.WebRequests; using ECS; using ECS.StreamableLoading.Cache; +using System; using System.Collections.Generic; namespace DCL.AvatarRendering.Wearables.Systems.Load @@ -29,8 +30,8 @@ public partial class LoadWearablesByParamSystem : LoadElementsByIntentionSystem< public LoadWearablesByParamSystem( World world, IWebRequestController webRequestController, IStreamableCache cache, IRealmData realmData, URLSubdirectory lambdaSubdirectory, URLSubdirectory wearablesSubdirectory, - IWearableStorage wearableStorage - ) : base(world, cache, wearableStorage, webRequestController, realmData) + IWearableStorage wearableStorage, string? builderContentURL = null + ) : base(world, cache, wearableStorage, webRequestController, realmData, builderContentURL) { this.realmData = realmData; this.lambdaSubdirectory = lambdaSubdirectory; @@ -43,9 +44,18 @@ protected override URLAddress BuildUrlFromIntention(in GetWearableByParamIntenti IReadOnlyList<(string, string)> urlEncodedParams = intention.Params; urlBuilder.Clear(); - urlBuilder.AppendDomainWithReplacedPath(realmData.Ipfs.LambdasBaseUrl, lambdaSubdirectory) - .AppendSubDirectory(URLSubdirectory.FromString(userID)) - .AppendSubDirectory(wearablesSubdirectory); + if (intention.CommonArguments.URL != URLAddress.EMPTY && intention.CommonArguments.NeedsBuilderAPISigning) + { + var url = new Uri(intention.CommonArguments.URL); + urlBuilder.AppendDomain(URLDomain.FromString($"{url.Scheme}://{url.Host}")) + .AppendSubDirectory(URLSubdirectory.FromString(url.AbsolutePath)); + } + else + { + urlBuilder.AppendDomainWithReplacedPath(realmData.Ipfs.LambdasBaseUrl, lambdaSubdirectory) + .AppendSubDirectory(URLSubdirectory.FromString(userID)) + .AppendSubDirectory(wearablesSubdirectory); + } for (var i = 0; i < urlEncodedParams.Count; i++) urlBuilder.AppendParameter(urlEncodedParams[i]); @@ -56,7 +66,13 @@ protected override URLAddress BuildUrlFromIntention(in GetWearableByParamIntenti protected override WearablesResponse AssetFromPreparedIntention(in GetWearableByParamIntention intention) => new (intention.Results, intention.TotalAmount); - protected override async UniTask>> ParsedResponseAsync(GenericDownloadHandlerUtils.Adapter adapter) => + protected override async UniTask>> ParseResponseAsync(GenericDownloadHandlerUtils.Adapter adapter) => await adapter.CreateFromJson(WRJsonParser.Unity); + + protected override async UniTask>> ParseBuilderResponseAsync(GenericDownloadHandlerUtils.Adapter adapter) + { + var result = await adapter.CreateFromJson(WRJsonParser.Newtonsoft); + return result; + } } } diff --git a/Explorer/Assets/DCL/AvatarRendering/Wearables/Wearables.asmdef b/Explorer/Assets/DCL/AvatarRendering/Wearables/Wearables.asmdef index 6f3e260763..d685959641 100644 --- a/Explorer/Assets/DCL/AvatarRendering/Wearables/Wearables.asmdef +++ b/Explorer/Assets/DCL/AvatarRendering/Wearables/Wearables.asmdef @@ -22,7 +22,8 @@ "GUID:91cf8206af184dac8e30eb46747e9939", "GUID:543b8f091a5947a3880b7f2bca2358bd", "GUID:e5a23cdae0ef4d86aafc237a73280975", - "GUID:8baf705856414dad9a73b3f382f1bc8b" + "GUID:8baf705856414dad9a73b3f382f1bc8b", + "GUID:166b65e6dfc848bb9fb075f53c293a38" ], "includePlatforms": [], "excludePlatforms": [], diff --git a/Explorer/Assets/DCL/Browser/DecentralandUrls/DecentralandUrl.cs b/Explorer/Assets/DCL/Browser/DecentralandUrls/DecentralandUrl.cs index 188a682a66..d4e27a21d4 100644 --- a/Explorer/Assets/DCL/Browser/DecentralandUrls/DecentralandUrl.cs +++ b/Explorer/Assets/DCL/Browser/DecentralandUrls/DecentralandUrl.cs @@ -54,7 +54,9 @@ public enum DecentralandUrl CameraReelLink, ApiFriends, - AssetBundleRegistry + AssetBundleRegistry, + BuilderApiDtos, + BuilderApiContent, } } diff --git a/Explorer/Assets/DCL/Browser/DecentralandUrls/DecentralandUrlsSource.cs b/Explorer/Assets/DCL/Browser/DecentralandUrls/DecentralandUrlsSource.cs index 83a31cf5af..bbb766b4df 100644 --- a/Explorer/Assets/DCL/Browser/DecentralandUrls/DecentralandUrlsSource.cs +++ b/Explorer/Assets/DCL/Browser/DecentralandUrls/DecentralandUrlsSource.cs @@ -79,6 +79,8 @@ private static string RawUrl(DecentralandUrl decentralandUrl) => DecentralandUrl.ApiPlaces => $"https://places.decentraland.{ENV}/api/places", DecentralandUrl.ApiAuth => $"https://auth-api.decentraland.{ENV}", DecentralandUrl.AuthSignature => $"https://decentraland.{ENV}/auth/requests", + DecentralandUrl.BuilderApiDtos => $"https://builder-api.decentraland.{ENV}/v1/collections/[COL-ID]/items", + DecentralandUrl.BuilderApiContent => $"https://builder-api.decentraland.{ENV}/v1/storage/contents/", DecentralandUrl.POI => $"https://dcl-lists.decentraland.{ENV}/pois", DecentralandUrl.Map => $"https://places.decentraland.{ENV}/api/map", DecentralandUrl.ContentModerationReport => $"https://places.decentraland.{ENV}/api/report", diff --git a/Explorer/Assets/DCL/CommunicationData/URLHelpers/IURLBuilder.cs b/Explorer/Assets/DCL/CommunicationData/URLHelpers/IURLBuilder.cs index 0762c0c167..87502630ea 100644 --- a/Explorer/Assets/DCL/CommunicationData/URLHelpers/IURLBuilder.cs +++ b/Explorer/Assets/DCL/CommunicationData/URLHelpers/IURLBuilder.cs @@ -8,6 +8,8 @@ public interface IURLBuilder URLAddress Build(); + IURLBuilder AppendDomain(in URLDomain domain); + IURLBuilder AppendDomainWithReplacedPath(in URLDomain domain, in URLSubdirectory newPath); IURLBuilder AppendSubDirectory(in URLSubdirectory subdirectory); diff --git a/Explorer/Assets/DCL/PluginSystem/Global/EmotePlugin.cs b/Explorer/Assets/DCL/PluginSystem/Global/EmotePlugin.cs index 15d9e9d521..15abe5beca 100644 --- a/Explorer/Assets/DCL/PluginSystem/Global/EmotePlugin.cs +++ b/Explorer/Assets/DCL/PluginSystem/Global/EmotePlugin.cs @@ -49,6 +49,7 @@ public class EmotePlugin : IDCLGlobalPlugin private readonly IReadOnlyEntityParticipantTable entityParticipantTable; private readonly AudioClipsCache audioClipsCache; private readonly URLDomain assetBundleURL; + private readonly string builderContentURL; private readonly MainUIView mainUIView; private readonly ICursor cursor; private readonly IInputBlock inputBlock; @@ -74,7 +75,8 @@ public EmotePlugin(IWebRequestController webRequestController, ICursor cursor, IInputBlock inputBlock, Arch.Core.World world, - Entity playerEntity) + Entity playerEntity, + string builderContentURL) { this.messageBus = messageBus; this.debugBuilder = debugBuilder; @@ -85,6 +87,7 @@ public EmotePlugin(IWebRequestController webRequestController, this.web3IdentityCache = web3IdentityCache; this.entityParticipantTable = entityParticipantTable; this.assetBundleURL = assetBundleURL; + this.builderContentURL = builderContentURL; this.webRequestController = webRequestController; this.emoteStorage = emoteStorage; this.realmData = realmData; @@ -115,7 +118,7 @@ public void InjectToWorld(ref ArchSystemsWorldBuilder builder, LoadOwnedEmotesSystem.InjectToWorld(ref builder, realmData, webRequestController, new NoCache(false, false), - emoteStorage); + emoteStorage, builderContentURL); CharacterEmoteSystem.InjectToWorld(ref builder, emoteStorage, messageBus, audioSourceReference, debugBuilder); diff --git a/Explorer/Assets/DCL/PluginSystem/Global/WearablePlugin.cs b/Explorer/Assets/DCL/PluginSystem/Global/WearablePlugin.cs index a7ddb72380..dbad8de405 100644 --- a/Explorer/Assets/DCL/PluginSystem/Global/WearablePlugin.cs +++ b/Explorer/Assets/DCL/PluginSystem/Global/WearablePlugin.cs @@ -16,6 +16,7 @@ using DCL.WebRequests; using ECS; using ECS.StreamableLoading.Cache; +using ECS.StreamableLoading.GLTF; using Newtonsoft.Json; using SceneRunner.Scene; using System; @@ -32,22 +33,32 @@ public class WearablePlugin : IDCLGlobalPlugin private static readonly URLSubdirectory WEARABLES_COMPLEMENT_URL = URLSubdirectory.FromString("/wearables/"); private static readonly URLSubdirectory WEARABLES_EMBEDDED_SUBDIRECTORY = URLSubdirectory.FromString("/Wearables/"); private readonly URLDomain assetBundleURL; + private readonly string builderContentURL; private readonly IAssetsProvisioner assetsProvisioner; private readonly IWebRequestController webRequestController; - + private readonly bool builderWearablesPreview; private readonly IRealmData realmData; private readonly IWearableStorage wearableStorage; private WearablesDTOList defaultWearablesDTOs; private GameObject defaultEmptyWearableAsset; - public WearablePlugin(IAssetsProvisioner assetsProvisioner, IWebRequestController webRequestController, IRealmData realmData, URLDomain assetBundleURL, CacheCleaner cacheCleaner, IWearableStorage wearableStorage) + public WearablePlugin(IAssetsProvisioner assetsProvisioner, + IWebRequestController webRequestController, + IRealmData realmData, + URLDomain assetBundleURL, + CacheCleaner cacheCleaner, + IWearableStorage wearableStorage, + string builderContentURL, + bool builderWearablesPreview) { this.wearableStorage = wearableStorage; this.assetsProvisioner = assetsProvisioner; this.webRequestController = webRequestController; this.realmData = realmData; this.assetBundleURL = assetBundleURL; + this.builderContentURL = builderContentURL; + this.builderWearablesPreview = builderWearablesPreview; cacheCleaner.Register(this.wearableStorage); } @@ -73,11 +84,20 @@ public async UniTask InitializeAsync(WearableSettings settings, CancellationToke public void InjectToWorld(ref ArchSystemsWorldBuilder builder, in GlobalPluginArguments arguments) { FinalizeWearableLoadingSystem.InjectToWorld(ref builder, wearableStorage, realmData, WEARABLES_EMBEDDED_SUBDIRECTORY); - LoadWearablesByParamSystem.InjectToWorld(ref builder, webRequestController, new NoCache(false, false), realmData, EXPLORER_SUBDIRECTORY, WEARABLES_COMPLEMENT_URL, wearableStorage); + LoadWearablesByParamSystem.InjectToWorld(ref builder, webRequestController, new NoCache(false, false), realmData, EXPLORER_SUBDIRECTORY, WEARABLES_COMPLEMENT_URL, wearableStorage, builderContentURL); LoadWearablesDTOByPointersSystem.InjectToWorld(ref builder, webRequestController, new NoCache(false, false)); LoadWearableAssetBundleManifestSystem.InjectToWorld(ref builder, new NoCache(true, true), assetBundleURL, webRequestController); - LoadDefaultWearablesSystem.InjectToWorld(ref builder, defaultWearablesDTOs, defaultEmptyWearableAsset, - wearableStorage); + LoadDefaultWearablesSystem.InjectToWorld(ref builder, defaultWearablesDTOs, defaultEmptyWearableAsset, wearableStorage); + + if (builderWearablesPreview) + { + LoadGLTFSystem.InjectToWorld(ref builder, + new NoCache(false, false), + webRequestController, + true, + true, + contentDownloadUrl: builderContentURL); + } ResolveAvatarAttachmentThumbnailSystem.InjectToWorld(ref builder); } diff --git a/Explorer/Assets/DCL/PluginSystem/World/GltfContainerPlugin.cs b/Explorer/Assets/DCL/PluginSystem/World/GltfContainerPlugin.cs index e021386806..78024eb4cd 100644 --- a/Explorer/Assets/DCL/PluginSystem/World/GltfContainerPlugin.cs +++ b/Explorer/Assets/DCL/PluginSystem/World/GltfContainerPlugin.cs @@ -60,7 +60,13 @@ public void InjectToWorld(ref ArchSystemsWorldBuilder builder, { var buffer = sharedDependencies.EntityEventsBuilder.Rent(); - LoadGLTFSystem.InjectToWorld(ref builder, new NoCache(false, false), sharedDependencies.SceneData, webRequestController); + LoadGLTFSystem.InjectToWorld( + ref builder, + new NoCache(false, false), + webRequestController, + false, + false, + sceneData: sharedDependencies.SceneData); // Asset loading PrepareGltfAssetLoadingSystem.InjectToWorld(ref builder, assetsCache, localSceneDevelopment, useRemoteAssetBundles); diff --git a/Explorer/Assets/DCL/Utilities/TextureUtilities.cs b/Explorer/Assets/DCL/Utilities/TextureUtilities.cs index 1ff52a73d9..e3274e8808 100644 --- a/Explorer/Assets/DCL/Utilities/TextureUtilities.cs +++ b/Explorer/Assets/DCL/Utilities/TextureUtilities.cs @@ -1,5 +1,3 @@ -using System.Collections; -using System.Collections.Generic; using UnityEngine; using UnityEngine.Experimental.Rendering; @@ -12,4 +10,46 @@ public static GraphicsFormat GetColorSpaceFormat() return GraphicsFormat.R32G32B32A32_SFloat; } + + public static Texture2D EnsureRGBA32Format(Texture2D sourceTexture) + { + if (sourceTexture.format == TextureFormat.RGBA32) + return sourceTexture; + + // Most likely the source texture won't be flagged as + // readable so the RenderTexture approach has to be used + RenderTexture rt = RenderTexture.GetTemporary( + sourceTexture.width, + sourceTexture.height, + 0, + RenderTextureFormat.ARGB32); + + try + { + Graphics.Blit(sourceTexture, rt); + + // Borrow active RT + RenderTexture previous = RenderTexture.active; + RenderTexture.active = rt; + + Texture2D rgba32Texture = new Texture2D( + sourceTexture.width, + sourceTexture.height, + TextureFormat.RGBA32, + false, + false); + + rgba32Texture.ReadPixels(new Rect(0, 0, rt.width, rt.height), 0, 0); + rgba32Texture.Apply(); + + // Return previously active RT + RenderTexture.active = previous; + + return rgba32Texture; + } + finally + { + RenderTexture.ReleaseTemporary(rt); + } + } } diff --git a/Explorer/Assets/Scripts/ECS/StreamableLoading/Common/Components/CommonLoadingArguments.cs b/Explorer/Assets/Scripts/ECS/StreamableLoading/Common/Components/CommonLoadingArguments.cs index ecd0d67814..a41d7af939 100644 --- a/Explorer/Assets/Scripts/ECS/StreamableLoading/Common/Components/CommonLoadingArguments.cs +++ b/Explorer/Assets/Scripts/ECS/StreamableLoading/Common/Components/CommonLoadingArguments.cs @@ -11,6 +11,7 @@ public struct CommonLoadingArguments public URLAddress? CacheableURL; public int Attempts; public int Timeout; + public bool NeedsBuilderAPISigning; /// /// When the system fails to load from the current source it removes the source from the flags @@ -36,7 +37,8 @@ public CommonLoadingArguments(URLAddress url, int attempts = StreamableLoadingDefaults.ATTEMPTS_COUNT, AssetSource permittedSources = AssetSource.WEB, AssetSource currentSource = AssetSource.WEB, - CancellationTokenSource? cancellationTokenSource = null) + CancellationTokenSource? cancellationTokenSource = null, + bool needsBuilderAPISigning = false) { URL = url; CustomEmbeddedSubDirectory = customEmbeddedSubDirectory; @@ -45,6 +47,7 @@ public CommonLoadingArguments(URLAddress url, PermittedSources = permittedSources; CurrentSource = currentSource; CacheableURL = null; + NeedsBuilderAPISigning = needsBuilderAPISigning; CancellationTokenSource = cancellationTokenSource ?? new CancellationTokenSource(); } @@ -57,8 +60,9 @@ public CommonLoadingArguments(string url, int attempts = StreamableLoadingDefaults.ATTEMPTS_COUNT, AssetSource permittedSources = AssetSource.WEB, AssetSource currentSource = AssetSource.WEB, - CancellationTokenSource cancellationTokenSource = null) : - this(URLAddress.FromString(url), customEmbeddedSubDirectory, timeout, attempts, permittedSources, currentSource, cancellationTokenSource) { } + CancellationTokenSource cancellationTokenSource = null, + bool needsBuilderAPISigning = false) : + this(URLAddress.FromString(url), customEmbeddedSubDirectory, timeout, attempts, permittedSources, currentSource, cancellationTokenSource, needsBuilderAPISigning) { } // Always override attempts count for streamable assets as repetitions are handled in LoadSystemBase public static implicit operator CommonArguments(in CommonLoadingArguments commonLoadingArguments) => diff --git a/Explorer/Assets/Scripts/ECS/StreamableLoading/GLTF/DownloadProvider.meta b/Explorer/Assets/Scripts/ECS/StreamableLoading/GLTF/DownloadProvider.meta new file mode 100644 index 0000000000..6492a7279f --- /dev/null +++ b/Explorer/Assets/Scripts/ECS/StreamableLoading/GLTF/DownloadProvider.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: b8da3a9131e042dbaa7f46ee306358ef +timeCreated: 1738251290 \ No newline at end of file diff --git a/Explorer/Assets/Scripts/ECS/StreamableLoading/GLTF/GltFastDownloadProvider.cs b/Explorer/Assets/Scripts/ECS/StreamableLoading/GLTF/DownloadProvider/GltFastDownloadProviderBase.cs similarity index 56% rename from Explorer/Assets/Scripts/ECS/StreamableLoading/GLTF/GltFastDownloadProvider.cs rename to Explorer/Assets/Scripts/ECS/StreamableLoading/GLTF/DownloadProvider/GltFastDownloadProviderBase.cs index 1bfbcc228a..2e08b14aa6 100644 --- a/Explorer/Assets/Scripts/ECS/StreamableLoading/GLTF/GltFastDownloadProvider.cs +++ b/Explorer/Assets/Scripts/ECS/StreamableLoading/GLTF/DownloadProvider/GltFastDownloadProviderBase.cs @@ -9,34 +9,28 @@ using ECS.StreamableLoading.Common.Components; using ECS.StreamableLoading.Textures; using GLTFast.Loading; -using SceneRunner.Scene; using System; using System.Threading; using System.Threading.Tasks; using UnityEngine.Networking; using Promise = ECS.StreamableLoading.Common.AssetPromise; -namespace ECS.StreamableLoading.GLTF +namespace ECS.StreamableLoading.GLTF.DownloadProvider { - internal class GltFastDownloadProvider : IDownloadProvider, IDisposable + internal abstract class GltFastDownloadProviderBase : IGLTFastDisposableDownloadProvider { - private const int ATTEMPTS_COUNT = 6; - - private readonly IAcquiredBudget acquiredBudget; - private readonly string targetGltfOriginalPath; - private readonly ISceneData sceneData; - private readonly World world; - private readonly IPartitionComponent partitionComponent; - private readonly IWebRequestController webRequestController; - private readonly ReportData reportData; - - public GltFastDownloadProvider(World world, ISceneData sceneData, IPartitionComponent partitionComponent, string targetGltfOriginalPath, ReportData reportData, - IWebRequestController webRequestController, IAcquiredBudget acquiredBudget) + protected const int ATTEMPTS_COUNT = 6; + + protected readonly IAcquiredBudget acquiredBudget; + protected readonly World world; + protected readonly IPartitionComponent partitionComponent; + protected readonly IWebRequestController webRequestController; + protected readonly ReportData reportData; + + protected GltFastDownloadProviderBase(World world, IPartitionComponent partitionComponent, ReportData reportData, IWebRequestController webRequestController, IAcquiredBudget acquiredBudget) { this.world = world; - this.sceneData = sceneData; this.partitionComponent = partitionComponent; - this.targetGltfOriginalPath = targetGltfOriginalPath; this.reportData = reportData; this.webRequestController = webRequestController; this.acquiredBudget = acquiredBudget; @@ -47,29 +41,20 @@ public void Dispose() acquiredBudget.Release(); } - private static string GetUrl(Uri uri) => + protected static string GetUrl(Uri uri) => (uri.IsAbsoluteUri ? uri.AbsoluteUri : uri.ToString()) ?? string.Empty; // RequestAsync is used for fetching the GLTF file itself + some external textures. Whenever this // method's request of the base GLTF is finished, the propagated budget for assets loading must be released. public async Task RequestAsync(Uri uri) { - bool isBaseGltfFetch = uri.OriginalString.Equals(targetGltfOriginalPath); - string originalFilePath = GetFileOriginalPathFromUri(uri); - - if (!sceneData.SceneContent.TryGetContentUrl(originalFilePath, out URLAddress tryGetContentUrlResult)) - { - if (isBaseGltfFetch) acquiredBudget.Release(); - throw new Exception($"Error on GLTF download ({targetGltfOriginalPath} - {originalFilePath}): NOT FOUND"); - } - - uri = new Uri(tryGetContentUrlResult); - var commonArguments = new CommonArguments(URLAddress.FromString(GetUrl(uri))); + var downloadUri = GetDownloadUri(uri); + var commonArguments = new CommonArguments(URLAddress.FromString(GetUrl(downloadUri))); byte[] data = Array.Empty(); string error = string.Empty; string text = string.Empty; - bool success; + bool success = false; DownloadHandler? downloadHandler = null; @@ -85,11 +70,12 @@ public async Task RequestAsync(Uri uri) } catch (UnityWebRequestException e) { - error = $"Error on GLTF download ({targetGltfOriginalPath} - {uri}): {e.Error} - {e.Message}"; + error = GetErrorMessage(downloadUri, e); } finally { - if (isBaseGltfFetch) acquiredBudget.Release(); + if (ShouldReleaseBudget(uri)) + acquiredBudget.Release(); success = string.IsNullOrEmpty(error); downloadHandler?.Dispose(); } @@ -105,19 +91,17 @@ public async Task RequestAsync(Uri uri) public async Task RequestTextureAsync(Uri uri, bool nonReadable, bool forceLinear) { - string textureOriginalPath = GetFileOriginalPathFromUri(uri); - sceneData.SceneContent.TryGetContentUrl(textureOriginalPath, out URLAddress tryGetContentUrlResult); - + var downloadUri = GetDownloadUri(uri); var texturePromise = Promise.Create(world, new GetTextureIntention { - CommonArguments = new CommonLoadingArguments(tryGetContentUrlResult, attempts: ATTEMPTS_COUNT), + CommonArguments = new CommonLoadingArguments(downloadUri.AbsoluteUri, attempts: ATTEMPTS_COUNT), }, partitionComponent); // The textures fetching need to finish before the GLTF loading can continue its flow... Promise promiseResult = await texturePromise.ToUniTaskAsync(world, cancellationToken: new CancellationToken()); if (promiseResult.Result is { Succeeded: false }) - throw new Exception($"Error on GLTF Texture download: {promiseResult.Result.Value.Exception!.Message}"); + throw new Exception(GetTextureErrorMessage(promiseResult)); return new TextureDownloadResult(promiseResult.Result?.Asset) { @@ -126,11 +110,9 @@ public async Task RequestTextureAsync(Uri uri, bool nonReadabl }; } - private string GetFileOriginalPathFromUri(Uri uri) - { - // On windows the URI may come with some invalid '\' in parts of the path - string patchedUri = uri.OriginalString.Replace('\\', '/'); - return patchedUri.Replace(sceneData.SceneContent.ContentBaseUrl.Value, string.Empty); - } + protected abstract Uri GetDownloadUri(Uri uri); + protected abstract string GetErrorMessage(Uri downloadUri, UnityWebRequestException e); + protected abstract bool ShouldReleaseBudget(Uri uri); + protected abstract string GetTextureErrorMessage(Promise promiseResult); } } diff --git a/Explorer/Assets/Scripts/ECS/StreamableLoading/GLTF/DownloadProvider/GltFastDownloadProviderBase.cs.meta b/Explorer/Assets/Scripts/ECS/StreamableLoading/GLTF/DownloadProvider/GltFastDownloadProviderBase.cs.meta new file mode 100644 index 0000000000..2cad8eb713 --- /dev/null +++ b/Explorer/Assets/Scripts/ECS/StreamableLoading/GLTF/DownloadProvider/GltFastDownloadProviderBase.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 8b6a2e641d77644f0915a1a673becd5a +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Explorer/Assets/Scripts/ECS/StreamableLoading/GLTF/DownloadProvider/GltFastGlobalDownloadProvider.cs b/Explorer/Assets/Scripts/ECS/StreamableLoading/GLTF/DownloadProvider/GltFastGlobalDownloadProvider.cs new file mode 100644 index 0000000000..af86280002 --- /dev/null +++ b/Explorer/Assets/Scripts/ECS/StreamableLoading/GLTF/DownloadProvider/GltFastGlobalDownloadProvider.cs @@ -0,0 +1,48 @@ +using Arch.Core; +using Cysharp.Threading.Tasks; +using DCL.Optimization.PerformanceBudgeting; +using DCL.Diagnostics; +using DCL.WebRequests; +using ECS.Prioritization.Components; +using System; +using Promise = ECS.StreamableLoading.Common.AssetPromise; + +namespace ECS.StreamableLoading.GLTF.DownloadProvider +{ + internal class GltFastGlobalDownloadProvider : GltFastDownloadProviderBase + { + private readonly string contentSourceUrl; + + public GltFastGlobalDownloadProvider( + World world, + string contentSourceUrl, + IPartitionComponent partitionComponent, + ReportData reportData, + IWebRequestController webRequestController, + IAcquiredBudget acquiredBudget) + : base(world, partitionComponent, reportData, webRequestController, acquiredBudget) + { + this.contentSourceUrl = contentSourceUrl; + } + + protected override Uri GetDownloadUri(Uri uri) + { + return new Uri(contentSourceUrl + uri); + } + + protected override string GetErrorMessage(Uri downloadUri, UnityWebRequestException e) + { + return $"Error on GLTF download ({downloadUri}): {e.Error} - {e.Message}"; + } + + protected override bool ShouldReleaseBudget(Uri uri) + { + return uri.OriginalString.Contains(".glb"); + } + + protected override string GetTextureErrorMessage(Promise promiseResult) + { + return $"Error on GLTF Texture download: {promiseResult.Result.Value.Exception!.Message}"; + } + } +} diff --git a/Explorer/Assets/Scripts/ECS/StreamableLoading/GLTF/DownloadProvider/GltFastGlobalDownloadProvider.cs.meta b/Explorer/Assets/Scripts/ECS/StreamableLoading/GLTF/DownloadProvider/GltFastGlobalDownloadProvider.cs.meta new file mode 100644 index 0000000000..0545c953a4 --- /dev/null +++ b/Explorer/Assets/Scripts/ECS/StreamableLoading/GLTF/DownloadProvider/GltFastGlobalDownloadProvider.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 95d7b73c5944466f80fe6e94359b656e +timeCreated: 1737389474 \ No newline at end of file diff --git a/Explorer/Assets/Scripts/ECS/StreamableLoading/GLTF/DownloadProvider/GltFastSceneDownloadProvider.cs b/Explorer/Assets/Scripts/ECS/StreamableLoading/GLTF/DownloadProvider/GltFastSceneDownloadProvider.cs new file mode 100644 index 0000000000..092c007885 --- /dev/null +++ b/Explorer/Assets/Scripts/ECS/StreamableLoading/GLTF/DownloadProvider/GltFastSceneDownloadProvider.cs @@ -0,0 +1,65 @@ +using Arch.Core; +using CommunicationData.URLHelpers; +using Cysharp.Threading.Tasks; +using DCL.Optimization.PerformanceBudgeting; +using DCL.Diagnostics; +using DCL.WebRequests; +using ECS.Prioritization.Components; +using SceneRunner.Scene; +using System; +using Promise = ECS.StreamableLoading.Common.AssetPromise; + +namespace ECS.StreamableLoading.GLTF.DownloadProvider +{ + internal class GltFastSceneDownloadProvider : GltFastDownloadProviderBase + { + private const int ATTEMPTS_COUNT = 6; + + private readonly string targetGltfOriginalPath; + private readonly ISceneData sceneData; + + public GltFastSceneDownloadProvider(World world, ISceneData sceneData, IPartitionComponent partitionComponent, string targetGltfOriginalPath, ReportData reportData, + IWebRequestController webRequestController, IAcquiredBudget acquiredBudget) + : base(world, partitionComponent, reportData, webRequestController, acquiredBudget) + { + this.sceneData = sceneData; + this.targetGltfOriginalPath = targetGltfOriginalPath; + } + + protected override Uri GetDownloadUri(Uri uri) + { + bool isBaseGltfFetch = uri.OriginalString.Equals(targetGltfOriginalPath); + string originalFilePath = GetFileOriginalPathFromUri(uri); + + if (!sceneData.SceneContent.TryGetContentUrl(originalFilePath, out URLAddress tryGetContentUrlResult)) + { + if (isBaseGltfFetch) acquiredBudget.Release(); + throw new Exception($"Error on GLTF download ({targetGltfOriginalPath} - {originalFilePath}): NOT FOUND"); + } + + return new Uri(tryGetContentUrlResult); + } + + protected override string GetErrorMessage(Uri downloadUri, UnityWebRequestException e) + { + return $"Error on GLTF download ({targetGltfOriginalPath} - {downloadUri}): {e.Error} - {e.Message}"; + } + + protected override bool ShouldReleaseBudget(Uri uri) + { + return uri.OriginalString.Equals(targetGltfOriginalPath); + } + + protected override string GetTextureErrorMessage(Promise promiseResult) + { + return $"Error on GLTF Texture download: {promiseResult.Result.Value.Exception!.Message}"; + } + + private string GetFileOriginalPathFromUri(Uri uri) + { + // On windows the URI may come with some invalid '\' in parts of the path + string patchedUri = uri.OriginalString.Replace('\\', '/'); + return patchedUri.Replace(sceneData.SceneContent.ContentBaseUrl.Value, string.Empty); + } + } +} diff --git a/Explorer/Assets/Scripts/ECS/StreamableLoading/GLTF/GltFastDownloadProvider.cs.meta b/Explorer/Assets/Scripts/ECS/StreamableLoading/GLTF/DownloadProvider/GltFastSceneDownloadProvider.cs.meta similarity index 100% rename from Explorer/Assets/Scripts/ECS/StreamableLoading/GLTF/GltFastDownloadProvider.cs.meta rename to Explorer/Assets/Scripts/ECS/StreamableLoading/GLTF/DownloadProvider/GltFastSceneDownloadProvider.cs.meta diff --git a/Explorer/Assets/Scripts/ECS/StreamableLoading/GLTF/DownloadProvider/IGLTFastDisposableDownloadProvider.cs b/Explorer/Assets/Scripts/ECS/StreamableLoading/GLTF/DownloadProvider/IGLTFastDisposableDownloadProvider.cs new file mode 100644 index 0000000000..5df792bbc1 --- /dev/null +++ b/Explorer/Assets/Scripts/ECS/StreamableLoading/GLTF/DownloadProvider/IGLTFastDisposableDownloadProvider.cs @@ -0,0 +1,10 @@ +using GLTFast.Loading; +using System; + +namespace ECS.StreamableLoading.GLTF +{ + public interface IGLTFastDisposableDownloadProvider : IDownloadProvider, IDisposable + { + + } +} diff --git a/Explorer/Assets/Scripts/ECS/StreamableLoading/GLTF/DownloadProvider/IGLTFastDisposableDownloadProvider.cs.meta b/Explorer/Assets/Scripts/ECS/StreamableLoading/GLTF/DownloadProvider/IGLTFastDisposableDownloadProvider.cs.meta new file mode 100644 index 0000000000..d498d02eed --- /dev/null +++ b/Explorer/Assets/Scripts/ECS/StreamableLoading/GLTF/DownloadProvider/IGLTFastDisposableDownloadProvider.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: edc3332227074de79c7f79d3c68fd75f +timeCreated: 1738251339 \ No newline at end of file diff --git a/Explorer/Assets/Scripts/ECS/StreamableLoading/GLTF/LoadGLTFSystem.cs b/Explorer/Assets/Scripts/ECS/StreamableLoading/GLTF/LoadGLTFSystem.cs index 97f3cff7ae..ae3c3ae824 100644 --- a/Explorer/Assets/Scripts/ECS/StreamableLoading/GLTF/LoadGLTFSystem.cs +++ b/Explorer/Assets/Scripts/ECS/StreamableLoading/GLTF/LoadGLTFSystem.cs @@ -9,6 +9,7 @@ using ECS.StreamableLoading.Cache; using ECS.StreamableLoading.Common.Components; using ECS.StreamableLoading.Common.Systems; +using ECS.StreamableLoading.GLTF.DownloadProvider; using GLTFast; using GLTFast.Materials; using SceneRunner.Scene; @@ -23,29 +24,44 @@ public partial class LoadGLTFSystem: LoadSystemBase { private static MaterialGenerator gltfMaterialGenerator = new DecentralandMaterialGenerator("DCL/Scene"); - private ISceneData sceneData; private readonly IWebRequestController webRequestController; - private GltFastReportHubLogger gltfConsoleLogger = new GltFastReportHubLogger(); - - internal LoadGLTFSystem(World world, IStreamableCache cache, ISceneData sceneData, IWebRequestController webRequestController) : base(world, cache) + private readonly GltFastReportHubLogger gltfConsoleLogger = new GltFastReportHubLogger(); + private readonly bool patchTexturesFormat; + private readonly bool importFilesByHash; + private readonly ISceneData? sceneData; + private readonly string? contentDownloadUrl; + + internal LoadGLTFSystem(World world, + IStreamableCache cache, + IWebRequestController webRequestController, + bool patchTexturesFormat, + bool importFilesByHash, + ISceneData? sceneData = null, + string? contentDownloadUrl = null) : base(world, cache) { - this.sceneData = sceneData; this.webRequestController = webRequestController; + this.patchTexturesFormat = patchTexturesFormat; + this.importFilesByHash = importFilesByHash; + this.sceneData = sceneData; + this.contentDownloadUrl = contentDownloadUrl; } protected override async UniTask> FlowInternalAsync(GetGLTFIntention intention, IAcquiredBudget acquiredBudget, IPartitionComponent partition, CancellationToken ct) { var reportData = new ReportData(GetReportCategory()); - if (!sceneData.SceneContent.TryGetContentUrl(intention.Name!, out _)) - return new StreamableLoadingResult( - reportData, - new Exception("The content to download couldn't be found")); + if (sceneData != null && !sceneData.SceneContent.TryGetContentUrl(intention.Name!, out _)) + return new StreamableLoadingResult(reportData, new Exception("The content to download couldn't be found")); // Acquired budget is released inside GLTFastDownloadedProvider once the GLTF has been fetched - GltFastDownloadProvider gltfDownloadProvider = new GltFastDownloadProvider(World, sceneData, partition, intention.Name!, reportData, webRequestController, acquiredBudget); + // Cannot inject DownloadProvider from outside, because it needs the AcquiredBudget and PartitionComponent + IGLTFastDisposableDownloadProvider gltFastDownloadProvider = + sceneData != null ? + new GltFastSceneDownloadProvider(World, sceneData!, partition, intention.Name!, reportData, webRequestController, acquiredBudget) + : new GltFastGlobalDownloadProvider(World, contentDownloadUrl!, partition, reportData, webRequestController, acquiredBudget); + var gltfImport = new GltfImport( - downloadProvider: gltfDownloadProvider, + downloadProvider: gltFastDownloadProvider, logger: gltfConsoleLogger, materialGenerator: gltfMaterialGenerator); @@ -56,8 +72,8 @@ protected override async UniTask> FlowInternal GenerateMipMaps = false, }; - bool success = await gltfImport.Load(intention.Name, gltFastSettings, ct); - gltfDownloadProvider.Dispose(); + bool success = await gltfImport.Load(importFilesByHash ? intention.Hash : intention.Name, gltFastSettings, ct); + gltFastDownloadProvider.Dispose(); if (success) { @@ -69,6 +85,10 @@ protected override async UniTask> FlowInternal await InstantiateGltfAsync(gltfImport, rootContainer.transform); + // Ensure the tex ends up being RGBA32 for all wearable textures that come from raw GLTFs + if (patchTexturesFormat) + PatchTexturesForWearable(gltfImport); + return new StreamableLoadingResult(new GLTFData(gltfImport, rootContainer)); } @@ -77,6 +97,43 @@ protected override async UniTask> FlowInternal new Exception("The content to download couldn't be found")); } + private void PatchTexturesForWearable(GltfImport gltfImport) + { + for (int i = 0; i < gltfImport.TextureCount; i++) + { + var originalTexture = gltfImport.GetTexture(i); + + // Note: BC7 (asset bundle textures optimization) cannot be compressed in runtime with + // Unity so a different format was chosen: RGBA32 + var compressedTexture = TextureUtilities.EnsureRGBA32Format(originalTexture); + if (compressedTexture == originalTexture) + continue; + + // Copy properties from original texture + compressedTexture.wrapMode = originalTexture.wrapMode; + compressedTexture.filterMode = originalTexture.filterMode; + compressedTexture.anisoLevel = originalTexture.anisoLevel; + + // Replace texture in all materials that use it + for (int matIndex = 0; matIndex < gltfImport.MaterialCount; matIndex++) + { + var material = gltfImport.GetMaterial(matIndex); + + // Check all texture properties in the material + foreach (string propertyName in material.GetTexturePropertyNames()) + { + if (material.GetTexture(propertyName) == originalTexture) + { + material.SetTexture(propertyName, compressedTexture); + } + } + } + + // Clean up original texture + UnityEngine.Object.Destroy(originalTexture); + } + } + private async UniTask InstantiateGltfAsync(GltfImport gltfImport, Transform rootContainerTransform) { if (gltfImport.SceneCount > 1) diff --git a/Explorer/Assets/Scripts/ECS/StreamableLoading/Tests/MultipleLoadSystemShould.cs b/Explorer/Assets/Scripts/ECS/StreamableLoading/Tests/MultipleLoadSystemShould.cs index d80d63063a..96544525e0 100644 --- a/Explorer/Assets/Scripts/ECS/StreamableLoading/Tests/MultipleLoadSystemShould.cs +++ b/Explorer/Assets/Scripts/ECS/StreamableLoading/Tests/MultipleLoadSystemShould.cs @@ -7,7 +7,7 @@ using Plugins.TexturesFuse.TexturesServerWrap.Unzips; using System.Collections.Generic; using UnityEngine; -using Promise = ECS.StreamableLoading.Common.AssetPromise; +using Promise = ECS.StreamableLoading.Common.AssetPromise; namespace ECS.StreamableLoading.Tests { diff --git a/Explorer/Assets/Scripts/Global/AppArgs/AppArgsFlags.cs b/Explorer/Assets/Scripts/Global/AppArgs/AppArgsFlags.cs index 1ee4529d2a..0d351fa6fa 100644 --- a/Explorer/Assets/Scripts/Global/AppArgs/AppArgsFlags.cs +++ b/Explorer/Assets/Scripts/Global/AppArgs/AppArgsFlags.cs @@ -18,6 +18,7 @@ public static class AppArgsFlags public const string FORCED_EMOTES = "self-force-emotes"; public const string SELF_PREVIEW_EMOTES = "self-preview-emotes"; public const string SELF_PREVIEW_WEARABLES = "self-preview-wearables"; + public const string SELF_PREVIEW_BUILDER_COLLECTION = "self-preview-builder-collection"; public const string CAMERA_REEL = "camera-reel"; diff --git a/Explorer/Assets/Scripts/Global/Dynamic/DynamicWorldContainer.cs b/Explorer/Assets/Scripts/Global/Dynamic/DynamicWorldContainer.cs index d8c2bd9f1b..d01e7a83c0 100644 --- a/Explorer/Assets/Scripts/Global/Dynamic/DynamicWorldContainer.cs +++ b/Explorer/Assets/Scripts/Global/Dynamic/DynamicWorldContainer.cs @@ -277,6 +277,8 @@ static IMultiPool MultiPoolFactory() => var memoryPool = new ArrayMemoryPool(ArrayPool.Shared!); var assetBundlesURL = URLDomain.FromString(bootstrapContainer.DecentralandUrlsSource.Url(DecentralandUrl.AssetBundlesCDN)); + var builderDTOsURL = URLDomain.FromString(bootstrapContainer.DecentralandUrlsSource.Url(DecentralandUrl.BuilderApiDtos)); + var builderContentURL = URLDomain.FromString(bootstrapContainer.DecentralandUrlsSource.Url(DecentralandUrl.BuilderApiContent)); var emotesCache = new MemoryEmotesStorage(); staticContainer.CacheCleaner.Register(emotesCache); @@ -295,9 +297,10 @@ static IMultiPool MultiPoolFactory() => new EcsEmoteProvider(globalWorld, staticContainer.RealmData)); var wearablesProvider = new ApplicationParametersWearablesProvider(appArgs, - new ECSWearablesProvider(identityCache, globalWorld)); + new ECSWearablesProvider(identityCache, globalWorld), builderDTOsURL.Value); bool localSceneDevelopment = !string.IsNullOrEmpty(dynamicWorldParams.LocalSceneDevelopmentRealm); + bool builderWearablesPreview = appArgs.HasFlag(AppArgsFlags.SELF_PREVIEW_BUILDER_COLLECTION); var realmContainer = RealmContainer.Create( staticContainer, @@ -545,9 +548,9 @@ await MapRendererContainer new InputPlugin(dclInput, dclCursor, unityEventSystem, assetsProvisioner, dynamicWorldDependencies.CursorUIDocument, multiplayerEmotesMessageBus, mvcManager, debugBuilder, dynamicWorldDependencies.RootUIDocument, dynamicWorldDependencies.ScenesUIDocument, dynamicWorldDependencies.CursorUIDocument, exposedGlobalDataContainer.ExposedCameraData), new GlobalInteractionPlugin(dclInput, dynamicWorldDependencies.RootUIDocument, assetsProvisioner, staticContainer.EntityCollidersGlobalCache, exposedGlobalDataContainer.GlobalInputEvents, dclCursor, unityEventSystem, mvcManager), new CharacterCameraPlugin(assetsProvisioner, realmSamplingData, exposedGlobalDataContainer.ExposedCameraData, debugBuilder, dynamicWorldDependencies.CommandLineArgs, dclInput), - new WearablePlugin(assetsProvisioner, staticContainer.WebRequestsContainer.WebRequestController, staticContainer.RealmData, assetBundlesURL, staticContainer.CacheCleaner, wearableCatalog), + new WearablePlugin(assetsProvisioner, staticContainer.WebRequestsContainer.WebRequestController, staticContainer.RealmData, assetBundlesURL, staticContainer.CacheCleaner, wearableCatalog, builderContentURL.Value, builderWearablesPreview), new EmotePlugin(staticContainer.WebRequestsContainer.WebRequestController, emotesCache, staticContainer.RealmData, multiplayerEmotesMessageBus, debugBuilder, - assetsProvisioner, selfProfile, mvcManager, dclInput, staticContainer.CacheCleaner, identityCache, entityParticipantTable, assetBundlesURL, mainUIView, dclCursor, staticContainer.InputBlock, globalWorld, playerEntity), + assetsProvisioner, selfProfile, mvcManager, dclInput, staticContainer.CacheCleaner, identityCache, entityParticipantTable, assetBundlesURL, mainUIView, dclCursor, staticContainer.InputBlock, globalWorld, playerEntity, builderContentURL.Value), new ProfilingPlugin(staticContainer.Profiler, staticContainer.RealmData, staticContainer.SingletonSharedDependencies.MemoryBudget, debugBuilder, staticContainer.ScenesCache), new AvatarPlugin( staticContainer.ComponentsContainer.ComponentPoolsRegistry, diff --git a/Explorer/Assets/Scripts/Global/Dynamic/GlobalWorldFactory.cs b/Explorer/Assets/Scripts/Global/Dynamic/GlobalWorldFactory.cs index d5cf2c25a7..158616b95f 100644 --- a/Explorer/Assets/Scripts/Global/Dynamic/GlobalWorldFactory.cs +++ b/Explorer/Assets/Scripts/Global/Dynamic/GlobalWorldFactory.cs @@ -119,7 +119,9 @@ public GlobalWorld Create(ISceneFactory sceneFactory, V8ActiveEngines v8ActiveEn AddShortInfo(world); - builder.InjectCustomGroup(new SyncedPreRenderingSystemGroup(globalSceneStateProvider)); + builder + .InjectCustomGroup(new SyncedPresentationSystemGroup(globalSceneStateProvider)) + .InjectCustomGroup(new SyncedPreRenderingSystemGroup(globalSceneStateProvider)); IReleasablePerformanceBudget sceneBudget = new ConcurrentLoadingPerformanceBudget(staticSettings.ScenesLoadingBudget); diff --git a/Explorer/Assets/Scripts/SceneRuntime/Apis/Modules/SignedFetch/SignedFetchWrap.cs b/Explorer/Assets/Scripts/SceneRuntime/Apis/Modules/SignedFetch/SignedFetchWrap.cs index ec2daebdaa..59d096ca2e 100644 --- a/Explorer/Assets/Scripts/SceneRuntime/Apis/Modules/SignedFetch/SignedFetchWrap.cs +++ b/Explorer/Assets/Scripts/SceneRuntime/Apis/Modules/SignedFetch/SignedFetchWrap.cs @@ -74,7 +74,7 @@ private static String sha256_hash(String value) { Byte[] result = hash.ComputeHash(enc.GetBytes(value)); foreach (Byte b in result) - Sb.Append(b.ToString("x2")); + Sb.Append(b.ToString("x2")); } return Sb.ToString();