-
Notifications
You must be signed in to change notification settings - Fork 32
Fixed animated group textures not animating #1124
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: develop
Are you sure you want to change the base?
Changes from all commits
635b81f
9f4970e
9a3d41f
cb7cc1f
90b0f9a
8ccd89d
be65355
f875ab2
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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(); | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. As above. Additionally, I don't understand how
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
|
|
||
| for (int x = area.X0; x <= area.X1; x++) | ||
| for (int z = area.Y0; z <= area.Y1; z++) | ||
| { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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); | ||
|
|
||
| /// <summary> | ||
| /// Quantizes a rectangle to the lookup margin so equivalent UV bounds collapse to a stable cache key. | ||
| /// </summary> | ||
| /// <param name="rect">The rectangle to normalize.</param> | ||
| /// <param name="margin">The quantization step used by animated texture lookup comparisons.</param> | ||
| /// <returns>A rectangle snapped to the lookup grid defined by <paramref name="margin"/>.</returns> | ||
| internal 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) | ||
| ); | ||
|
|
||
| /// <summary> | ||
| /// Determines whether two textures should be treated as the same logical texture for animated lookup purposes. | ||
| /// </summary> | ||
| /// <param name="first">The first texture to compare.</param> | ||
| /// <param name="second">The second texture to compare.</param> | ||
| /// <returns><see langword="true"/> when both textures resolve to the same identity; otherwise <see langword="false"/>.</returns> | ||
| internal 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); | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Builds a stable hash for a texture identity using the same precedence as <see cref="AreEquivalentTextures"/>. | ||
| /// </summary> | ||
| /// <param name="texture">The texture whose identity hash should be computed.</param> | ||
| /// <returns>A hash code suitable for deduplication keys.</returns> | ||
| internal 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(); | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Checks whether two rectangles match within the specified per-edge tolerance. | ||
| /// </summary> | ||
| /// <param name="first">The first rectangle.</param> | ||
| /// <param name="second">The second rectangle.</param> | ||
| /// <param name="margin">The allowed epsilon for each rectangle edge.</param> | ||
| /// <returns><see langword="true"/> when all corresponding edges are within <paramref name="margin"/>.</returns> | ||
| private 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); | ||
|
|
||
| /// <summary> | ||
| /// Finds the closest frame in an animated set whose texture identity and bounds match the requested parent area. | ||
| /// </summary> | ||
| /// <param name="set">The animated texture set to scan.</param> | ||
| /// <param name="texture">The texture area whose source frame is being resolved.</param> | ||
| /// <param name="parentRect">The full parent rectangle that should match one frame in the set.</param> | ||
| /// <param name="margin">The matching tolerance for rectangle comparison.</param> | ||
| /// <returns>The best matching frame, or <see langword="null"/> when no acceptable match exists.</returns> | ||
| private 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; | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Rebuilds a texture area so its UVs cover the full stored parent area instead of the current sub-area. | ||
| /// </summary> | ||
| /// <param name="texture">The texture area whose parent bounds should become the full UV rectangle.</param> | ||
| /// <returns>A copy of <paramref name="texture"/> expanded to its full parent area.</returns> | ||
| internal 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; | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Creates a synthetic animated texture set whose frames are cropped to the same relative sub-area as the input texture. | ||
| /// </summary> | ||
| /// <param name="originalSet">The source animated texture set.</param> | ||
| /// <param name="texture">The texture area that defines the desired sub-area.</param> | ||
| /// <param name="parentRect">The full parent rectangle expected to match a frame in <paramref name="originalSet"/>.</param> | ||
| /// <param name="subRect">The actual sub-rectangle that should be projected onto every frame.</param> | ||
| /// <param name="margin">The matching tolerance for resolving the source frame.</param> | ||
| /// <param name="subSet">Receives the generated sub-area animation set when the method succeeds.</param> | ||
| /// <returns><see langword="true"/> when a valid sub-area animation set was generated; otherwise <see langword="false"/>.</returns> | ||
| internal 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<SubAreaKey> | ||
| { | ||
| private readonly int _destinationKey; | ||
|
|
||
| public readonly Texture Texture; | ||
| public readonly Rectangle2 ParentRect; | ||
| public readonly Rectangle2 SubRect; | ||
|
|
||
| /// <summary> | ||
| /// Creates a cache key for a sub-area lookup that does not vary by texture destination. | ||
| /// </summary> | ||
| /// <param name="texture">The texture identity to track.</param> | ||
| /// <param name="parentRect">The full parent rectangle of the animated frame.</param> | ||
| /// <param name="subRect">The cropped sub-area rectangle.</param> | ||
| /// <param name="margin">The rectangle quantization step used for lookup deduplication.</param> | ||
| public SubAreaKey(Texture texture, Rectangle2 parentRect, Rectangle2 subRect, float margin) | ||
| : this(texture, 0, parentRect, subRect, margin) | ||
| { } | ||
|
|
||
| /// <summary> | ||
| /// Creates a cache key for a sub-area lookup scoped to the classic room versus object destination split. | ||
| /// </summary> | ||
| /// <param name="texture">The texture identity to track.</param> | ||
| /// <param name="isForRoom"><see langword="true"/> for room textures; <see langword="false"/> for object textures.</param> | ||
| /// <param name="parentRect">The full parent rectangle of the animated frame.</param> | ||
| /// <param name="subRect">The cropped sub-area rectangle.</param> | ||
| /// <param name="margin">The rectangle quantization step used for lookup deduplication.</param> | ||
| public SubAreaKey(Texture texture, bool isForRoom, Rectangle2 parentRect, Rectangle2 subRect, float margin) | ||
| : this(texture, isForRoom ? 1 : 2, parentRect, subRect, margin) | ||
| { } | ||
|
|
||
| /// <summary> | ||
| /// Creates a cache key for a sub-area lookup scoped to a specific texture destination. | ||
| /// </summary> | ||
| /// <param name="texture">The texture identity to track.</param> | ||
| /// <param name="destination">The destination bucket that the generated lookup belongs to.</param> | ||
| /// <param name="parentRect">The full parent rectangle of the animated frame.</param> | ||
| /// <param name="subRect">The cropped sub-area rectangle.</param> | ||
| /// <param name="margin">The rectangle quantization step used for lookup deduplication.</param> | ||
| 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); | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Compares two sub-area keys using normalized bounds, destination scope, and logical texture identity. | ||
| /// </summary> | ||
| /// <param name="other">The key to compare against.</param> | ||
| /// <returns><see langword="true"/> when both keys refer to the same deduplicated sub-area lookup.</returns> | ||
| 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)); | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not sure if this is correct, resetting ParentArea was supposed to introduce padding for textures that were applied directly from the texture panel with non-group tools.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@copilot Could you please verify this claim above? Did the
texture.ParentArea = new Rectangle2();line really do anything meaningful? Removing it allows to copy part of an animated group texture and apply it elsewhere, but has that caused any regressions in another place in the code, namely padding for textures that were applied directly from the texture panel with non-group tools? Please check all removals of that line in the file. Also, please check the removal ofresult.Animated = true;and whether that causes any regressions.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I verified both removals of
texture.ParentArea = new Rectangle2();inEditorActions.cs. For direct texture-panel usage,SelectedTexture.ParentAreais already zero, so those resets were effectively no-ops for normal non-group tools; padding behavior still comes from compiler packing logic, not this reset. KeepingParentAreais required when the source texture was copied from a grouped/partial animated frame, otherwise sub-area animation context is lost. I also re-checked theresult.Animated = trueremoval in the sub-area retry path: the recursiveAddTexture(...)now decides animation state from actual matching, which avoids false positives and does not regress valid animated matches. Commit:f875ab2.