feat(anim): carefully integrate AnimationSequencer into TickAnimations
Rewritten AnimationSequencer from the decompiled pseudocode, then
carefully integrated into GameWindow using the same surgical approach:
only replace the frame interpolation source, keep the transform
composition pipeline (scale → rotate → translate → entity scale →
MeshRef) UNTOUCHED.
Key differences from the reverted attempt (e4dae3d):
- Sequencer returns raw PartTransform (Origin, Orientation) — NOT
pre-composed matrices. The existing transform pipeline consumes
these identically to the legacy slerp output.
- Legacy slerp path is kept as explicit fallback for entities
without a MotionTable.
- SetCycle called from both UpdatePlayerAnimation and
OnLiveMotionUpdated — adjust_motion handles left→right remapping
internally with negative framerate.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
78aef6d575
commit
a57c5ccb76
1 changed files with 96 additions and 43 deletions
|
|
@ -91,8 +91,11 @@ public sealed class GameWindow : IDisposable
|
|||
/// </summary>
|
||||
public required IReadOnlyList<(uint GfxObjId, IReadOnlyDictionary<uint, uint>? SurfaceOverrides)> PartTemplate;
|
||||
public float CurrFrame; // monotonically increasing within [LowFrame, HighFrame]
|
||||
public AcDream.Core.Physics.AnimationSequencer? Sequencer;
|
||||
}
|
||||
|
||||
private AcDream.Core.Physics.DatCollectionLoader? _animLoader;
|
||||
|
||||
// Phase B.2: player movement mode.
|
||||
private AcDream.App.Input.PlayerMovementController? _playerController;
|
||||
private AcDream.App.Rendering.ChaseCamera? _chaseCamera;
|
||||
|
|
@ -323,6 +326,7 @@ public sealed class GameWindow : IDisposable
|
|||
_cameraController.ModeChanged += OnCameraModeChanged;
|
||||
|
||||
_dats = new DatCollection(_datDir, DatAccessType.Read);
|
||||
_animLoader = new AcDream.Core.Physics.DatCollectionLoader(_dats);
|
||||
|
||||
uint centerLandblockId = 0xA9B4FFFFu;
|
||||
Console.WriteLine($"loading world view centered on 0x{centerLandblockId:X8}");
|
||||
|
|
@ -822,6 +826,24 @@ public sealed class GameWindow : IDisposable
|
|||
for (int i = 0; i < meshRefs.Count; i++)
|
||||
template[i] = (meshRefs[i].GfxObjId, meshRefs[i].SurfaceOverrides);
|
||||
|
||||
// Create an AnimationSequencer if we can load the MotionTable.
|
||||
AcDream.Core.Physics.AnimationSequencer? sequencer = null;
|
||||
if (_animLoader is not null)
|
||||
{
|
||||
uint mtableId = spawn.MotionTableId ?? (uint)setup.DefaultMotionTable;
|
||||
if (mtableId != 0)
|
||||
{
|
||||
var mtable = _dats.Get<DatReaderWriter.DBObjs.MotionTable>(mtableId);
|
||||
if (mtable is not null)
|
||||
{
|
||||
sequencer = new AcDream.Core.Physics.AnimationSequencer(setup, mtable, _animLoader);
|
||||
uint seqStyle = stanceOverride is > 0 ? (uint)stanceOverride.Value : (uint)mtable.DefaultStyle;
|
||||
uint seqMotion = commandOverride is > 0 ? (uint)commandOverride.Value : 0x41000003u;
|
||||
sequencer.SetCycle(seqStyle, seqMotion);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_animatedEntities[entity.Id] = new AnimatedEntity
|
||||
{
|
||||
Entity = entity,
|
||||
|
|
@ -833,6 +855,7 @@ public sealed class GameWindow : IDisposable
|
|||
Scale = scale,
|
||||
PartTemplate = template,
|
||||
CurrFrame = idleCycle.LowFrame,
|
||||
Sequencer = sequencer,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -920,6 +943,15 @@ public sealed class GameWindow : IDisposable
|
|||
if (!newCycleIsGood)
|
||||
return;
|
||||
|
||||
// Sequencer path
|
||||
if (ae.Sequencer is not null)
|
||||
{
|
||||
uint fullStyle = stance != 0 ? (0x80000000u | (uint)stance) : ae.Sequencer.CurrentStyle;
|
||||
uint fullMotion = command is > 0 ? (uint)command.Value : 0x41000003u;
|
||||
ae.Sequencer.SetCycle(fullStyle, fullMotion);
|
||||
}
|
||||
|
||||
// Legacy path
|
||||
ae.Animation = newCycle!.Animation;
|
||||
ae.LowFrame = Math.Max(0, newCycle.LowFrame);
|
||||
ae.HighFrame = Math.Min(newCycle.HighFrame, newCycle.Animation.PartFrames.Count - 1);
|
||||
|
|
@ -1837,40 +1869,28 @@ public sealed class GameWindow : IDisposable
|
|||
foreach (var kv in _animatedEntities)
|
||||
{
|
||||
var ae = kv.Value;
|
||||
int span = ae.HighFrame - ae.LowFrame;
|
||||
if (span <= 0) continue;
|
||||
|
||||
ae.CurrFrame += dt * ae.Framerate;
|
||||
// Wrap into [LowFrame, HighFrame]. Use a guarded modulo so
|
||||
// big dts (first frame after a stall) don't blow the loop.
|
||||
if (ae.CurrFrame > ae.HighFrame)
|
||||
// ── Get per-part (origin, orientation) from either sequencer or legacy ──
|
||||
IReadOnlyList<AcDream.Core.Physics.PartTransform>? seqFrames = null;
|
||||
if (ae.Sequencer is not null)
|
||||
{
|
||||
float over = ae.CurrFrame - ae.LowFrame;
|
||||
ae.CurrFrame = ae.LowFrame + (over % (span + 1));
|
||||
seqFrames = ae.Sequencer.Advance(dt);
|
||||
}
|
||||
else if (ae.CurrFrame < ae.LowFrame)
|
||||
else
|
||||
{
|
||||
ae.CurrFrame = ae.LowFrame;
|
||||
// Legacy path (entities without a MotionTable / sequencer).
|
||||
int span = ae.HighFrame - ae.LowFrame;
|
||||
if (span <= 0) continue;
|
||||
ae.CurrFrame += dt * ae.Framerate;
|
||||
if (ae.CurrFrame > ae.HighFrame)
|
||||
{
|
||||
float over = ae.CurrFrame - ae.LowFrame;
|
||||
ae.CurrFrame = ae.LowFrame + (over % (span + 1));
|
||||
}
|
||||
else if (ae.CurrFrame < ae.LowFrame)
|
||||
ae.CurrFrame = ae.LowFrame;
|
||||
}
|
||||
|
||||
// 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);
|
||||
var scaleMat = ae.Scale == 1.0f
|
||||
|
|
@ -1879,25 +1899,50 @@ public sealed class GameWindow : IDisposable
|
|||
|
||||
for (int i = 0; i < partCount; i++)
|
||||
{
|
||||
// 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)
|
||||
|
||||
if (seqFrames is not null)
|
||||
{
|
||||
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);
|
||||
// Sequencer path.
|
||||
if (i < seqFrames.Count)
|
||||
{
|
||||
origin = seqFrames[i].Origin;
|
||||
orientation = seqFrames[i].Orientation;
|
||||
}
|
||||
else
|
||||
{
|
||||
origin = System.Numerics.Vector3.Zero;
|
||||
orientation = System.Numerics.Quaternion.Identity;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
origin = System.Numerics.Vector3.Zero;
|
||||
orientation = System.Numerics.Quaternion.Identity;
|
||||
// Legacy slerp path.
|
||||
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;
|
||||
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;
|
||||
if (i < partFrames.Count)
|
||||
{
|
||||
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
|
||||
{
|
||||
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.
|
||||
|
|
@ -1907,8 +1952,8 @@ public sealed class GameWindow : IDisposable
|
|||
|
||||
var partTransform =
|
||||
System.Numerics.Matrix4x4.CreateScale(defaultScale) *
|
||||
System.Numerics.Matrix4x4.CreateFromQuaternion(frame.Orientation) *
|
||||
System.Numerics.Matrix4x4.CreateTranslation(frame.Origin);
|
||||
System.Numerics.Matrix4x4.CreateFromQuaternion(orientation) *
|
||||
System.Numerics.Matrix4x4.CreateTranslation(origin);
|
||||
|
||||
// Bake the entity's ObjScale on top, matching the hydration
|
||||
// order (PartTransform * scaleMat) — see comment in OnLiveEntitySpawned.
|
||||
|
|
@ -1996,8 +2041,16 @@ public sealed class GameWindow : IDisposable
|
|||
stanceOverride: NonCombatStance,
|
||||
commandOverride: cmdOverride);
|
||||
|
||||
if (cycle is null || cycle.Framerate == 0f || cycle.HighFrame <= cycle.LowFrame) return;
|
||||
// Sequencer path: SetCycle handles adjust_motion internally
|
||||
// (TurnLeft→TurnRight with negative speed, etc.)
|
||||
if (ae.Sequencer is not null)
|
||||
{
|
||||
uint fullStyle = 0x80000000u | (uint)NonCombatStance;
|
||||
ae.Sequencer.SetCycle(fullStyle, animCommand);
|
||||
}
|
||||
|
||||
// Legacy path: update the manual slerp fields (for entities without sequencer)
|
||||
if (cycle is null || cycle.Framerate == 0f || cycle.HighFrame <= cycle.LowFrame) return;
|
||||
ae.Animation = cycle.Animation;
|
||||
ae.LowFrame = Math.Max(0, cycle.LowFrame);
|
||||
ae.HighFrame = Math.Min(cycle.HighFrame, cycle.Animation.PartFrames.Count - 1);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue