111 lines
4.6 KiB
C#
111 lines
4.6 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 is the shared implementation used by both the "legacy" (no
|
|
/// <see cref="AnimationSequencer"/>) NPC idle-cycle path and the chargen
|
|
/// preview. The live presenter 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 neither consumer can
|
|
/// own the primitive. Keeping it here prevents the two paths from drifting.
|
|
/// </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 retail playback and the live presenter's legacy branch use
|
|
/// (<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"/>) or a
|
|
/// non-positive <paramref name="elapsedSeconds"/>. A negative framerate
|
|
/// advances backward and clamps at the low frame, matching the former
|
|
/// live-presenter implementation exactly.
|
|
/// </summary>
|
|
public static float Advance(
|
|
float currFrame,
|
|
int lowFrame,
|
|
int highFrame,
|
|
float framerate,
|
|
float elapsedSeconds)
|
|
{
|
|
int span = highFrame - lowFrame;
|
|
if (span <= 0 || 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
|
|
/// the live presenter'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;
|
|
}
|
|
}
|