feat(app): Phase 6.5 — slerp between adjacent keyframes for smooth animation
Phase 6.4 advanced CurrFrame as a float but only sampled the integer floor, so animations stepped instead of flowing. Phase 6.5 takes the fractional part as a blend factor t and slerps each part's orientation + lerps its origin between PartFrames[floor] and PartFrames[floor+1] (wrapping back to LowFrame at HighFrame). The result is smooth motion at any framerate without changing the cycle length or frame indices. Defensive: if a part index exceeds the keyframe's bone list (rare, typically when AnimPartChanges grew the part count above the animation's NumParts) it falls back to identity instead of throwing. 160 tests still green. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
f0fa067566
commit
225e75b8b4
1 changed files with 32 additions and 12 deletions
|
|
@ -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<AcDream.Core.World.MeshRef>(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.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue