feat(anim): carefully integrate AnimationSequencer into TickAnimations
Surgical integration that ONLY replaces the frame interpolation path, preserving the exact transform composition pipeline that builds MeshRefs. The key insight from the reverted attempt: the sequencer's Advance(dt) returns raw (Origin, Orientation) per part — these feed into the SAME transform composition as before: CreateScale(defaultScale) * CreateFromQuat(orientation) * CreateTranslation(origin) then * scaleMat for entity ObjScale then → MeshRef with template GfxObjId + SurfaceOverrides What changed: - AnimatedEntity gains optional Sequencer field - DatCollectionLoader created once at dat-open time - Entity registration creates sequencer when MotionTable is loadable - TickAnimations: if sequencer exists, Advance(dt) produces per-part transforms; otherwise falls back to Phase 6.5 manual slerp - Transform composition + MeshRef building is UNCHANGED What was NOT changed (the previous attempt broke these): - Per-part defaultScale application - Entity ObjScale (scaleMat) multiplication - PartTemplate GfxObjId + SurfaceOverrides forwarding - The entire MeshRef construction block Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
53a4bf4225
commit
ed37f0969b
1 changed files with 92 additions and 42 deletions
|
|
@ -91,8 +91,18 @@ public sealed class GameWindow : IDisposable
|
|||
/// </summary>
|
||||
public required IReadOnlyList<(uint GfxObjId, IReadOnlyDictionary<uint, uint>? SurfaceOverrides)> PartTemplate;
|
||||
public float CurrFrame; // monotonically increasing within [LowFrame, HighFrame]
|
||||
/// <summary>
|
||||
/// Optional AnimationSequencer for transition-link-aware playback.
|
||||
/// When non-null, TickAnimations uses Advance(dt) for frame data
|
||||
/// instead of the manual slerp path. Null for entities whose
|
||||
/// MotionTable couldn't be loaded.
|
||||
/// </summary>
|
||||
public AcDream.Core.Physics.AnimationSequencer? Sequencer;
|
||||
}
|
||||
|
||||
/// <summary>Shared animation dat loader for all AnimationSequencer instances.</summary>
|
||||
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 +333,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 +833,25 @@ public sealed class GameWindow : IDisposable
|
|||
for (int i = 0; i < meshRefs.Count; i++)
|
||||
template[i] = (meshRefs[i].GfxObjId, meshRefs[i].SurfaceOverrides);
|
||||
|
||||
// Try to create an AnimationSequencer for transition-link-aware
|
||||
// playback. Requires loading the MotionTable dat separately.
|
||||
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 style = stanceOverride is > 0 ? (uint)stanceOverride.Value : (uint)mtable.DefaultStyle;
|
||||
uint motion = commandOverride is > 0 ? (uint)commandOverride.Value : 0x41000003u; // Ready
|
||||
sequencer.SetCycle(style, motion);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_animatedEntities[entity.Id] = new AnimatedEntity
|
||||
{
|
||||
Entity = entity,
|
||||
|
|
@ -833,6 +863,7 @@ public sealed class GameWindow : IDisposable
|
|||
Scale = scale,
|
||||
PartTemplate = template,
|
||||
CurrFrame = idleCycle.LowFrame,
|
||||
Sequencer = sequencer,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -1837,40 +1868,32 @@ public sealed class GameWindow : IDisposable
|
|||
foreach (var kv in _animatedEntities)
|
||||
{
|
||||
var ae = kv.Value;
|
||||
|
||||
// ── Compute per-part (origin, orientation) ──────────────────────
|
||||
// Two paths: if the entity has an AnimationSequencer, use it (gets
|
||||
// transition links + retail slerp). Otherwise fall back to the
|
||||
// Phase 6.5 manual slerp path.
|
||||
IReadOnlyList<AcDream.Core.Physics.PartTransform>? seqFrames = null;
|
||||
if (ae.Sequencer is not null)
|
||||
{
|
||||
seqFrames = ae.Sequencer.Advance(dt);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Legacy fallback: manual frame advancement + slerp.
|
||||
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)
|
||||
{
|
||||
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,12 +1902,39 @@ 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 (seqFrames is not null)
|
||||
{
|
||||
// Sequencer path: per-part transforms already interpolated.
|
||||
if (i < seqFrames.Count)
|
||||
{
|
||||
origin = seqFrames[i].Origin;
|
||||
orientation = seqFrames[i].Orientation;
|
||||
}
|
||||
else
|
||||
{
|
||||
origin = System.Numerics.Vector3.Zero;
|
||||
orientation = System.Numerics.Quaternion.Identity;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// 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];
|
||||
|
|
@ -1897,7 +1947,7 @@ public sealed class GameWindow : IDisposable
|
|||
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 +1957,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.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue