From 635b81f8e21821b8d4803f8a01435d15583d166d Mon Sep 17 00:00:00 2001 From: Kewin Kupilas Date: Thu, 12 Feb 2026 20:19:25 +0000 Subject: [PATCH 1/7] Fixed animated group textures not animating --- .../TombEngine/TombEngineTexInfoManager.cs | 101 ++++++++++++++++++ .../Compilers/Util/TexInfoManager.cs | 99 +++++++++++++++++ 2 files changed, 200 insertions(+) diff --git a/TombLib/TombLib/LevelData/Compilers/TombEngine/TombEngineTexInfoManager.cs b/TombLib/TombLib/LevelData/Compilers/TombEngine/TombEngineTexInfoManager.cs index 073330ce4a..445eae2f41 100644 --- a/TombLib/TombLib/LevelData/Compilers/TombEngine/TombEngineTexInfoManager.cs +++ b/TombLib/TombLib/LevelData/Compilers/TombEngine/TombEngineTexInfoManager.cs @@ -1264,6 +1264,107 @@ public Result AddTexture(TextureArea texture, TextureDestination destination, bo } } + // Check if this is a sub-area of an animated texture (e.g. applied via group texturing tools). + // In this case, the actual UV coordinates represent a portion of the full animation frame, + // so we reconstruct the full frame from ParentArea and match against reference animations. + if (!texture.ParentArea.IsZero && _referenceAnimTextures.Count > 0) + { + TextureArea fullTexture = texture; + fullTexture.TexCoord0 = new Vector2(texture.ParentArea.X0, texture.ParentArea.Y0); + fullTexture.TexCoord1 = new Vector2(texture.ParentArea.X0, texture.ParentArea.Y1); + fullTexture.TexCoord2 = new Vector2(texture.ParentArea.X1, texture.ParentArea.Y1); + fullTexture.TexCoord3 = new Vector2(texture.ParentArea.X1, texture.ParentArea.Y0); + fullTexture.ParentArea = Rectangle2.Zero; + + foreach (var refTex in _referenceAnimTextures) + { + // UVRotate and Video animation types are incompatible with sub-area splitting + // because they rely on specific frame arrangement assumptions (vertical strip scrolling + // for UVRotate, sequential frame playback for Video) that break when coordinates + // are transformed to sub-areas. + if (refTex.Origin.IsUvRotate || refTex.Origin.AnimationType == AnimatedTextureAnimationType.Video) + continue; + + if (GetTexInfo(fullTexture, refTex.CompiledAnimation, destination, false, blendMode, false, _animTextureLookupMargin).HasValue) + { + var origSet = refTex.Origin; + var parentRect = texture.ParentArea; + var subRect = texture.GetRect(isForTriangle); + + // Find which frame in the set matches our ParentArea + AnimatedTextureFrame matchedFrame = null; + + foreach (var frame in origSet.Frames) + { + if (frame.Texture != texture.Texture) + continue; + + var fRect = new Rectangle2( + MathF.Min(frame.TexCoord0.X, MathF.Min(frame.TexCoord1.X, MathF.Min(frame.TexCoord2.X, frame.TexCoord3.X))), + MathF.Min(frame.TexCoord0.Y, MathF.Min(frame.TexCoord1.Y, MathF.Min(frame.TexCoord2.Y, frame.TexCoord3.Y))), + MathF.Max(frame.TexCoord0.X, MathF.Max(frame.TexCoord1.X, MathF.Max(frame.TexCoord2.X, frame.TexCoord3.X))), + MathF.Max(frame.TexCoord0.Y, MathF.Max(frame.TexCoord1.Y, MathF.Max(frame.TexCoord2.Y, frame.TexCoord3.Y)))); + + if (MathC.WithinEpsilon(fRect.X0, parentRect.X0, _animTextureLookupMargin) && + MathC.WithinEpsilon(fRect.Y0, parentRect.Y0, _animTextureLookupMargin) && + MathC.WithinEpsilon(fRect.X1, parentRect.X1, _animTextureLookupMargin) && + MathC.WithinEpsilon(fRect.Y1, parentRect.Y1, _animTextureLookupMargin)) + { + matchedFrame = frame; + break; + } + } + + if (matchedFrame == null) + continue; + + // Calculate relative sub-area position within the matched frame + var mfRect = new Rectangle2( + MathF.Min(matchedFrame.TexCoord0.X, MathF.Min(matchedFrame.TexCoord1.X, MathF.Min(matchedFrame.TexCoord2.X, matchedFrame.TexCoord3.X))), + MathF.Min(matchedFrame.TexCoord0.Y, MathF.Min(matchedFrame.TexCoord1.Y, MathF.Min(matchedFrame.TexCoord2.Y, matchedFrame.TexCoord3.Y))), + MathF.Max(matchedFrame.TexCoord0.X, MathF.Max(matchedFrame.TexCoord1.X, MathF.Max(matchedFrame.TexCoord2.X, matchedFrame.TexCoord3.X))), + MathF.Max(matchedFrame.TexCoord0.Y, MathF.Max(matchedFrame.TexCoord1.Y, MathF.Max(matchedFrame.TexCoord2.Y, matchedFrame.TexCoord3.Y)))); + + float relX0 = (subRect.X0 - mfRect.X0) / mfRect.Width; + float relY0 = (subRect.Y0 - mfRect.Y0) / mfRect.Height; + float relX1 = (subRect.X1 - mfRect.X0) / mfRect.Width; + float relY1 = (subRect.Y1 - mfRect.Y0) / mfRect.Height; + + // Create a synthetic AnimatedTextureSet with sub-area frames + var subSet = origSet.Clone(); + subSet.Frames.Clear(); + + foreach (var frame in origSet.Frames) + { + var frameRect = new Rectangle2( + MathF.Min(frame.TexCoord0.X, MathF.Min(frame.TexCoord1.X, MathF.Min(frame.TexCoord2.X, frame.TexCoord3.X))), + MathF.Min(frame.TexCoord0.Y, MathF.Min(frame.TexCoord1.Y, MathF.Min(frame.TexCoord2.Y, frame.TexCoord3.Y))), + MathF.Max(frame.TexCoord0.X, MathF.Max(frame.TexCoord1.X, MathF.Max(frame.TexCoord2.X, frame.TexCoord3.X))), + MathF.Max(frame.TexCoord0.Y, MathF.Max(frame.TexCoord1.Y, MathF.Max(frame.TexCoord2.Y, frame.TexCoord3.Y)))); + + float fw = frameRect.Width; + float fh = frameRect.Height; + + var subFrame = frame.Clone(); + subFrame.TexCoord0 = new Vector2(frameRect.X0 + relX0 * fw, frameRect.Y0 + relY0 * fh); + subFrame.TexCoord1 = new Vector2(frameRect.X0 + relX0 * fw, frameRect.Y0 + relY1 * fh); + subFrame.TexCoord2 = new Vector2(frameRect.X0 + relX1 * fw, frameRect.Y0 + relY1 * fh); + subFrame.TexCoord3 = new Vector2(frameRect.X0 + relX1 * fw, frameRect.Y0 + relY0 * fh); + + subSet.Frames.Add(subFrame); + } + + // Generate reference lookups for this sub-area animation set + GenerateAnimLookups(new List { subSet }, destination); + + // Retry - the sub-area coordinates should now match the new reference lookups + var result = AddTexture(texture, destination, isForTriangle, blendMode); + result.Animated = true; + return result; + } + } + } + var parentTextures = _parentRoomTextureAreas; if (destination == TextureDestination.Moveable) parentTextures = _parentMoveableTextureAreas; diff --git a/TombLib/TombLib/LevelData/Compilers/Util/TexInfoManager.cs b/TombLib/TombLib/LevelData/Compilers/Util/TexInfoManager.cs index b1cd775e30..f5047a8b2b 100644 --- a/TombLib/TombLib/LevelData/Compilers/Util/TexInfoManager.cs +++ b/TombLib/TombLib/LevelData/Compilers/Util/TexInfoManager.cs @@ -830,6 +830,105 @@ public Result AddTexture(TextureArea texture, bool isForRoom, bool isForTriangle } } + // Check if this is a sub-area of an animated texture (e.g. applied via group texturing tools). + // In this case, the actual UV coordinates represent a portion of the full animation frame, + // so we reconstruct the full frame from ParentArea and match against reference animations. + if (!texture.ParentArea.IsZero && _referenceAnimTextures.Count > 0) + { + TextureArea fullTexture = texture; + fullTexture.TexCoord0 = new Vector2(texture.ParentArea.X0, texture.ParentArea.Y0); + fullTexture.TexCoord1 = new Vector2(texture.ParentArea.X0, texture.ParentArea.Y1); + fullTexture.TexCoord2 = new Vector2(texture.ParentArea.X1, texture.ParentArea.Y1); + fullTexture.TexCoord3 = new Vector2(texture.ParentArea.X1, texture.ParentArea.Y0); + fullTexture.ParentArea = Rectangle2.Zero; + + foreach (var refTex in _referenceAnimTextures) + { + // UVRotate and Video animation types are incompatible with sub-area splitting + // because they rely on specific frame arrangement assumptions (vertical strip scrolling + // for UVRotate, sequential frame playback for Video) that break when coordinates + // are transformed to sub-areas. + if (refTex.Origin.IsUvRotate || refTex.Origin.AnimationType == AnimatedTextureAnimationType.Video) + continue; + + if (GetTexInfo(fullTexture, refTex.CompiledAnimation, isForRoom, false, false, false, remapAnimatedTextures, _animTextureLookupMargin).HasValue) + { + var origSet = refTex.Origin; + var parentRect = texture.ParentArea; + var subRect = texture.GetRect(isForTriangle); + + // Find which frame in the set matches our ParentArea + AnimatedTextureFrame matchedFrame = null; + + foreach (var frame in origSet.Frames) + { + if (frame.Texture != texture.Texture) + continue; + + var fRect = new Rectangle2( + MathF.Min(frame.TexCoord0.X, MathF.Min(frame.TexCoord1.X, MathF.Min(frame.TexCoord2.X, frame.TexCoord3.X))), + MathF.Min(frame.TexCoord0.Y, MathF.Min(frame.TexCoord1.Y, MathF.Min(frame.TexCoord2.Y, frame.TexCoord3.Y))), + MathF.Max(frame.TexCoord0.X, MathF.Max(frame.TexCoord1.X, MathF.Max(frame.TexCoord2.X, frame.TexCoord3.X))), + MathF.Max(frame.TexCoord0.Y, MathF.Max(frame.TexCoord1.Y, MathF.Max(frame.TexCoord2.Y, frame.TexCoord3.Y)))); + + if (MathC.WithinEpsilon(fRect.X0, parentRect.X0, _animTextureLookupMargin) && + MathC.WithinEpsilon(fRect.Y0, parentRect.Y0, _animTextureLookupMargin) && + MathC.WithinEpsilon(fRect.X1, parentRect.X1, _animTextureLookupMargin) && + MathC.WithinEpsilon(fRect.Y1, parentRect.Y1, _animTextureLookupMargin)) + { + matchedFrame = frame; + break; + } + } + + if (matchedFrame == null) + continue; + + // Calculate relative sub-area position within the matched frame + var mfRect = new Rectangle2( + MathF.Min(matchedFrame.TexCoord0.X, MathF.Min(matchedFrame.TexCoord1.X, MathF.Min(matchedFrame.TexCoord2.X, matchedFrame.TexCoord3.X))), + MathF.Min(matchedFrame.TexCoord0.Y, MathF.Min(matchedFrame.TexCoord1.Y, MathF.Min(matchedFrame.TexCoord2.Y, matchedFrame.TexCoord3.Y))), + MathF.Max(matchedFrame.TexCoord0.X, MathF.Max(matchedFrame.TexCoord1.X, MathF.Max(matchedFrame.TexCoord2.X, matchedFrame.TexCoord3.X))), + MathF.Max(matchedFrame.TexCoord0.Y, MathF.Max(matchedFrame.TexCoord1.Y, MathF.Max(matchedFrame.TexCoord2.Y, matchedFrame.TexCoord3.Y)))); + + float relX0 = (subRect.X0 - mfRect.X0) / mfRect.Width; + float relY0 = (subRect.Y0 - mfRect.Y0) / mfRect.Height; + float relX1 = (subRect.X1 - mfRect.X0) / mfRect.Width; + float relY1 = (subRect.Y1 - mfRect.Y0) / mfRect.Height; + + // Create a synthetic AnimatedTextureSet with sub-area frames + var subSet = origSet.Clone(); + subSet.Frames.Clear(); + + foreach (var frame in origSet.Frames) + { + var frameRect = new Rectangle2( + MathF.Min(frame.TexCoord0.X, MathF.Min(frame.TexCoord1.X, MathF.Min(frame.TexCoord2.X, frame.TexCoord3.X))), + MathF.Min(frame.TexCoord0.Y, MathF.Min(frame.TexCoord1.Y, MathF.Min(frame.TexCoord2.Y, frame.TexCoord3.Y))), + MathF.Max(frame.TexCoord0.X, MathF.Max(frame.TexCoord1.X, MathF.Max(frame.TexCoord2.X, frame.TexCoord3.X))), + MathF.Max(frame.TexCoord0.Y, MathF.Max(frame.TexCoord1.Y, MathF.Max(frame.TexCoord2.Y, frame.TexCoord3.Y)))); + + float fw = frameRect.Width; + float fh = frameRect.Height; + + var subFrame = frame.Clone(); + subFrame.TexCoord0 = new Vector2(frameRect.X0 + relX0 * fw, frameRect.Y0 + relY0 * fh); + subFrame.TexCoord1 = new Vector2(frameRect.X0 + relX0 * fw, frameRect.Y0 + relY1 * fh); + subFrame.TexCoord2 = new Vector2(frameRect.X0 + relX1 * fw, frameRect.Y0 + relY1 * fh); + subFrame.TexCoord3 = new Vector2(frameRect.X0 + relX1 * fw, frameRect.Y0 + relY0 * fh); + + subSet.Frames.Add(subFrame); + } + + // Generate reference lookups for this sub-area animation set + GenerateAnimLookups(new List { subSet }); + + // Retry - the sub-area coordinates should now match the new reference lookups + return AddTexture(texture, isForRoom, isForTriangle, topmostAndUnpadded); + } + } + } + // No animated textures identified, add texture as ordinary one return AddTexture(texture, _parentTextures, isForRoom, isForTriangle, topmostAndUnpadded); } From 9f4970e2d64c684d2baa24a1dd8f26ea9b76fb0d Mon Sep 17 00:00:00 2001 From: Kewin Kupilas Date: Sun, 22 Mar 2026 21:29:01 +0000 Subject: [PATCH 2/7] Apply Copilot suggestions --- .../TombEngine/TombEngineTexInfoManager.cs | 29 +++++++++---------- .../Compilers/Util/TexInfoManager.cs | 29 +++++++++---------- 2 files changed, 28 insertions(+), 30 deletions(-) diff --git a/TombLib/TombLib/LevelData/Compilers/TombEngine/TombEngineTexInfoManager.cs b/TombLib/TombLib/LevelData/Compilers/TombEngine/TombEngineTexInfoManager.cs index 445eae2f41..10955e3897 100644 --- a/TombLib/TombLib/LevelData/Compilers/TombEngine/TombEngineTexInfoManager.cs +++ b/TombLib/TombLib/LevelData/Compilers/TombEngine/TombEngineTexInfoManager.cs @@ -73,6 +73,7 @@ private static HashSet GetCandidateSet() private List _referenceAnimTextures = new List(); private List _actualAnimTextures = new List(); + private HashSet<(Rectangle2 parentRect, Rectangle2 subRect)> _processedSubAreas = new HashSet<(Rectangle2, Rectangle2)>(); // UVRotate count should be placed after anim texture data to identify how many first anim seqs // should be processed using UVRotate engine function @@ -1299,11 +1300,7 @@ public Result AddTexture(TextureArea texture, TextureDestination destination, bo if (frame.Texture != texture.Texture) continue; - var fRect = new Rectangle2( - MathF.Min(frame.TexCoord0.X, MathF.Min(frame.TexCoord1.X, MathF.Min(frame.TexCoord2.X, frame.TexCoord3.X))), - MathF.Min(frame.TexCoord0.Y, MathF.Min(frame.TexCoord1.Y, MathF.Min(frame.TexCoord2.Y, frame.TexCoord3.Y))), - MathF.Max(frame.TexCoord0.X, MathF.Max(frame.TexCoord1.X, MathF.Max(frame.TexCoord2.X, frame.TexCoord3.X))), - MathF.Max(frame.TexCoord0.Y, MathF.Max(frame.TexCoord1.Y, MathF.Max(frame.TexCoord2.Y, frame.TexCoord3.Y)))); + var fRect = Rectangle2.FromCoordinates(frame.TexCoord0, frame.TexCoord1, frame.TexCoord2, frame.TexCoord3); if (MathC.WithinEpsilon(fRect.X0, parentRect.X0, _animTextureLookupMargin) && MathC.WithinEpsilon(fRect.Y0, parentRect.Y0, _animTextureLookupMargin) && @@ -1319,11 +1316,17 @@ public Result AddTexture(TextureArea texture, TextureDestination destination, bo continue; // Calculate relative sub-area position within the matched frame - var mfRect = new Rectangle2( - MathF.Min(matchedFrame.TexCoord0.X, MathF.Min(matchedFrame.TexCoord1.X, MathF.Min(matchedFrame.TexCoord2.X, matchedFrame.TexCoord3.X))), - MathF.Min(matchedFrame.TexCoord0.Y, MathF.Min(matchedFrame.TexCoord1.Y, MathF.Min(matchedFrame.TexCoord2.Y, matchedFrame.TexCoord3.Y))), - MathF.Max(matchedFrame.TexCoord0.X, MathF.Max(matchedFrame.TexCoord1.X, MathF.Max(matchedFrame.TexCoord2.X, matchedFrame.TexCoord3.X))), - MathF.Max(matchedFrame.TexCoord0.Y, MathF.Max(matchedFrame.TexCoord1.Y, MathF.Max(matchedFrame.TexCoord2.Y, matchedFrame.TexCoord3.Y)))); + var mfRect = Rectangle2.FromCoordinates(matchedFrame.TexCoord0, matchedFrame.TexCoord1, matchedFrame.TexCoord2, matchedFrame.TexCoord3); + + // Skip degenerate frames with zero-sized extents to avoid NaN/Infinity + if (mfRect.Width == 0 || mfRect.Height == 0) + continue; + + // Skip if this sub-area was already processed + var subAreaKey = (parentRect, subRect); + + if (!_processedSubAreas.Add(subAreaKey)) + continue; float relX0 = (subRect.X0 - mfRect.X0) / mfRect.Width; float relY0 = (subRect.Y0 - mfRect.Y0) / mfRect.Height; @@ -1336,11 +1339,7 @@ public Result AddTexture(TextureArea texture, TextureDestination destination, bo foreach (var frame in origSet.Frames) { - var frameRect = new Rectangle2( - MathF.Min(frame.TexCoord0.X, MathF.Min(frame.TexCoord1.X, MathF.Min(frame.TexCoord2.X, frame.TexCoord3.X))), - MathF.Min(frame.TexCoord0.Y, MathF.Min(frame.TexCoord1.Y, MathF.Min(frame.TexCoord2.Y, frame.TexCoord3.Y))), - MathF.Max(frame.TexCoord0.X, MathF.Max(frame.TexCoord1.X, MathF.Max(frame.TexCoord2.X, frame.TexCoord3.X))), - MathF.Max(frame.TexCoord0.Y, MathF.Max(frame.TexCoord1.Y, MathF.Max(frame.TexCoord2.Y, frame.TexCoord3.Y)))); + var frameRect = Rectangle2.FromCoordinates(frame.TexCoord0, frame.TexCoord1, frame.TexCoord2, frame.TexCoord3); float fw = frameRect.Width; float fh = frameRect.Height; diff --git a/TombLib/TombLib/LevelData/Compilers/Util/TexInfoManager.cs b/TombLib/TombLib/LevelData/Compilers/Util/TexInfoManager.cs index f5047a8b2b..5c15303d7c 100644 --- a/TombLib/TombLib/LevelData/Compilers/Util/TexInfoManager.cs +++ b/TombLib/TombLib/LevelData/Compilers/Util/TexInfoManager.cs @@ -52,6 +52,7 @@ public class TexInfoManager private List _referenceAnimTextures = new List(); private List _actualAnimTextures = new List(); + private HashSet<(Rectangle2 parentRect, Rectangle2 subRect)> _processedSubAreas = new HashSet<(Rectangle2, Rectangle2)>(); // UVRotate count should be placed after anim texture data to identify how many first anim seqs // should be processed using UVRotate engine function @@ -865,11 +866,7 @@ public Result AddTexture(TextureArea texture, bool isForRoom, bool isForTriangle if (frame.Texture != texture.Texture) continue; - var fRect = new Rectangle2( - MathF.Min(frame.TexCoord0.X, MathF.Min(frame.TexCoord1.X, MathF.Min(frame.TexCoord2.X, frame.TexCoord3.X))), - MathF.Min(frame.TexCoord0.Y, MathF.Min(frame.TexCoord1.Y, MathF.Min(frame.TexCoord2.Y, frame.TexCoord3.Y))), - MathF.Max(frame.TexCoord0.X, MathF.Max(frame.TexCoord1.X, MathF.Max(frame.TexCoord2.X, frame.TexCoord3.X))), - MathF.Max(frame.TexCoord0.Y, MathF.Max(frame.TexCoord1.Y, MathF.Max(frame.TexCoord2.Y, frame.TexCoord3.Y)))); + var fRect = Rectangle2.FromCoordinates(frame.TexCoord0, frame.TexCoord1, frame.TexCoord2, frame.TexCoord3); if (MathC.WithinEpsilon(fRect.X0, parentRect.X0, _animTextureLookupMargin) && MathC.WithinEpsilon(fRect.Y0, parentRect.Y0, _animTextureLookupMargin) && @@ -885,11 +882,17 @@ public Result AddTexture(TextureArea texture, bool isForRoom, bool isForTriangle continue; // Calculate relative sub-area position within the matched frame - var mfRect = new Rectangle2( - MathF.Min(matchedFrame.TexCoord0.X, MathF.Min(matchedFrame.TexCoord1.X, MathF.Min(matchedFrame.TexCoord2.X, matchedFrame.TexCoord3.X))), - MathF.Min(matchedFrame.TexCoord0.Y, MathF.Min(matchedFrame.TexCoord1.Y, MathF.Min(matchedFrame.TexCoord2.Y, matchedFrame.TexCoord3.Y))), - MathF.Max(matchedFrame.TexCoord0.X, MathF.Max(matchedFrame.TexCoord1.X, MathF.Max(matchedFrame.TexCoord2.X, matchedFrame.TexCoord3.X))), - MathF.Max(matchedFrame.TexCoord0.Y, MathF.Max(matchedFrame.TexCoord1.Y, MathF.Max(matchedFrame.TexCoord2.Y, matchedFrame.TexCoord3.Y)))); + var mfRect = Rectangle2.FromCoordinates(matchedFrame.TexCoord0, matchedFrame.TexCoord1, matchedFrame.TexCoord2, matchedFrame.TexCoord3); + + // Skip degenerate frames with zero-sized extents to avoid NaN/Infinity + if (mfRect.Width == 0 || mfRect.Height == 0) + continue; + + // Skip if this sub-area was already processed + var subAreaKey = (parentRect, subRect); + + if (!_processedSubAreas.Add(subAreaKey)) + continue; float relX0 = (subRect.X0 - mfRect.X0) / mfRect.Width; float relY0 = (subRect.Y0 - mfRect.Y0) / mfRect.Height; @@ -902,11 +905,7 @@ public Result AddTexture(TextureArea texture, bool isForRoom, bool isForTriangle foreach (var frame in origSet.Frames) { - var frameRect = new Rectangle2( - MathF.Min(frame.TexCoord0.X, MathF.Min(frame.TexCoord1.X, MathF.Min(frame.TexCoord2.X, frame.TexCoord3.X))), - MathF.Min(frame.TexCoord0.Y, MathF.Min(frame.TexCoord1.Y, MathF.Min(frame.TexCoord2.Y, frame.TexCoord3.Y))), - MathF.Max(frame.TexCoord0.X, MathF.Max(frame.TexCoord1.X, MathF.Max(frame.TexCoord2.X, frame.TexCoord3.X))), - MathF.Max(frame.TexCoord0.Y, MathF.Max(frame.TexCoord1.Y, MathF.Max(frame.TexCoord2.Y, frame.TexCoord3.Y)))); + var frameRect = Rectangle2.FromCoordinates(frame.TexCoord0, frame.TexCoord1, frame.TexCoord2, frame.TexCoord3); float fw = frameRect.Width; float fh = frameRect.Height; From 9a3d41f220ff5c8f1c061540b04774610703f462 Mon Sep 17 00:00:00 2001 From: Kewin Kupilas Date: Sun, 22 Mar 2026 21:55:34 +0000 Subject: [PATCH 3/7] Apply more Copilot suggestions --- .../TombEngine/TombEngineTexInfoManager.cs | 17 ++++++++--------- .../LevelData/Compilers/Util/TexInfoManager.cs | 13 +++++++------ 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/TombLib/TombLib/LevelData/Compilers/TombEngine/TombEngineTexInfoManager.cs b/TombLib/TombLib/LevelData/Compilers/TombEngine/TombEngineTexInfoManager.cs index 10955e3897..86e597164e 100644 --- a/TombLib/TombLib/LevelData/Compilers/TombEngine/TombEngineTexInfoManager.cs +++ b/TombLib/TombLib/LevelData/Compilers/TombEngine/TombEngineTexInfoManager.cs @@ -73,7 +73,9 @@ private static HashSet GetCandidateSet() private List _referenceAnimTextures = new List(); private List _actualAnimTextures = new List(); - private HashSet<(Rectangle2 parentRect, Rectangle2 subRect)> _processedSubAreas = new HashSet<(Rectangle2, Rectangle2)>(); + + private record SubAreaKey(Texture Texture, TextureDestination Destination, Rectangle2 ParentRect, Rectangle2 SubRect); + private HashSet _processedSubAreas = new HashSet(); // UVRotate count should be placed after anim texture data to identify how many first anim seqs // should be processed using UVRotate engine function @@ -1268,7 +1270,8 @@ public Result AddTexture(TextureArea texture, TextureDestination destination, bo // Check if this is a sub-area of an animated texture (e.g. applied via group texturing tools). // In this case, the actual UV coordinates represent a portion of the full animation frame, // so we reconstruct the full frame from ParentArea and match against reference animations. - if (!texture.ParentArea.IsZero && _referenceAnimTextures.Count > 0) + if (!texture.ParentArea.IsZero && _referenceAnimTextures.Count > 0 && + texture.ParentArea != texture.GetRect(isForTriangle)) { TextureArea fullTexture = texture; fullTexture.TexCoord0 = new Vector2(texture.ParentArea.X0, texture.ParentArea.Y0); @@ -1322,10 +1325,8 @@ public Result AddTexture(TextureArea texture, TextureDestination destination, bo if (mfRect.Width == 0 || mfRect.Height == 0) continue; - // Skip if this sub-area was already processed - var subAreaKey = (parentRect, subRect); - - if (!_processedSubAreas.Add(subAreaKey)) + // Skip if this sub-area was already processed for this texture and destination + if (!_processedSubAreas.Add(new SubAreaKey(texture.Texture, destination, parentRect, subRect))) continue; float relX0 = (subRect.X0 - mfRect.X0) / mfRect.Width; @@ -1357,9 +1358,7 @@ public Result AddTexture(TextureArea texture, TextureDestination destination, bo GenerateAnimLookups(new List { subSet }, destination); // Retry - the sub-area coordinates should now match the new reference lookups - var result = AddTexture(texture, destination, isForTriangle, blendMode); - result.Animated = true; - return result; + return AddTexture(texture, destination, isForTriangle, blendMode); } } } diff --git a/TombLib/TombLib/LevelData/Compilers/Util/TexInfoManager.cs b/TombLib/TombLib/LevelData/Compilers/Util/TexInfoManager.cs index 5c15303d7c..3a86cc6029 100644 --- a/TombLib/TombLib/LevelData/Compilers/Util/TexInfoManager.cs +++ b/TombLib/TombLib/LevelData/Compilers/Util/TexInfoManager.cs @@ -52,7 +52,9 @@ public class TexInfoManager private List _referenceAnimTextures = new List(); private List _actualAnimTextures = new List(); - private HashSet<(Rectangle2 parentRect, Rectangle2 subRect)> _processedSubAreas = new HashSet<(Rectangle2, Rectangle2)>(); + + private record SubAreaKey(Texture Texture, Rectangle2 ParentRect, Rectangle2 SubRect); + private HashSet _processedSubAreas = new HashSet(); // UVRotate count should be placed after anim texture data to identify how many first anim seqs // should be processed using UVRotate engine function @@ -834,7 +836,8 @@ public Result AddTexture(TextureArea texture, bool isForRoom, bool isForTriangle // Check if this is a sub-area of an animated texture (e.g. applied via group texturing tools). // In this case, the actual UV coordinates represent a portion of the full animation frame, // so we reconstruct the full frame from ParentArea and match against reference animations. - if (!texture.ParentArea.IsZero && _referenceAnimTextures.Count > 0) + if (!texture.ParentArea.IsZero && _referenceAnimTextures.Count > 0 && + texture.ParentArea != texture.GetRect(isForTriangle)) { TextureArea fullTexture = texture; fullTexture.TexCoord0 = new Vector2(texture.ParentArea.X0, texture.ParentArea.Y0); @@ -888,10 +891,8 @@ public Result AddTexture(TextureArea texture, bool isForRoom, bool isForTriangle if (mfRect.Width == 0 || mfRect.Height == 0) continue; - // Skip if this sub-area was already processed - var subAreaKey = (parentRect, subRect); - - if (!_processedSubAreas.Add(subAreaKey)) + // Skip if this sub-area was already processed for this texture + if (!_processedSubAreas.Add(new SubAreaKey(texture.Texture, parentRect, subRect))) continue; float relX0 = (subRect.X0 - mfRect.X0) / mfRect.Width; From cb7cc1f5401ed7831dc149410c7ab9011bc565f2 Mon Sep 17 00:00:00 2001 From: Kewin Kupilas Date: Sun, 22 Mar 2026 23:18:20 +0000 Subject: [PATCH 4/7] Massive refactor --- .../Compilers/AnimatedTextureLookupUtility.cs | 263 ++++++++++++++++++ .../TombEngine/TombEngineTexInfoManager.cs | 69 +---- .../Compilers/Util/TexInfoManager.cs | 75 +---- 3 files changed, 276 insertions(+), 131 deletions(-) create mode 100644 TombLib/TombLib/LevelData/Compilers/AnimatedTextureLookupUtility.cs diff --git a/TombLib/TombLib/LevelData/Compilers/AnimatedTextureLookupUtility.cs b/TombLib/TombLib/LevelData/Compilers/AnimatedTextureLookupUtility.cs new file mode 100644 index 0000000000..2fc063c324 --- /dev/null +++ b/TombLib/TombLib/LevelData/Compilers/AnimatedTextureLookupUtility.cs @@ -0,0 +1,263 @@ +#nullable enable + +using System; +using System.Diagnostics.CodeAnalysis; +using System.Numerics; +using TombLib.Utils; + +namespace TombLib.LevelData.Compilers +{ + internal static class AnimatedTextureLookupUtility + { + private static float NormalizeLookupCoordinate(float value, float margin) + => (float)(Math.Round(value / margin) * margin); + + /// + /// Quantizes a rectangle to the lookup margin so equivalent UV bounds collapse to a stable cache key. + /// + /// The rectangle to normalize. + /// The quantization step used by animated texture lookup comparisons. + /// A rectangle snapped to the lookup grid defined by . + public static Rectangle2 NormalizeLookupRectangle(Rectangle2 rect, float margin) => new( + NormalizeLookupCoordinate(rect.Start.X, margin), + NormalizeLookupCoordinate(rect.Start.Y, margin), + NormalizeLookupCoordinate(rect.End.X, margin), + NormalizeLookupCoordinate(rect.End.Y, margin) + ); + + /// + /// Determines whether two textures should be treated as the same logical texture for animated lookup purposes. + /// + /// The first texture to compare. + /// The second texture to compare. + /// when both textures resolve to the same identity; otherwise . + public static bool AreEquivalentTextures(Texture first, Texture second) + { + if (ReferenceEquals(first, second)) + return true; + + if (!string.IsNullOrEmpty(first.AbsolutePath) && !string.IsNullOrEmpty(second.AbsolutePath)) + return first.AbsolutePath.Equals(second.AbsolutePath, StringComparison.OrdinalIgnoreCase); + + if (first is TextureHashed firstHashed) + return second is TextureHashed secondHashed && firstHashed.Hash == secondHashed.Hash; + + if (second is TextureHashed) + return false; + + return first.Equals(second); + } + + /// + /// Builds a stable hash for a texture identity using the same precedence as . + /// + /// The texture whose identity hash should be computed. + /// A hash code suitable for deduplication keys. + public static int GetTextureIdentityHash(Texture texture) + { + if (!string.IsNullOrEmpty(texture.AbsolutePath)) + return StringComparer.OrdinalIgnoreCase.GetHashCode(texture.AbsolutePath); + + if (texture is TextureHashed hashed) + return hashed.Hash.GetHashCode(); + + return texture.GetHashCode(); + } + + /// + /// Checks whether two rectangles match within the specified per-edge tolerance. + /// + /// The first rectangle. + /// The second rectangle. + /// The allowed epsilon for each rectangle edge. + /// when all corresponding edges are within . + public static bool RectanglesMatch(Rectangle2 first, Rectangle2 second, float margin) + => MathC.WithinEpsilon(first.X0, second.X0, margin) && + MathC.WithinEpsilon(first.Y0, second.Y0, margin) && + MathC.WithinEpsilon(first.X1, second.X1, margin) && + MathC.WithinEpsilon(first.Y1, second.Y1, margin); + + private static float GetRectangleMatchScore(Rectangle2 first, Rectangle2 second) + => Math.Abs(first.X0 - second.X0) + + Math.Abs(first.Y0 - second.Y0) + + Math.Abs(first.X1 - second.X1) + + Math.Abs(first.Y1 - second.Y1); + + /// + /// Finds the closest frame in an animated set whose texture identity and bounds match the requested parent area. + /// + /// The animated texture set to scan. + /// The texture area whose source frame is being resolved. + /// The full parent rectangle that should match one frame in the set. + /// The matching tolerance for rectangle comparison. + /// The best matching frame, or when no acceptable match exists. + public static AnimatedTextureFrame? FindBestMatchingAnimatedFrame(AnimatedTextureSet set, TextureArea texture, Rectangle2 parentRect, float margin) + { + AnimatedTextureFrame? bestFrame = null; + float bestScore = float.MaxValue; + + foreach (var frame in set.Frames) + { + if (!AreEquivalentTextures(frame.Texture, texture.Texture)) + continue; + + var frameRect = Rectangle2.FromCoordinates(frame.TexCoord0, frame.TexCoord1, frame.TexCoord2, frame.TexCoord3); + + if (!RectanglesMatch(frameRect, parentRect, margin)) + continue; + + var score = GetRectangleMatchScore(frameRect, parentRect); + + if (score >= bestScore) + continue; + + bestScore = score; + bestFrame = frame; + + if (score == 0.0f) + break; + } + + return bestFrame; + } + + /// + /// Rebuilds a texture area so its UVs cover the full stored parent area instead of the current sub-area. + /// + /// The texture area whose parent bounds should become the full UV rectangle. + /// A copy of expanded to its full parent area. + public static TextureArea CreateFullParentAreaTexture(TextureArea texture) + { + TextureArea fullTexture = texture; + fullTexture.TexCoord0 = new Vector2(texture.ParentArea.X0, texture.ParentArea.Y0); + fullTexture.TexCoord1 = new Vector2(texture.ParentArea.X0, texture.ParentArea.Y1); + fullTexture.TexCoord2 = new Vector2(texture.ParentArea.X1, texture.ParentArea.Y1); + fullTexture.TexCoord3 = new Vector2(texture.ParentArea.X1, texture.ParentArea.Y0); + fullTexture.ParentArea = Rectangle2.Zero; + return fullTexture; + } + + /// + /// Creates a synthetic animated texture set whose frames are cropped to the same relative sub-area as the input texture. + /// + /// The source animated texture set. + /// The texture area that defines the desired sub-area. + /// The full parent rectangle expected to match a frame in . + /// The actual sub-rectangle that should be projected onto every frame. + /// The matching tolerance for resolving the source frame. + /// Receives the generated sub-area animation set when the method succeeds. + /// when a valid sub-area animation set was generated; otherwise . + public static bool TryCreateSubAreaAnimationSet( + AnimatedTextureSet originalSet, + TextureArea texture, + Rectangle2 parentRect, + Rectangle2 subRect, + float margin, + [NotNullWhen(true)] out AnimatedTextureSet? subSet) + { + subSet = null; + + AnimatedTextureFrame? matchedFrame = FindBestMatchingAnimatedFrame(originalSet, texture, parentRect, margin); + + if (matchedFrame is null) + return false; + + var matchedFrameRect = Rectangle2.FromCoordinates(matchedFrame.TexCoord0, matchedFrame.TexCoord1, matchedFrame.TexCoord2, matchedFrame.TexCoord3); + + if (matchedFrameRect.Width == 0 || matchedFrameRect.Height == 0) + return false; + + float relX0 = (subRect.X0 - matchedFrameRect.X0) / matchedFrameRect.Width; + float relY0 = (subRect.Y0 - matchedFrameRect.Y0) / matchedFrameRect.Height; + float relX1 = (subRect.X1 - matchedFrameRect.X0) / matchedFrameRect.Width; + float relY1 = (subRect.Y1 - matchedFrameRect.Y0) / matchedFrameRect.Height; + + subSet = originalSet.Clone(); + + foreach (var subFrame in subSet.Frames) + { + var frameRect = Rectangle2.FromCoordinates(subFrame.TexCoord0, subFrame.TexCoord1, subFrame.TexCoord2, subFrame.TexCoord3); + + float frameWidth = frameRect.Width; + float frameHeight = frameRect.Height; + + subFrame.TexCoord0 = new Vector2(frameRect.X0 + relX0 * frameWidth, frameRect.Y0 + relY0 * frameHeight); + subFrame.TexCoord1 = new Vector2(frameRect.X0 + relX0 * frameWidth, frameRect.Y0 + relY1 * frameHeight); + subFrame.TexCoord2 = new Vector2(frameRect.X0 + relX1 * frameWidth, frameRect.Y0 + relY1 * frameHeight); + subFrame.TexCoord3 = new Vector2(frameRect.X0 + relX1 * frameWidth, frameRect.Y0 + relY0 * frameHeight); + } + + return true; + } + } + + internal readonly struct SubAreaKey : IEquatable + { + private readonly int _destinationKey; + + public readonly Texture Texture; + public readonly Rectangle2 ParentRect; + public readonly Rectangle2 SubRect; + + /// + /// Creates a cache key for a sub-area lookup that does not vary by texture destination. + /// + /// The texture identity to track. + /// The full parent rectangle of the animated frame. + /// The cropped sub-area rectangle. + /// The rectangle quantization step used for lookup deduplication. + public SubAreaKey(Texture texture, Rectangle2 parentRect, Rectangle2 subRect, float margin) + : this(texture, 0, parentRect, subRect, margin) + { } + + /// + /// Creates a cache key for a sub-area lookup scoped to the classic room versus object destination split. + /// + /// The texture identity to track. + /// for room textures; for object textures. + /// The full parent rectangle of the animated frame. + /// The cropped sub-area rectangle. + /// The rectangle quantization step used for lookup deduplication. + public SubAreaKey(Texture texture, bool isForRoom, Rectangle2 parentRect, Rectangle2 subRect, float margin) + : this(texture, isForRoom ? 1 : 2, parentRect, subRect, margin) + { } + + /// + /// Creates a cache key for a sub-area lookup scoped to a specific texture destination. + /// + /// The texture identity to track. + /// The destination bucket that the generated lookup belongs to. + /// The full parent rectangle of the animated frame. + /// The cropped sub-area rectangle. + /// The rectangle quantization step used for lookup deduplication. + public SubAreaKey(Texture texture, TextureDestination destination, Rectangle2 parentRect, Rectangle2 subRect, float margin) + : this(texture, (int)destination + 1, parentRect, subRect, margin) + { } + + private SubAreaKey(Texture texture, int destinationKey, Rectangle2 parentRect, Rectangle2 subRect, float margin) + { + Texture = texture; + _destinationKey = destinationKey; + ParentRect = AnimatedTextureLookupUtility.NormalizeLookupRectangle(parentRect, margin); + SubRect = AnimatedTextureLookupUtility.NormalizeLookupRectangle(subRect, margin); + } + + /// + /// Compares two sub-area keys using normalized bounds, destination scope, and logical texture identity. + /// + /// The key to compare against. + /// when both keys refer to the same deduplicated sub-area lookup. + public bool Equals(SubAreaKey other) + { + if (_destinationKey != other._destinationKey || ParentRect != other.ParentRect || SubRect != other.SubRect) + return false; + + return AnimatedTextureLookupUtility.AreEquivalentTextures(Texture, other.Texture); + } + + public override bool Equals(object? obj) => obj is SubAreaKey key && Equals(key); + + public override int GetHashCode() + => HashCode.Combine(_destinationKey, ParentRect, SubRect, AnimatedTextureLookupUtility.GetTextureIdentityHash(Texture)); + } +} diff --git a/TombLib/TombLib/LevelData/Compilers/TombEngine/TombEngineTexInfoManager.cs b/TombLib/TombLib/LevelData/Compilers/TombEngine/TombEngineTexInfoManager.cs index 86e597164e..a93060eeb3 100644 --- a/TombLib/TombLib/LevelData/Compilers/TombEngine/TombEngineTexInfoManager.cs +++ b/TombLib/TombLib/LevelData/Compilers/TombEngine/TombEngineTexInfoManager.cs @@ -74,7 +74,6 @@ private static HashSet GetCandidateSet() private List _referenceAnimTextures = new List(); private List _actualAnimTextures = new List(); - private record SubAreaKey(Texture Texture, TextureDestination Destination, Rectangle2 ParentRect, Rectangle2 SubRect); private HashSet _processedSubAreas = new HashSet(); // UVRotate count should be placed after anim texture data to identify how many first anim seqs @@ -1260,9 +1259,6 @@ public Result AddTexture(TextureArea texture, TextureDestination destination, bo { GenerateAnimTexture(refTex, refQuad, destination, isForTriangle); var result = AddTexture(texture, destination, isForTriangle, blendMode); - { - result.Animated = true; - } return new Result() { ConvertToQuad = false, Rotation = result.Rotation, TexInfoIndex = result.TexInfoIndex, Animated = true }; } } @@ -1273,12 +1269,7 @@ public Result AddTexture(TextureArea texture, TextureDestination destination, bo if (!texture.ParentArea.IsZero && _referenceAnimTextures.Count > 0 && texture.ParentArea != texture.GetRect(isForTriangle)) { - TextureArea fullTexture = texture; - fullTexture.TexCoord0 = new Vector2(texture.ParentArea.X0, texture.ParentArea.Y0); - fullTexture.TexCoord1 = new Vector2(texture.ParentArea.X0, texture.ParentArea.Y1); - fullTexture.TexCoord2 = new Vector2(texture.ParentArea.X1, texture.ParentArea.Y1); - fullTexture.TexCoord3 = new Vector2(texture.ParentArea.X1, texture.ParentArea.Y0); - fullTexture.ParentArea = Rectangle2.Zero; + TextureArea fullTexture = AnimatedTextureLookupUtility.CreateFullParentAreaTexture(texture); foreach (var refTex in _referenceAnimTextures) { @@ -1295,64 +1286,12 @@ public Result AddTexture(TextureArea texture, TextureDestination destination, bo var parentRect = texture.ParentArea; var subRect = texture.GetRect(isForTriangle); - // Find which frame in the set matches our ParentArea - AnimatedTextureFrame matchedFrame = null; - - foreach (var frame in origSet.Frames) - { - if (frame.Texture != texture.Texture) - continue; - - var fRect = Rectangle2.FromCoordinates(frame.TexCoord0, frame.TexCoord1, frame.TexCoord2, frame.TexCoord3); - - if (MathC.WithinEpsilon(fRect.X0, parentRect.X0, _animTextureLookupMargin) && - MathC.WithinEpsilon(fRect.Y0, parentRect.Y0, _animTextureLookupMargin) && - MathC.WithinEpsilon(fRect.X1, parentRect.X1, _animTextureLookupMargin) && - MathC.WithinEpsilon(fRect.Y1, parentRect.Y1, _animTextureLookupMargin)) - { - matchedFrame = frame; - break; - } - } - - if (matchedFrame == null) - continue; - - // Calculate relative sub-area position within the matched frame - var mfRect = Rectangle2.FromCoordinates(matchedFrame.TexCoord0, matchedFrame.TexCoord1, matchedFrame.TexCoord2, matchedFrame.TexCoord3); - - // Skip degenerate frames with zero-sized extents to avoid NaN/Infinity - if (mfRect.Width == 0 || mfRect.Height == 0) - continue; - // Skip if this sub-area was already processed for this texture and destination - if (!_processedSubAreas.Add(new SubAreaKey(texture.Texture, destination, parentRect, subRect))) + if (!_processedSubAreas.Add(new SubAreaKey(texture.Texture, destination, parentRect, subRect, _animTextureLookupMargin))) continue; - float relX0 = (subRect.X0 - mfRect.X0) / mfRect.Width; - float relY0 = (subRect.Y0 - mfRect.Y0) / mfRect.Height; - float relX1 = (subRect.X1 - mfRect.X0) / mfRect.Width; - float relY1 = (subRect.Y1 - mfRect.Y0) / mfRect.Height; - - // Create a synthetic AnimatedTextureSet with sub-area frames - var subSet = origSet.Clone(); - subSet.Frames.Clear(); - - foreach (var frame in origSet.Frames) - { - var frameRect = Rectangle2.FromCoordinates(frame.TexCoord0, frame.TexCoord1, frame.TexCoord2, frame.TexCoord3); - - float fw = frameRect.Width; - float fh = frameRect.Height; - - var subFrame = frame.Clone(); - subFrame.TexCoord0 = new Vector2(frameRect.X0 + relX0 * fw, frameRect.Y0 + relY0 * fh); - subFrame.TexCoord1 = new Vector2(frameRect.X0 + relX0 * fw, frameRect.Y0 + relY1 * fh); - subFrame.TexCoord2 = new Vector2(frameRect.X0 + relX1 * fw, frameRect.Y0 + relY1 * fh); - subFrame.TexCoord3 = new Vector2(frameRect.X0 + relX1 * fw, frameRect.Y0 + relY0 * fh); - - subSet.Frames.Add(subFrame); - } + if (!AnimatedTextureLookupUtility.TryCreateSubAreaAnimationSet(origSet, texture, parentRect, subRect, _animTextureLookupMargin, out var subSet)) + continue; // Generate reference lookups for this sub-area animation set GenerateAnimLookups(new List { subSet }, destination); diff --git a/TombLib/TombLib/LevelData/Compilers/Util/TexInfoManager.cs b/TombLib/TombLib/LevelData/Compilers/Util/TexInfoManager.cs index 3a86cc6029..ec59d86890 100644 --- a/TombLib/TombLib/LevelData/Compilers/Util/TexInfoManager.cs +++ b/TombLib/TombLib/LevelData/Compilers/Util/TexInfoManager.cs @@ -10,6 +10,7 @@ using System.Numerics; using System.Threading.Tasks; using TombLib.IO; +using TombLib.LevelData.Compilers; using TombLib.Utils; using TombLib.Wad; @@ -53,7 +54,6 @@ public class TexInfoManager private List _referenceAnimTextures = new List(); private List _actualAnimTextures = new List(); - private record SubAreaKey(Texture Texture, Rectangle2 ParentRect, Rectangle2 SubRect); private HashSet _processedSubAreas = new HashSet(); // UVRotate count should be placed after anim texture data to identify how many first anim seqs @@ -571,7 +571,7 @@ public TexInfoManager(Level level, IProgressReporter progressReporter, int maxTi MaxTileSize = _minimumTileSize; } - GenerateAnimLookups(_level.Settings.AnimatedTextureSets); // Generate anim texture lookup table + GenerateAnimLookups(_level.Settings.AnimatedTextureSets, true); // Generate anim texture lookup table _generateTexInfos = true; // Set manager ready state } @@ -839,12 +839,7 @@ public Result AddTexture(TextureArea texture, bool isForRoom, bool isForTriangle if (!texture.ParentArea.IsZero && _referenceAnimTextures.Count > 0 && texture.ParentArea != texture.GetRect(isForTriangle)) { - TextureArea fullTexture = texture; - fullTexture.TexCoord0 = new Vector2(texture.ParentArea.X0, texture.ParentArea.Y0); - fullTexture.TexCoord1 = new Vector2(texture.ParentArea.X0, texture.ParentArea.Y1); - fullTexture.TexCoord2 = new Vector2(texture.ParentArea.X1, texture.ParentArea.Y1); - fullTexture.TexCoord3 = new Vector2(texture.ParentArea.X1, texture.ParentArea.Y0); - fullTexture.ParentArea = Rectangle2.Zero; + TextureArea fullTexture = AnimatedTextureLookupUtility.CreateFullParentAreaTexture(texture); foreach (var refTex in _referenceAnimTextures) { @@ -861,67 +856,15 @@ public Result AddTexture(TextureArea texture, bool isForRoom, bool isForTriangle var parentRect = texture.ParentArea; var subRect = texture.GetRect(isForTriangle); - // Find which frame in the set matches our ParentArea - AnimatedTextureFrame matchedFrame = null; - - foreach (var frame in origSet.Frames) - { - if (frame.Texture != texture.Texture) - continue; - - var fRect = Rectangle2.FromCoordinates(frame.TexCoord0, frame.TexCoord1, frame.TexCoord2, frame.TexCoord3); - - if (MathC.WithinEpsilon(fRect.X0, parentRect.X0, _animTextureLookupMargin) && - MathC.WithinEpsilon(fRect.Y0, parentRect.Y0, _animTextureLookupMargin) && - MathC.WithinEpsilon(fRect.X1, parentRect.X1, _animTextureLookupMargin) && - MathC.WithinEpsilon(fRect.Y1, parentRect.Y1, _animTextureLookupMargin)) - { - matchedFrame = frame; - break; - } - } - - if (matchedFrame == null) - continue; - - // Calculate relative sub-area position within the matched frame - var mfRect = Rectangle2.FromCoordinates(matchedFrame.TexCoord0, matchedFrame.TexCoord1, matchedFrame.TexCoord2, matchedFrame.TexCoord3); - - // Skip degenerate frames with zero-sized extents to avoid NaN/Infinity - if (mfRect.Width == 0 || mfRect.Height == 0) - continue; - // Skip if this sub-area was already processed for this texture - if (!_processedSubAreas.Add(new SubAreaKey(texture.Texture, parentRect, subRect))) + if (!_processedSubAreas.Add(new SubAreaKey(texture.Texture, isForRoom, parentRect, subRect, _animTextureLookupMargin))) continue; - float relX0 = (subRect.X0 - mfRect.X0) / mfRect.Width; - float relY0 = (subRect.Y0 - mfRect.Y0) / mfRect.Height; - float relX1 = (subRect.X1 - mfRect.X0) / mfRect.Width; - float relY1 = (subRect.Y1 - mfRect.Y0) / mfRect.Height; - - // Create a synthetic AnimatedTextureSet with sub-area frames - var subSet = origSet.Clone(); - subSet.Frames.Clear(); - - foreach (var frame in origSet.Frames) - { - var frameRect = Rectangle2.FromCoordinates(frame.TexCoord0, frame.TexCoord1, frame.TexCoord2, frame.TexCoord3); - - float fw = frameRect.Width; - float fh = frameRect.Height; - - var subFrame = frame.Clone(); - subFrame.TexCoord0 = new Vector2(frameRect.X0 + relX0 * fw, frameRect.Y0 + relY0 * fh); - subFrame.TexCoord1 = new Vector2(frameRect.X0 + relX0 * fw, frameRect.Y0 + relY1 * fh); - subFrame.TexCoord2 = new Vector2(frameRect.X0 + relX1 * fw, frameRect.Y0 + relY1 * fh); - subFrame.TexCoord3 = new Vector2(frameRect.X0 + relX1 * fw, frameRect.Y0 + relY0 * fh); - - subSet.Frames.Add(subFrame); - } + if (!AnimatedTextureLookupUtility.TryCreateSubAreaAnimationSet(origSet, texture, parentRect, subRect, _animTextureLookupMargin, out var subSet)) + continue; // Generate reference lookups for this sub-area animation set - GenerateAnimLookups(new List { subSet }); + GenerateAnimLookups(new List { subSet }, isForRoom); // Retry - the sub-area coordinates should now match the new reference lookups return AddTexture(texture, isForRoom, isForTriangle, topmostAndUnpadded); @@ -998,7 +941,7 @@ private void SortOutAlpha(TRVersion.Game version, List parent // Generates list of dummy lookup animated textures. - private void GenerateAnimLookups(List sets) + private void GenerateAnimLookups(List sets, bool isForRoom) { foreach (var set in sets) { @@ -1065,7 +1008,7 @@ private void GenerateAnimLookups(List sets) // Make frame, including repeat versions for (int i = 0; i < frame.Repeat; i++) { - AddTexture(newFrame, refAnim.CompiledAnimation, true, (triangleVariation > 0), set.AnimationType == AnimatedTextureAnimationType.UVRotate, index, set.IsUvRotate); + AddTexture(newFrame, refAnim.CompiledAnimation, isForRoom, (triangleVariation > 0), set.AnimationType == AnimatedTextureAnimationType.UVRotate, index, set.IsUvRotate); index++; } } From 90b0f9aaa8bc2bd6616ded06b7efbcb4b403fe35 Mon Sep 17 00:00:00 2001 From: Kewin Kupilas Date: Sun, 22 Mar 2026 23:28:58 +0000 Subject: [PATCH 5/7] Apply Copilot suggestion --- .../Compilers/TombEngine/TombEngineTexInfoManager.cs | 5 ++++- TombLib/TombLib/LevelData/Compilers/Util/TexInfoManager.cs | 5 ++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/TombLib/TombLib/LevelData/Compilers/TombEngine/TombEngineTexInfoManager.cs b/TombLib/TombLib/LevelData/Compilers/TombEngine/TombEngineTexInfoManager.cs index a93060eeb3..7fd2a3c14a 100644 --- a/TombLib/TombLib/LevelData/Compilers/TombEngine/TombEngineTexInfoManager.cs +++ b/TombLib/TombLib/LevelData/Compilers/TombEngine/TombEngineTexInfoManager.cs @@ -1270,9 +1270,12 @@ public Result AddTexture(TextureArea texture, TextureDestination destination, bo texture.ParentArea != texture.GetRect(isForTriangle)) { TextureArea fullTexture = AnimatedTextureLookupUtility.CreateFullParentAreaTexture(texture); + int initialReferenceAnimTextureCount = _referenceAnimTextures.Count; - foreach (var refTex in _referenceAnimTextures) + for (int i = 0; i < initialReferenceAnimTextureCount; i++) { + var refTex = _referenceAnimTextures[i]; + // UVRotate and Video animation types are incompatible with sub-area splitting // because they rely on specific frame arrangement assumptions (vertical strip scrolling // for UVRotate, sequential frame playback for Video) that break when coordinates diff --git a/TombLib/TombLib/LevelData/Compilers/Util/TexInfoManager.cs b/TombLib/TombLib/LevelData/Compilers/Util/TexInfoManager.cs index ec59d86890..830f771bdb 100644 --- a/TombLib/TombLib/LevelData/Compilers/Util/TexInfoManager.cs +++ b/TombLib/TombLib/LevelData/Compilers/Util/TexInfoManager.cs @@ -840,9 +840,12 @@ public Result AddTexture(TextureArea texture, bool isForRoom, bool isForTriangle texture.ParentArea != texture.GetRect(isForTriangle)) { TextureArea fullTexture = AnimatedTextureLookupUtility.CreateFullParentAreaTexture(texture); + int initialReferenceAnimTextureCount = _referenceAnimTextures.Count; - foreach (var refTex in _referenceAnimTextures) + for (int i = 0; i < initialReferenceAnimTextureCount; i++) { + var refTex = _referenceAnimTextures[i]; + // UVRotate and Video animation types are incompatible with sub-area splitting // because they rely on specific frame arrangement assumptions (vertical strip scrolling // for UVRotate, sequential frame playback for Video) that break when coordinates From 8ccd89d4121bac0c0cf5585eca6eecb0404015f0 Mon Sep 17 00:00:00 2001 From: Kewin Kupilas Date: Mon, 23 Mar 2026 00:00:09 +0000 Subject: [PATCH 6/7] Preserve ParentArea --- TombEditor/EditorActions.cs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/TombEditor/EditorActions.cs b/TombEditor/EditorActions.cs index b3ea287e37..cd5156475e 100644 --- a/TombEditor/EditorActions.cs +++ b/TombEditor/EditorActions.cs @@ -1847,8 +1847,6 @@ public static bool ApplyTexture(Room room, VectorInt2 pos, SectorFace face, Text if(!disableUndo) _editor.UndoManager.PushGeometryChanged(_editor.SelectedRoom); - texture.ParentArea = new Rectangle2(); - bool textureApplied = ApplyTextureToFace(room, pos, face, texture); if (textureApplied) @@ -2279,8 +2277,6 @@ public static void TexturizeAll(Room room, SectorSelection selection, TextureAre if (type == SectorFaceType.Ceiling) texture.Mirror(); RectangleInt2 area = selection.Valid ? selection.Area : _editor.SelectedRoom.LocalArea; - texture.ParentArea = new Rectangle2(); - for (int x = area.X0; x <= area.X1; x++) for (int z = area.Y0; z <= area.Y1; z++) { From be65355033a4a3a70ec28a928147c517a316e469 Mon Sep 17 00:00:00 2001 From: Kewin Kupilas Date: Mon, 23 Mar 2026 00:29:20 +0000 Subject: [PATCH 7/7] Preserve animated set identity --- .../Compilers/TombEngine/TombEngineTexInfoManager.cs | 9 +++++---- .../TombLib/LevelData/Compilers/Util/TexInfoManager.cs | 9 +++++---- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/TombLib/TombLib/LevelData/Compilers/TombEngine/TombEngineTexInfoManager.cs b/TombLib/TombLib/LevelData/Compilers/TombEngine/TombEngineTexInfoManager.cs index 7fd2a3c14a..685499b410 100644 --- a/TombLib/TombLib/LevelData/Compilers/TombEngine/TombEngineTexInfoManager.cs +++ b/TombLib/TombLib/LevelData/Compilers/TombEngine/TombEngineTexInfoManager.cs @@ -1296,8 +1296,9 @@ public Result AddTexture(TextureArea texture, TextureDestination destination, bo if (!AnimatedTextureLookupUtility.TryCreateSubAreaAnimationSet(origSet, texture, parentRect, subRect, _animTextureLookupMargin, out var subSet)) continue; - // Generate reference lookups for this sub-area animation set - GenerateAnimLookups(new List { subSet }, destination); + // Generate reference lookups for this sub-area animation set while preserving + // the original animated set identity for downstream metadata consumers. + GenerateAnimLookups(new List { subSet }, destination, origSet); // Retry - the sub-area coordinates should now match the new reference lookups return AddTexture(texture, destination, isForTriangle, blendMode); @@ -1379,7 +1380,7 @@ private void SortOutAlpha(List parentList) // Generates list of dummy lookup animated textures. - private void GenerateAnimLookups(List sets, TextureDestination destination) + private void GenerateAnimLookups(List sets, TextureDestination destination, AnimatedTextureSet exportOriginOverride = null) { foreach (var set in sets) { @@ -1398,7 +1399,7 @@ private void GenerateAnimLookups(List sets, TextureDestinati while (true) { - var refAnim = new ParentAnimatedTexture(set); + var refAnim = new ParentAnimatedTexture(exportOriginOverride ?? set); int index = 0; foreach (var frame in set.Frames) diff --git a/TombLib/TombLib/LevelData/Compilers/Util/TexInfoManager.cs b/TombLib/TombLib/LevelData/Compilers/Util/TexInfoManager.cs index 830f771bdb..e5362ced2a 100644 --- a/TombLib/TombLib/LevelData/Compilers/Util/TexInfoManager.cs +++ b/TombLib/TombLib/LevelData/Compilers/Util/TexInfoManager.cs @@ -866,8 +866,9 @@ public Result AddTexture(TextureArea texture, bool isForRoom, bool isForTriangle if (!AnimatedTextureLookupUtility.TryCreateSubAreaAnimationSet(origSet, texture, parentRect, subRect, _animTextureLookupMargin, out var subSet)) continue; - // Generate reference lookups for this sub-area animation set - GenerateAnimLookups(new List { subSet }, isForRoom); + // Generate reference lookups for this sub-area animation set while preserving + // the original animated set identity for downstream exporters such as TRNG. + GenerateAnimLookups(new List { subSet }, isForRoom, origSet); // Retry - the sub-area coordinates should now match the new reference lookups return AddTexture(texture, isForRoom, isForTriangle, topmostAndUnpadded); @@ -944,7 +945,7 @@ private void SortOutAlpha(TRVersion.Game version, List parent // Generates list of dummy lookup animated textures. - private void GenerateAnimLookups(List sets, bool isForRoom) + private void GenerateAnimLookups(List sets, bool isForRoom, AnimatedTextureSet exportOriginOverride = null) { foreach (var set in sets) { @@ -963,7 +964,7 @@ private void GenerateAnimLookups(List sets, bool isForRoom) while (true) { - var refAnim = new ParentAnimatedTexture(set); + var refAnim = new ParentAnimatedTexture(exportOriginOverride ?? set); int index = 0; foreach (var frame in set.Frames)