R1: the unsound elided-ctor-byte argument survived at its canonical citation site (ChargenPreviewEntityBuilder's class doc, which the two corrected docs point at) and in the ledger row's Deliverables column, which contradicted its own review-status column. Both now carry the real evidence: InitializePage @0x0047FDD0 writes an explicit m_bZoomedIn = 0 at 0x004802C3. R2: the verified 180-degree initial heading (m_fCurHeading = 180f at 0x00480235 + SetPlayerHeading at 0x0048023F, cross-confirmed at gmBarberUI::PostInit and the summary page) now has a durable home in the CC6b-mount OWED list — without it the mount half ships a character facing away from the camera. Merge prep: the branch-local TS-82 renumbered to TS-84 (the CC4 branch independently allocated TS-82 and landed first) and the branch-local ISSUES #402 renumbered to #403 (same collision, same rule), with the Core doc reference updated. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
124 lines
5.4 KiB
C#
124 lines
5.4 KiB
C#
using System;
|
|
using System.Numerics;
|
|
using DatReaderWriter.DBObjs;
|
|
|
|
namespace AcDream.Core.Physics;
|
|
|
|
/// <summary>
|
|
/// Retail's simplest animation-clip playback shape: advance a frame position
|
|
/// at a fixed framerate and wrap it back into <c>[LowFrame, HighFrame]</c>,
|
|
/// then linearly interpolate one part's origin/orientation between the two
|
|
/// bracketing frames. This is the effect of
|
|
/// <c>CPhysicsObj::set_sequence_animation</c> (<c>0x0050F6F0</c>) when called
|
|
/// with a constant DID and a nonzero framerate and no further motion-command
|
|
/// traffic — e.g. <c>gmCG3DView::StartAnimation</c> (<c>0x004EE600</c>),
|
|
/// which plays the chargen preview's idle DID at a flat 30 fps with no
|
|
/// transitional blending.
|
|
///
|
|
/// <para>
|
|
/// This exact advance-with-wrap-then-lerp/slerp algorithm already exists as
|
|
/// an inline, App-layer-only implementation for the "legacy" (no
|
|
/// <see cref="AnimationSequencer"/>) NPC idle-cycle path —
|
|
/// <c>LiveEntityAnimationPresenter.Present</c>'s non-sequencer branch
|
|
/// (<c>CurrFrame += legacyAdvanceSeconds * Framerate</c> with the same
|
|
/// modulo wrap) and its private <c>TryResolvePartFrame</c> helper (the same
|
|
/// frame-bracket lerp/slerp). That call site has a live entity, a
|
|
/// <c>LiveEntityRuntime</c> membership, and per-tick elapsed time supplied by
|
|
/// the render loop; the chargen preview has none of that (there is no live
|
|
/// entity — character creation hasn't happened yet), so it cannot reuse that
|
|
/// class directly. Rather than re-typing the same formula a second time,
|
|
/// this Core, pure, unit-testable class is the shared primitive: the
|
|
/// chargen preview (<c>AcDream.App.Rendering.ChargenPreviewAnimator</c>)
|
|
/// consumes it directly, and it is safe for a future pass to redirect
|
|
/// <c>LiveEntityAnimationPresenter</c>'s inline copy through it as a
|
|
/// behavior-preserving mechanical follow-up (not done here — that file is
|
|
/// live, heavily tested production entity-rendering code with zero relation
|
|
/// to this preview-only feature, so touching it is out of this slice's
|
|
/// blast radius by design, not oversight). Tracked as
|
|
/// <c>docs/ISSUES.md</c> #403 so the follow-up has an owner.
|
|
/// </para>
|
|
/// </summary>
|
|
public static class RetailAnimationCyclePlayback
|
|
{
|
|
/// <summary>
|
|
/// Advances <paramref name="currFrame"/> by <c>elapsedSeconds * framerate</c>
|
|
/// and wraps it back into <c>[lowFrame, highFrame]</c> with the SAME modulo
|
|
/// shape <c>LiveEntityAnimationPresenter.Present</c>'s legacy branch uses
|
|
/// (<c>over % (span + 1)</c>, not a plain clamp — a frame position that
|
|
/// overshoots the end by more than one span wraps around more than once
|
|
/// rather than sticking at the boundary, matching a long stall/resume).
|
|
/// Returns <paramref name="currFrame"/> unchanged for a degenerate cycle
|
|
/// (<paramref name="highFrame"/> <= <paramref name="lowFrame"/>), a
|
|
/// non-positive <paramref name="framerate"/>, or a non-positive
|
|
/// <paramref name="elapsedSeconds"/>.
|
|
/// </summary>
|
|
public static float Advance(
|
|
float currFrame,
|
|
int lowFrame,
|
|
int highFrame,
|
|
float framerate,
|
|
float elapsedSeconds)
|
|
{
|
|
int span = highFrame - lowFrame;
|
|
if (span <= 0 || framerate <= 0f || elapsedSeconds <= 0f)
|
|
return currFrame;
|
|
|
|
float next = currFrame + elapsedSeconds * framerate;
|
|
if (next > highFrame)
|
|
{
|
|
float over = next - lowFrame;
|
|
next = lowFrame + (over % (span + 1));
|
|
}
|
|
else if (next < lowFrame)
|
|
{
|
|
next = lowFrame;
|
|
}
|
|
return next;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Resolves part <paramref name="partIndex"/>'s origin/orientation at
|
|
/// <paramref name="currFrame"/> by linearly interpolating (lerp origin,
|
|
/// slerp orientation) between the frame at <c>floor(currFrame)</c> and
|
|
/// the next frame in the cycle (wrapping <paramref name="highFrame"/>+1
|
|
/// back to <paramref name="lowFrame"/>). Returns <c>false</c> — with
|
|
/// <c>default</c> outputs — when <paramref name="partIndex"/> is outside
|
|
/// the bracketing frame's part list, matching
|
|
/// <c>LiveEntityAnimationPresenter.TryResolvePartFrame</c>'s no-
|
|
/// sequence-frames branch exactly.
|
|
/// </summary>
|
|
public static bool TryInterpolatePart(
|
|
Animation animation,
|
|
float currFrame,
|
|
int lowFrame,
|
|
int highFrame,
|
|
int partIndex,
|
|
out Vector3 origin,
|
|
out Quaternion orientation)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(animation);
|
|
|
|
int frameIndex = (int)MathF.Floor(currFrame);
|
|
if (frameIndex < lowFrame || frameIndex > highFrame || frameIndex >= animation.PartFrames.Count)
|
|
frameIndex = lowFrame;
|
|
int nextIndex = frameIndex + 1;
|
|
if (nextIndex > highFrame || nextIndex >= animation.PartFrames.Count)
|
|
nextIndex = lowFrame;
|
|
float t = Math.Clamp(currFrame - frameIndex, 0f, 1f);
|
|
|
|
var frames = animation.PartFrames[frameIndex].Frames;
|
|
var nextFrames = animation.PartFrames[nextIndex].Frames;
|
|
if (partIndex < frames.Count)
|
|
{
|
|
var first = frames[partIndex];
|
|
var next = partIndex < nextFrames.Count ? nextFrames[partIndex] : first;
|
|
origin = Vector3.Lerp(first.Origin, next.Origin, t);
|
|
orientation = Quaternion.Slerp(first.Orientation, next.Orientation, t);
|
|
return true;
|
|
}
|
|
|
|
origin = default;
|
|
orientation = default;
|
|
return false;
|
|
}
|
|
}
|