using System.Numerics; namespace AcDream.Core.Physics; /// /// Retail frame-orientation helpers used by physics presentation. /// public static class RetailFrameMath { /// /// Points the frame's local +Y axis along a world-space direction with /// zero roll. This is the observable contract of retail /// Frame::set_vector_heading at 0x00535DB0: normalize the /// full three-dimensional vector, derive compass yaw plus elevation, and /// replace the frame rotation. A zero-length vector leaves the prior /// orientation unchanged. /// public static Quaternion SetVectorHeading( Quaternion currentOrientation, Vector3 direction) { float lengthSquared = direction.LengthSquared(); if (lengthSquared < PhysicsGlobals.EpsilonSq || !float.IsFinite(lengthSquared)) return currentOrientation; Vector3 forward = direction / MathF.Sqrt(lengthSquared); // Retail's zero-roll Euler construction is equivalent to choosing a // horizontal right axis from the compass heading, then deriving the // remaining up axis. At a perfectly vertical heading atan2(0, 0) // resolves to zero, so retain +X as the deterministic right axis. Vector3 right; if (forward.X == 0f && forward.Y == 0f) { right = Vector3.UnitX; } else { // Normalize the horizontal pair without squaring the original // components. That preserves every non-zero compass component, // including a nearly vertical vector below PhysicsGlobals.EPSILON, // just as retail's atan2-based construction does. float scale = MathF.Max(MathF.Abs(forward.X), MathF.Abs(forward.Y)); float x = forward.X / scale; float y = forward.Y / scale; float horizontalLength = MathF.Sqrt((x * x) + (y * y)); right = new Vector3(y / horizontalLength, -x / horizontalLength, 0f); } Vector3 up = Vector3.Normalize(Vector3.Cross(right, forward)); // System.Numerics uses row-vector transforms. Each row below is the // world direction of one local basis axis: +X right, +Y heading, // +Z up. var rotation = new Matrix4x4( right.X, right.Y, right.Z, 0f, forward.X, forward.Y, forward.Z, 0f, up.X, up.Y, up.Z, 0f, 0f, 0f, 0f, 1f); return Quaternion.Normalize(Quaternion.CreateFromRotationMatrix(rotation)); } }