diff --git a/engine/MUNGA/MOVER.cpp b/engine/MUNGA/MOVER.cpp index cb1a062..b1b3b33 100644 --- a/engine/MUNGA/MOVER.cpp +++ b/engine/MUNGA/MOVER.cpp @@ -11,6 +11,34 @@ #include "line.h" #include "app.h" #include "notation.h" +#include + +// +// EXACT axis-angle rotation composition -- matches the 1995 BT binary's angular +// integrator (FUN_00409f58): build a unit rotation quaternion from the rotation +// VECTOR `rotVec` (angle = |rotVec|, axis = rotVec/angle) as { axis*sin(angle/2), +// cos(angle/2) } and Hamilton-multiply it onto `base`. The dead-reckoner previously +// did `out.Add(base, rotVec)` -- adding a scaled angular-velocity vector to the heading +// quaternion. That is only a small-angle approximation: fine per-frame (tiny angle), +// but over a long replicant dead-reckon gap it DIVERGES (the heading drifts to ~180deg +// then snaps -- the spinning-peer hesitation). This composition is exact for any angle +// and stays on the unit sphere. +// +static void ExactAngularProject(Quaternion &out, const Quaternion &base, const Vector3D &rotVec) +{ + const Scalar ang = rotVec.Length(); + if (ang > 1.0e-6f) + { + const Scalar h = 0.5f * (Scalar)fmodf((float)ang, 6.2831853f); // half of angle mod 2pi + const Scalar s = (Scalar)(sinf((float)h) / ang); // sin(angle/2)/angle + const Quaternion dq(rotVec.x * s, rotVec.y * s, rotVec.z * s, (Scalar)cosf((float)h)); + out.Multiply(base, dq); // base (X) dq + } + else + { + out = base; + } +} //############################################################################# //############################### Mover ################################# @@ -395,10 +423,8 @@ Logical //------------------------------- // position_delta.Multiply(updateVelocity.angularMotion, time_slice); - projectedOrigin.angularPosition.Add( - updateOrigin.angularPosition, - position_delta - ); + ExactAngularProject(projectedOrigin.angularPosition, // was .Add (diverging vector-add) + updateOrigin.angularPosition, position_delta); projectedVelocity = updateVelocity; Check_Fpu(); @@ -460,10 +486,8 @@ Logical updateVelocity.angularMotion, time_slice ); - projectedOrigin.angularPosition.Add( - updateOrigin.angularPosition, - position_delta - ); + ExactAngularProject(projectedOrigin.angularPosition, // was .Add (diverging vector-add) + updateOrigin.angularPosition, position_delta); // //----------------------------------- diff --git a/game/reconstructed/mech4.cpp b/game/reconstructed/mech4.cpp index 0d8f15f..a56b001 100644 --- a/game/reconstructed/mech4.cpp +++ b/game/reconstructed/mech4.cpp @@ -1930,7 +1930,7 @@ void { if (dt > 0.0001f && dt < 0.5f) { - DeadReckon(dt); // engine: reckoner + lerp + DeadReckon(dt); // engine: reckoner (exact angular) + lerp localToWorld = localOrigin; // RENDER-vs-SIM decoupling probe: publish the heading the RENDER will @@ -3672,6 +3672,17 @@ void { resync = True; // stand-in when unstreamed } + // ANGULAR dense-send: the original refreshed orientation in its FREQUENT + // pose record so the peer's dead-reckon gap stayed tiny (decomp: 7-float + // pose carries the quaternion, refreshed every broadcast). Our orientation + // rides only the type-4 resync, and during a PURE spin the linear dense-send + // never fires (not translating), so the gap balloons (~1.6s) and even the + // exact integrator's projection is far ahead -> the slerp jumps. Send the + // resync every frame while rotating to keep updateOrigin.angular fresh + // (small gap -> smooth), mirroring the original's frequent-orientation model. + static const int s_denseRot = getenv("BT_NO_DENSE_TX") ? 0 : 1; + if (s_denseRot && Abs(localVelocity.angularMotion.y) > 0.1f) + resync = True; if (resync) { ForceUpdate(1 << MechResyncUpdateModelBit); // type 4 diff --git a/game/reconstructed/mechrecon.hpp b/game/reconstructed/mechrecon.hpp index 26f4d06..def87fb 100644 --- a/game/reconstructed/mechrecon.hpp +++ b/game/reconstructed/mechrecon.hpp @@ -435,11 +435,33 @@ namespace Vector // // P3 GAIT CUTOVER: backed by the real engine Quaternion (were no-op stubs). // ReconQuatIdentity(q, &id) -> q = Quaternion::Identity -// ReconQuatIntegrate(out,in,d) -> out = in integrated by angular delta d -// (engine Quaternion::Add(source, Vector3D) == FUN_00409f58) +// ReconQuatIntegrate(out,in,d) -> out = in composed with an EXACT rotation by +// angular delta d (= the real FUN_00409f58). // All callers pass (Quaternion*,Quaternion*) / (Quaternion*,Quaternion*,Vector3D*). inline void ReconQuatIdentity(Quaternion *q, const Quaternion * /*id*/) { *q = Quaternion::Identity; } -inline void ReconQuatIntegrate(Quaternion *out, const Quaternion *in, const Vector3D *delta) { out->Add(*in, *delta); } +// EXACT axis-angle quaternion integration -- the real FUN_00409f58 (part_000.c:9359), +// verified from the decomp. A prior stand-in wrongly used `out->Add(*in,*delta)` (a +// small-angle VECTOR add): correct every-frame on the master (tiny angle) but it +// DIVERGES over a long dead-reckon gap -- the spinning-peer heading drifting to ~180deg +// then snapping. The binary builds the unit rotation quaternion from the rotation vector +// `delta` (angle=|delta|, axis=delta/angle) as { axis*sin(angle/2), cos(angle/2) } and +// Hamilton-multiplies it onto `in` (FUN_00409d9c: out = in (X) deltaQuat). +inline void ReconQuatIntegrate(Quaternion *out, const Quaternion *in, const Vector3D *delta) +{ + const Scalar ang = delta->Length(); + if (ang > 1.0e-6f) + { + const Scalar half = (Scalar)(fmodf((float)ang, 6.2831853f) * 0.5f); // reduce2pi, then halve + const Scalar s = (Scalar)(sinf((float)half) / ang); // sin(angle/2)/angle + const Quaternion dq((Scalar)(delta->x * s), (Scalar)(delta->y * s), + (Scalar)(delta->z * s), (Scalar)cosf((float)half)); + out->Multiply(*in, dq); // in (X) dq + } + else + { + *out = *in; + } +} template inline Recon ReconQuatSlerp(A&&...) { return Recon(); } template inline Recon ComputeImpactDamage(A&&...) { return Recon(); }