diff --git a/src/AcDream.App/Rendering/GameWindow.cs b/src/AcDream.App/Rendering/GameWindow.cs index 4578f20..ba1c021 100644 --- a/src/AcDream.App/Rendering/GameWindow.cs +++ b/src/AcDream.App/Rendering/GameWindow.cs @@ -1002,10 +1002,23 @@ public sealed class GameWindow : IDisposable ae.CurrFrame = ae.LowFrame; } - int frameIdx = (int)ae.CurrFrame; - if (frameIdx < 0 || frameIdx >= ae.Animation.PartFrames.Count) + // Phase 6.5: blend between adjacent keyframes using the fractional + // part of CurrFrame so the animation is smooth at any framerate + // instead of snapping to integer frame indices. + int frameIdx = (int)Math.Floor(ae.CurrFrame); + if (frameIdx < ae.LowFrame || frameIdx > ae.HighFrame + || frameIdx >= ae.Animation.PartFrames.Count) frameIdx = ae.LowFrame; + + int nextIdx = frameIdx + 1; + if (nextIdx > ae.HighFrame || nextIdx >= ae.Animation.PartFrames.Count) + nextIdx = ae.LowFrame; // cycle wraps within [LowFrame, HighFrame] + + float t = ae.CurrFrame - frameIdx; + if (t < 0f) t = 0f; else if (t > 1f) t = 1f; + var partFrames = ae.Animation.PartFrames[frameIdx].Frames; + var partFramesNext = ae.Animation.PartFrames[nextIdx].Frames; int partCount = ae.PartTemplate.Count; var newMeshRefs = new List(partCount); @@ -1015,18 +1028,25 @@ public sealed class GameWindow : IDisposable for (int i = 0; i < partCount; i++) { - // Frame source: the animation's per-part transform when - // the index is in range, otherwise identity (parts that - // outnumber the animation's bone list — rare but defensive). - DatReaderWriter.Types.Frame frame; + // Slerp between the current and next keyframe per part. + // Out-of-range parts get an identity transform — defensive + // for setups whose part count exceeds the animation's bone + // count. + System.Numerics.Vector3 origin; + System.Numerics.Quaternion orientation; if (i < partFrames.Count) - frame = partFrames[i]; + { + var f0 = partFrames[i]; + var f1 = i < partFramesNext.Count ? partFramesNext[i] : f0; + origin = System.Numerics.Vector3.Lerp(f0.Origin, f1.Origin, t); + orientation = System.Numerics.Quaternion.Slerp(f0.Orientation, f1.Orientation, t); + } else - frame = new DatReaderWriter.Types.Frame - { - Origin = System.Numerics.Vector3.Zero, - Orientation = System.Numerics.Quaternion.Identity, - }; + { + origin = System.Numerics.Vector3.Zero; + orientation = System.Numerics.Quaternion.Identity; + } + var frame = new DatReaderWriter.Types.Frame { Origin = origin, Orientation = orientation }; // Per-part default scale from the Setup, matching SetupMesh.Flatten's // composition order: scale → rotate → translate.