fix(anim): hold the last frame at a cyclic seam instead of blending into frame 0 (Holtburg windmill flap-back); register AP-233
The Holtburg windmill (Setup 0x020003E5, cycle 0x0300061B) is a 60-frame quarter turn that loops by 4-blade symmetry: frame 59 and frame 0 are the same picture but 88.5 deg apart. BuildBlendedFrame wrapped the cyclic node's next-frame index to frame 0, so the seam slerped 88.5 deg backwards inside one 33 ms interval - the blades visibly flapped back every two seconds. Retail never blends animation frames at all: CPartArray::UpdateParts (0x005190F0) applies get_part_frame(floor(frame_number)), holding every authored frame for its interval and hard-cutting at the wrap. The render-side blend now holds the boundary frame at BOTH ends of a node's window - the same rule the #61 link-tail fix already applied to one-shot nodes - so every seam is retail's cut while interior frames stay smooth (the owner's choice over dropping the blend, 2026-08-23). Register row AP-233 records the blend as the deviation it has been since the R1-P5 cutover. Test: Advance_CyclicSeamHoldsLastFrameInsteadOfBlendingIntoFrame0 (fails on the previous code at ~45 deg, passes held at 90 deg). Core 4,696/0, App 6,068/0 hermetic (Release). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
2c2d57b25a
commit
51a5fe99ef
3 changed files with 82 additions and 12 deletions
|
|
@ -538,6 +538,65 @@ public sealed class AnimationSequencerTests
|
|||
+ "(would be ~5 if nextIdx still wrapped to link frame 0)");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Advance_CyclicSeamHoldsLastFrameInsteadOfBlendingIntoFrame0()
|
||||
{
|
||||
// Holtburg windmill (2026-08-23): cycle 0x0300061B is a 60-frame
|
||||
// quarter turn (1.5 deg per frame about the blade axis) that loops
|
||||
// by 4-blade symmetry — frame 59 and frame 0 are the same picture
|
||||
// but 88.5 deg apart numerically. Blending frame 59 → frame 0 at the
|
||||
// cyclic seam swept the blades BACKWARDS 88.5 deg inside one 33 ms
|
||||
// interval. Retail never blends (CPartArray::UpdateParts applies
|
||||
// get_part_frame(floor(frame))), so the seam is a hard cut; our
|
||||
// render-side blend must hold the last frame there exactly like the
|
||||
// #61 link tail does. Modelled with 4 frames about Y: 0, 30, 60, 90
|
||||
// deg — the fractional tail past frame 3 must stay at 90 deg, not
|
||||
// blend toward 0 deg.
|
||||
const uint Style = 0x003Du;
|
||||
const uint Motion = 0x0003u;
|
||||
const uint AnimId = 0x03000002u;
|
||||
|
||||
var anim = new Animation();
|
||||
for (int f = 0; f < 4; f++)
|
||||
{
|
||||
var pf = new AnimationFrame(1);
|
||||
pf.Frames.Add(new Frame
|
||||
{
|
||||
Origin = Vector3.Zero,
|
||||
Orientation = Quaternion.CreateFromAxisAngle(Vector3.UnitY, MathF.PI / 6f * f),
|
||||
});
|
||||
anim.PartFrames.Add(pf);
|
||||
}
|
||||
var setup = Fixtures.MakeSetup(1);
|
||||
var mt = new MotionTable();
|
||||
mt.DefaultStyle = (DRWMotionCommand)Style;
|
||||
mt.StyleDefaults[(DRWMotionCommand)Style] = (DRWMotionCommand)Motion;
|
||||
int cycleKey = (int)((Style << 16) | (Motion & 0xFFFFFFu));
|
||||
mt.Cycles[cycleKey] = new MotionData();
|
||||
QualifiedDataId<Animation> qid = AnimId;
|
||||
mt.Cycles[cycleKey].Anims.Add(new AnimData
|
||||
{
|
||||
AnimId = qid,
|
||||
LowFrame = 0,
|
||||
HighFrame = 3,
|
||||
Framerate = 10f,
|
||||
});
|
||||
var loader = new FakeLoader();
|
||||
loader.Register(AnimId, anim);
|
||||
var seq = new AnimationSequencer(setup, mt, loader);
|
||||
seq.SetCycle(Style, Motion);
|
||||
|
||||
// Frame position 3.5 at 10 fps: the fractional tail of the LAST frame.
|
||||
seq.Advance(0.35f);
|
||||
var transforms = seq.Advance(0.0001f);
|
||||
Assert.InRange(GetFramePosition(seq), 3.4, 3.7);
|
||||
|
||||
// Angle of the blended orientation about Y.
|
||||
Quaternion q = transforms[0].Orientation;
|
||||
float angleDeg = 2f * MathF.Atan2(MathF.Abs(q.Y), MathF.Abs(q.W)) * 180f / MathF.PI;
|
||||
Assert.InRange(angleDeg, 89f, 91f); // held at frame 3; a seam blend would give ~45 deg
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SetCycle_StopFromWalkBackward_FallsBackToWalkForwardStopLink()
|
||||
{
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue