using System; using System.Numerics; using DatReaderWriter.Types; namespace AcDream.Core.Physics.Motion; /// /// R1-P3 — verbatim ports of retail's Frame rotation helpers used by /// the CSequence physics path (oracle: raw decomp reads 2026-07-02). /// /// /// Frame::grotate (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. /// Frame::rotate (0x004525b0, pc:91477): map the LOCAL /// rotation vector through the frame's local→global rotation /// (m_fl2gv — our quaternion), then grotate. /// /// public static class FrameOps { /// Retail F_EPSILON (0.000199999995f); grotate gates on its /// square against |v|². public const float FEpsilon = 0.000199999995f; /// Frame::grotate — incremental WORLD-space rotation. 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)); } /// Frame::rotate — LOCAL rotation vector, mapped to /// global through the orientation, then . public static void Rotate(Frame frame, Vector3 rotationLocal) => GRotate(frame, Vector3.Transform(rotationLocal, frame.Orientation)); }