diff --git a/Assets/Gothic-Core/Scripts/Const/Constants.cs b/Assets/Gothic-Core/Scripts/Const/Constants.cs index ab9a974e8..fa617ecf6 100644 --- a/Assets/Gothic-Core/Scripts/Const/Constants.cs +++ b/Assets/Gothic-Core/Scripts/Const/Constants.cs @@ -154,7 +154,7 @@ public static class Animations public const string DaedalusHeroInstanceName = "PC_HERO"; // TODO - can be read from .ini file. // Alter this value to enforce game to recreate cache during next start. - public const string StaticCacheVersion = "4"; + public const string StaticCacheVersion = "5"; /// /// Used during pre-caching to calculate world chunks to merge. diff --git a/Assets/Gothic-Core/Scripts/Domain/StaticCache/TextureArrayCacheCreatorDomain.cs b/Assets/Gothic-Core/Scripts/Domain/StaticCache/TextureArrayCacheCreatorDomain.cs index 4837224b5..6d5008b98 100644 --- a/Assets/Gothic-Core/Scripts/Domain/StaticCache/TextureArrayCacheCreatorDomain.cs +++ b/Assets/Gothic-Core/Scripts/Domain/StaticCache/TextureArrayCacheCreatorDomain.cs @@ -23,7 +23,17 @@ namespace Gothic.Core.Domain.StaticCache { public class TextureArrayCacheCreatorDomain { - public Dictionary TextureArrayInformation { get; } = new(); + /// + /// Texture information per target array. A texture can be registered in the Water array AND a solid + /// array at the same time (dual-use, e.g. G1's OWODWAT_A0 is used by river materials and by the Old + /// Camp cauldron's soup surface). Gothic's VFS is case-insensitive, therefore the keys are as well. + /// + public Dictionary> TextureArrayInformation { get; } = new() + { + [TextureCacheService.TextureArrayTypes.Opaque] = new Dictionary(StringComparer.OrdinalIgnoreCase), + [TextureCacheService.TextureArrayTypes.Transparent] = new Dictionary(StringComparer.OrdinalIgnoreCase), + [TextureCacheService.TextureArrayTypes.Water] = new Dictionary(StringComparer.OrdinalIgnoreCase) + }; [Inject] private readonly VmCacheService _vmCacheService; [Inject] private readonly FrameSkipperService _frameSkipperService; @@ -46,11 +56,6 @@ public async Task CalculateTextureArrayInformation(IMesh worldMesh, int worldInd foreach (var material in worldMesh.Materials) { - if (TextureArrayInformation.ContainsKey(material.Texture)) - { - continue; - } - AddTextureToCache(material.Group, material.Texture); _loadingService.Tick(); @@ -200,8 +205,19 @@ private void AddTexInfoForSingleVob(IVirtualObject vob) private void AddTextureToCache(MaterialGroup group, string textureName) { - // Already cached. - if (TextureArrayInformation.ContainsKey(textureName)) + // The target array is a pure function of (material group, texture format): registration is + // deterministic and independent of the order in which worlds, VOBs and items are processed. + // Dual-use textures get registered once per referencing side (Water and solid). + if (group == MaterialGroup.Water) + { + if (TextureArrayInformation[TextureCacheService.TextureArrayTypes.Water].ContainsKey(textureName)) + { + return; + } + } + // Solid materials land in Opaque or Transparent depending on the texture's format - check both. + else if (TextureArrayInformation[TextureCacheService.TextureArrayTypes.Opaque].ContainsKey(textureName) + || TextureArrayInformation[TextureCacheService.TextureArrayTypes.Transparent].ContainsKey(textureName)) { return; } @@ -220,40 +236,39 @@ private void AddTextureToCache(MaterialGroup group, string textureName) Logger.LogError("Only DXT1 and RGBA32 textures are supported for texture arrays as of now!", LogCat.PreCaching); } - var textureArrayType = TextureCacheService.TextureArrayTypes.Unknown; + TextureCacheService.TextureArrayTypes textureArrayType; // Water is separate as we use a different shader. - // TODO - Do we need to check for different TextureFormats as well? if (group == MaterialGroup.Water) { textureArrayType = TextureCacheService.TextureArrayTypes.Water; } - // DXT1 can be opaque - else if (unityTextureFormat == TextureFormat.DXT1) - { - textureArrayType = TextureCacheService.TextureArrayTypes.Opaque; - } - // RGBA32 is transparent - else if (unityTextureFormat == TextureFormat.RGBA32) - { - textureArrayType = TextureCacheService.TextureArrayTypes.Transparent; - } else { - Logger.LogError($"TextureFormat={unityTextureFormat} + MaterialGroup={group} isn't handled for TextureArray so far.", LogCat.PreCaching); + // DXT1 is opaque, everything else carries an alpha channel and is handled as transparent. + textureArrayType = unityTextureFormat == TextureFormat.DXT1 + ? TextureCacheService.TextureArrayTypes.Opaque + : TextureCacheService.TextureArrayTypes.Transparent; } + var textures = TextureArrayInformation[textureArrayType]; var animationTextures = CalculateAnimationTextures(textureName); // TryAdd is used to ignore duplicates. - TextureArrayInformation.TryAdd(textureName, + textures.TryAdd(textureName, new StaticCacheService.TextureInfo(textureArrayType, Math.Max(texture.Width, texture.Height), animationTextures.Count)); // If the texture is an "animated one", we also need to add the animation textures. During runtime, water will iterate the z-index of TextureArray to loop through these elements. foreach (var animationTexture in animationTextures) { - TextureArrayInformation.Add(animationTexture.Key, - new StaticCacheService.TextureInfo(textureArrayType, Math.Max(animationTexture.Value.Width, animationTexture.Value.Height), 0)); + // Animation frames must occupy the array slices directly after their base texture. + // If a frame was already registered on its own, that contiguity is broken and the animation samples wrong slices. + if (!textures.TryAdd(animationTexture.Key, + new StaticCacheService.TextureInfo(textureArrayType, Math.Max(animationTexture.Value.Width, animationTexture.Value.Height), 0))) + { + Logger.LogError($"Animation frame texture >{animationTexture.Key}< was already registered before its base texture >{textureName}<. " + + "Its animation will sample wrong texture array slices.", LogCat.PreCaching); + } } } diff --git a/Assets/Gothic-Core/Scripts/Services/Caches/TextureCacheService.cs b/Assets/Gothic-Core/Scripts/Services/Caches/TextureCacheService.cs index 1bc641f39..f5564946f 100644 --- a/Assets/Gothic-Core/Scripts/Services/Caches/TextureCacheService.cs +++ b/Assets/Gothic-Core/Scripts/Services/Caches/TextureCacheService.cs @@ -162,34 +162,26 @@ public Texture2D TryGetTexture(ITexture zkTexture, string key, bool useCache = t public void GetTextureArrayIndex(IMaterial materialData, out TextureArrayTypes textureArrayType, out int arrayIndex, out Vector2 textureScale, out int maxMipLevel, out int animFrameCount) { var textureName = materialData.Texture; - var texture = _resourceCacheService.TryGetTexture(textureName); + (int Index, StaticCacheService.TextureInfo Data) entry; - if (_staticCacheService.LoadedTextureInfoOpaque.ContainsKey(materialData.Texture)) + // A texture can live in the Water array AND a solid array at the same time (dual-use, e.g. G1's + // OWODWAT_A0 on rivers and on the Old Camp cauldron's soup surface). The material's group decides + // which array this mesh samples - mirroring the registration in TextureArrayCacheCreatorDomain. + if (materialData.Group == MaterialGroup.Water && _staticCacheService.LoadedTextureInfoWater.TryGetValue(textureName, out entry)) + { + textureArrayType = TextureArrayTypes.Water; + } + else if (_staticCacheService.LoadedTextureInfoOpaque.TryGetValue(textureName, out entry)) { - arrayIndex = _staticCacheService.LoadedTextureInfoOpaque[materialData.Texture].Index; textureArrayType = TextureArrayTypes.Opaque; - - maxMipLevel = texture!.MipmapCount - 1; - textureScale = new Vector2((float)texture.Width / ReferenceTextureSize, (float)texture.Height / ReferenceTextureSize); - animFrameCount = _staticCacheService.LoadedTextureInfoOpaque[materialData.Texture].Data.AnimFrameC; } - else if (_staticCacheService.LoadedTextureInfoTransparent.ContainsKey(materialData.Texture)) + else if (_staticCacheService.LoadedTextureInfoTransparent.TryGetValue(textureName, out entry)) { - arrayIndex = _staticCacheService.LoadedTextureInfoTransparent[materialData.Texture].Index; textureArrayType = TextureArrayTypes.Transparent; - - maxMipLevel = texture!.MipmapCount - 1; - textureScale = new Vector2((float)texture.Width / ReferenceTextureSize, (float)texture.Height / ReferenceTextureSize); - animFrameCount = _staticCacheService.LoadedTextureInfoTransparent[materialData.Texture].Data.AnimFrameC; } - else if (_staticCacheService.LoadedTextureInfoWater.ContainsKey(materialData.Texture)) + else if (_staticCacheService.LoadedTextureInfoWater.TryGetValue(textureName, out entry)) { - arrayIndex = _staticCacheService.LoadedTextureInfoWater[materialData.Texture].Index; textureArrayType = TextureArrayTypes.Water; - - maxMipLevel = texture!.MipmapCount - 1; - textureScale = new Vector2((float)texture.Width / ReferenceTextureSize, (float)texture.Height / ReferenceTextureSize); - animFrameCount = _staticCacheService.LoadedTextureInfoWater[materialData.Texture].Data.AnimFrameC; } else { @@ -199,7 +191,15 @@ public void GetTextureArrayIndex(IMaterial materialData, out TextureArrayTypes t maxMipLevel = 0; textureScale = Vector2.one; animFrameCount = 0; + return; } + + var texture = _resourceCacheService.TryGetTexture(textureName); + + arrayIndex = entry.Index; + maxMipLevel = texture!.MipmapCount - 1; + textureScale = new Vector2((float)texture.Width / ReferenceTextureSize, (float)texture.Height / ReferenceTextureSize); + animFrameCount = entry.Data.AnimFrameC; } public async Task BuildTextureArray() @@ -247,13 +247,8 @@ private async Task BuildTextureArray(TextureFormat textureFormat, Dictionary vobBounds, Dictionary> itemCollider, - Dictionary textureArrayInformation) + Dictionary> textureArrayInformation) { try { @@ -276,16 +276,15 @@ public async Task SaveGlobalCache(Dictionary vobBounds, }; await SaveCacheFile(vobItemCollider, BuildFilePathName(_fileNameGlobalVobItemCollider)); + // A texture may appear in more than one list: dual-use textures occupy a slice in the Water + // array AND a solid array (e.g. G1's OWODWAT_A0 on rivers and the Old Camp cauldron). var textureArrayContainer = new TextureArrayContainer { - TexturesOpaque = textureArrayInformation - .Where(i => i.Value.T == TextureCacheService.TextureArrayTypes.Opaque) + TexturesOpaque = textureArrayInformation[TextureCacheService.TextureArrayTypes.Opaque] .Select(i => new TextureArrayEntry(i.Key, i.Value.MaxDim, i.Value.AnimFrameC)).ToList(), - TexturesTransparent = textureArrayInformation - .Where(i => i.Value.T == TextureCacheService.TextureArrayTypes.Transparent) + TexturesTransparent = textureArrayInformation[TextureCacheService.TextureArrayTypes.Transparent] .Select(i => new TextureArrayEntry(i.Key, i.Value.MaxDim, i.Value.AnimFrameC)).ToList(), - TexturesWater = textureArrayInformation - .Where(i => i.Value.T == TextureCacheService.TextureArrayTypes.Water) + TexturesWater = textureArrayInformation[TextureCacheService.TextureArrayTypes.Water] .Select(i => new TextureArrayEntry(i.Key, i.Value.MaxDim, i.Value.AnimFrameC)).ToList(), }; await SaveCacheFile(textureArrayContainer, BuildFilePathName(_fileNameGlobalTextureArrayData)); @@ -365,17 +364,19 @@ public async Task LoadGlobalCache() LoadedVobsBounds = vobBoundsContainer.BoundsEntries.ToDictionary(i => i.Mesh, i => i.Bounds); LoadedVobItemColliders = vobItemsColliderContainer.ColliderEntries.ToDictionary(i => i.Mesh, i => i.Colls); + // Keys are case-insensitive like Gothic's VFS, so materials referencing a texture with deviating + // casing still resolve to the same array slice. var loopIndex = 0; LoadedTextureInfoOpaque = textureArrayContainer.TexturesOpaque - .ToDictionary(i => i.Tex, i => (index: loopIndex++, data: new TextureInfo(TextureCacheService.TextureArrayTypes.Opaque, i.MaxDim, i.AnimFrameC))); + .ToDictionary(i => i.Tex, i => (index: loopIndex++, data: new TextureInfo(TextureCacheService.TextureArrayTypes.Opaque, i.MaxDim, i.AnimFrameC)), StringComparer.OrdinalIgnoreCase); loopIndex = 0; LoadedTextureInfoTransparent = textureArrayContainer.TexturesTransparent - .ToDictionary(i => i.Tex, i => (index: loopIndex++, data: new TextureInfo(TextureCacheService.TextureArrayTypes.Transparent, i.MaxDim, i.AnimFrameC))); + .ToDictionary(i => i.Tex, i => (index: loopIndex++, data: new TextureInfo(TextureCacheService.TextureArrayTypes.Transparent, i.MaxDim, i.AnimFrameC)), StringComparer.OrdinalIgnoreCase); loopIndex = 0; LoadedTextureInfoWater = textureArrayContainer.TexturesWater - .ToDictionary(i => i.Tex, i => (index: loopIndex++, data: new TextureInfo(TextureCacheService.TextureArrayTypes.Water, i.MaxDim, i.AnimFrameC))); + .ToDictionary(i => i.Tex, i => (index: loopIndex++, data: new TextureInfo(TextureCacheService.TextureArrayTypes.Water, i.MaxDim, i.AnimFrameC)), StringComparer.OrdinalIgnoreCase); } public async Task LoadWorldCache(string worldName)