Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Assets/Gothic-Core/Scripts/Const/Constants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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";

/// <summary>
/// Used during pre-caching to calculate world chunks to merge.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,17 @@ namespace Gothic.Core.Domain.StaticCache
{
public class TextureArrayCacheCreatorDomain
{
public Dictionary<string, StaticCacheService.TextureInfo> TextureArrayInformation { get; } = new();
/// <summary>
/// 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.
/// </summary>
public Dictionary<TextureCacheService.TextureArrayTypes, Dictionary<string, StaticCacheService.TextureInfo>> TextureArrayInformation { get; } = new()
{
[TextureCacheService.TextureArrayTypes.Opaque] = new Dictionary<string, StaticCacheService.TextureInfo>(StringComparer.OrdinalIgnoreCase),
[TextureCacheService.TextureArrayTypes.Transparent] = new Dictionary<string, StaticCacheService.TextureInfo>(StringComparer.OrdinalIgnoreCase),
[TextureCacheService.TextureArrayTypes.Water] = new Dictionary<string, StaticCacheService.TextureInfo>(StringComparer.OrdinalIgnoreCase)
};

[Inject] private readonly VmCacheService _vmCacheService;
[Inject] private readonly FrameSkipperService _frameSkipperService;
Expand All @@ -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();
Expand Down Expand Up @@ -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)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this separate IF is needed. Just put it all together:
IF (Water.contains() || Opaque.Contains() || Transparent.Contains()
Helps better readability.

@JucanAndreiDaniel JucanAndreiDaniel Jun 15, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

now that i think about it the whole if/else if logic is not robust enough, this only checks if a texture with the water material is used in water and other caches. dual-textures can only be in water and either opaque or transparent because of how they are processed later in the function

i need to take a look at this

{
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;
}
Expand All @@ -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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add more words from your end why this is needed as comment in code. I currently don't understand.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this comment is to be taken in context with the one from the foreach loop

// 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);
}
}
}

Expand Down
43 changes: 19 additions & 24 deletions Assets/Gothic-Core/Scripts/Services/Caches/TextureCacheService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Comment thread
JaXt0r marked this conversation as resolved.
// 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
{
Expand All @@ -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()
Expand Down Expand Up @@ -247,13 +247,8 @@ private async Task BuildTextureArray(TextureFormat textureFormat, Dictionary<str
{
++i;

if (texInfo.Key.EqualsIgnoreCase("obj_city_boulderbig_03.tga"))
{
int a = 2;
}
var sourceTex = TryGetTexture(texInfo.Key, false);


if (sourceTex == null)
{
continue;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -253,7 +253,7 @@ public void InitCacheFolder()

public async Task SaveGlobalCache(Dictionary<string, Bounds> vobBounds,
Dictionary<string, List<VobItemColliderCacheCreatorDomain.Data>> itemCollider,
Dictionary<string, TextureInfo> textureArrayInformation)
Dictionary<TextureCacheService.TextureArrayTypes, Dictionary<string, TextureInfo>> textureArrayInformation)
{
try
{
Expand All @@ -276,16 +276,15 @@ public async Task SaveGlobalCache(Dictionary<string, Bounds> 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));
Expand Down Expand Up @@ -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)
Expand Down