- 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>
58 lines
2.4 KiB
C#
58 lines
2.4 KiB
C#
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|² < 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));
|
|
}
|