feat(R1-P3): verbatim apply_physics + Frame rotate/grotate

- CSequence.ApplyPhysics (0x00524ab0): copysign semantics — magnitude
  from the quantum arg, sign from the sign-source arg (call sites pass
  1/framerate + signed elapsed); origin += velocity*signed, then
  rotate(omega*signed).
- FrameOps.GRotate (0x005357a0): axis-angle quaternion (angle=|v|,
  half-angle sin/cos) PREMULTIPLIED onto the orientation — incremental
  rotation in WORLD space; |v|^2 < F_EPSILON^2 skipped.
- FrameOps.Rotate (0x004525b0): local rotation vector mapped through
  the frame's local->global rotation, then GRotate.

7 numeric conformance tests (copysign matrix, global-space composition,
local->global mapping equivalence, epsilon gate).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-07-02 19:53:32 +02:00
parent 778744bf3e
commit 5138b8fb01
3 changed files with 220 additions and 0 deletions

View file

@ -253,6 +253,26 @@ public sealed class CSequence
/// <summary><c>get_curr_frame_number</c> (0x005249d0).</summary>
public int GetCurrFrameNumber() => (int)Math.Floor(FrameNumber);
// ── apply_physics (0x00524ab0, §19) ─────────────────────────────────
/// <summary>
/// Accumulated-physics root motion: advance <paramref name="frame"/> by
/// <see cref="Velocity"/>/<see cref="Omega"/> over a signed quantum —
/// MAGNITUDE from <paramref name="quantum"/>, SIGN from
/// <paramref name="signSource"/> (retail copysign semantics; call sites
/// pass 1/framerate as magnitude and the signed elapsed time as sign).
/// </summary>
public void ApplyPhysics(Frame frame, double quantum, double signSource)
{
double signed = Math.Abs(quantum);
if (signSource < 0.0)
signed = -signed;
float sq = (float)signed;
frame.Origin += Velocity * sq;
FrameOps.Rotate(frame, Omega * sq);
}
// ── internal cursors for P4 (update_internal operates on nodes) ─────
internal LinkedListNode<AnimSequenceNode>? CurrAnimNode

View file

@ -0,0 +1,58 @@
using System;
using System.Numerics;
using DatReaderWriter.Types;
namespace AcDream.Core.Physics.Motion;
/// <summary>
/// R1-P3 — verbatim ports of retail's <c>Frame</c> rotation helpers used by
/// the CSequence physics path (oracle: raw decomp reads 2026-07-02).
///
/// <list type="bullet">
/// <item><c>Frame::grotate</c> (0x005357a0, pc:319782): build the
/// axis-angle quaternion from a GLOBAL rotation vector (angle = |v|,
/// axis = v/|v|, half-angle sin/cos) and PREMULTIPLY it onto the frame's
/// orientation — the incremental rotation is applied in world space.
/// Rotations with |v|² &lt; F_EPSILON² are skipped.</item>
/// <item><c>Frame::rotate</c> (0x004525b0, pc:91477): map the LOCAL
/// rotation vector through the frame's local→global rotation
/// (m_fl2gv — our quaternion), then <c>grotate</c>.</item>
/// </list>
/// </summary>
public static class FrameOps
{
/// <summary>Retail F_EPSILON (0.000199999995f); grotate gates on its
/// square against |v|².</summary>
public const float FEpsilon = 0.000199999995f;
/// <summary><c>Frame::grotate</c> — incremental WORLD-space rotation.</summary>
public static void GRotate(Frame frame, Vector3 rotationGlobal)
{
float magSq = rotationGlobal.LengthSquared();
if (magSq < FEpsilon * FEpsilon)
return;
float angle = MathF.Sqrt(magSq);
float invMag = 1f / angle;
float half = angle * 0.5f;
float s = MathF.Sin(half);
float c = MathF.Cos(half);
// Retail's set_rotate receives the raw Hamilton product r ⊗ q
// (r = the new axis-angle quat, q = the current orientation) —
// rotation applied in GLOBAL space. System.Numerics
// Quaternion.Multiply(r, q) is that product with
// Transform(v, r*q) == r-applied-after-q. set_rotate renormalizes.
var r = new Quaternion(
rotationGlobal.X * s * invMag,
rotationGlobal.Y * s * invMag,
rotationGlobal.Z * s * invMag,
c);
frame.Orientation = Quaternion.Normalize(Quaternion.Multiply(r, frame.Orientation));
}
/// <summary><c>Frame::rotate</c> — LOCAL rotation vector, mapped to
/// global through the orientation, then <see cref="GRotate"/>.</summary>
public static void Rotate(Frame frame, Vector3 rotationLocal)
=> GRotate(frame, Vector3.Transform(rotationLocal, frame.Orientation));
}