From 6523c7199be0bdabcd908b3142a7d1094cbe89c1 Mon Sep 17 00:00:00 2001 From: Erik Date: Tue, 14 Apr 2026 10:42:19 +0200 Subject: [PATCH] fix(movement): correct facing direction quaternion convention MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit AC heading convention: 0=West, 90=North, 180=East, 270=South. Our internal yaw: 0=+X (East), PI/2=+Y (North). Conversion: heading_deg = 180 - yaw_degrees, then holtburger's from_heading formula: theta=(450-heading).toRad, w=cos(θ/2), z=sin(θ/2). Previously sent raw Yaw as axis-angle rotation which was ~90° off. Other clients now see correct facing direction. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/AcDream.App/Rendering/GameWindow.cs | 31 +++++++++++++++++++++++-- 1 file changed, 29 insertions(+), 2 deletions(-) diff --git a/src/AcDream.App/Rendering/GameWindow.cs b/src/AcDream.App/Rendering/GameWindow.cs index d425bcb..3adb7bd 100644 --- a/src/AcDream.App/Rendering/GameWindow.cs +++ b/src/AcDream.App/Rendering/GameWindow.cs @@ -1896,8 +1896,7 @@ public sealed class GameWindow : IDisposable float localY = result.Position.Y - (lbY - _liveCenterY) * 192f; uint wireCellId = ((uint)lbX << 24) | ((uint)lbY << 16) | (result.CellId & 0xFFFFu); var wirePos = new System.Numerics.Vector3(localX, localY, result.Position.Z); - var wireRot = System.Numerics.Quaternion.CreateFromAxisAngle( - System.Numerics.Vector3.UnitZ, _playerController.Yaw); + var wireRot = YawToAcQuaternion(_playerController.Yaw); if (result.MotionStateChanged) { @@ -1956,6 +1955,34 @@ public sealed class GameWindow : IDisposable } } + /// + /// Convert our internal yaw (math convention: 0=+X East, PI/2=+Y North) + /// to AC's quaternion heading convention. + /// AC heading: 0=West, 90=North, 180=East, 270=South. + /// Formula from holtburger Quaternion::from_heading. + /// + private static System.Numerics.Quaternion YawToAcQuaternion(float yaw) + { + // Our yaw → AC heading in degrees: + // yaw=0 → East → AC 180°, yaw=PI/2 → North → AC 90° + // heading_deg = 180 - yaw_degrees + float yawDeg = yaw * (180f / MathF.PI); + float headingDeg = 180f - yawDeg; + if (headingDeg < 0f) headingDeg += 360f; + if (headingDeg >= 360f) headingDeg -= 360f; + + // holtburger from_heading: theta = (450 - heading_deg) in radians + float theta = (450f - headingDeg) * (MathF.PI / 180f); + float halfTheta = theta * 0.5f; + float w = MathF.Cos(halfTheta); + float z = MathF.Sin(halfTheta); + + // Canonicalize: w must be non-negative + if (w < 0f) { w = -w; z = -z; } + + return new System.Numerics.Quaternion(0f, 0f, z, w); + } + private void OnCameraModeChanged(bool isFlyMode) { if (_input is null) return;