Body Stage 1 collapse root-caused + fixed: NaN in torso servo math (GetAngle = 2*acos(W) at W=1.0000001), not solver stacking
- BodyMath.RotationError: atan2-based shortest-arc error (no NaN, no GetAxis gain collapse inside 0.16 deg); used by PhysicsTorso orientation servo and PhysicsHands rotation lock - BodyMath.ResetIfNonFinite escape hatch on torso + hands - PhysicsHands.Drive extracted (static) so spikes run the real hand chase - CollapseSpike: deterministic repro (slow yaw drift across a parked chest, 7/40 NaN on old code) + solver-stacking scenarios (seated chest in table, two-hand-welded 50 kg anvil, real PropShop) — 49/49 clean after fix, x3; all other spikes PASS x3 - Failure brief RESOLUTION section; HANDOFF/AGENTS updated Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
ce1e0e3b0c
commit
cb518999e5
9 files changed
+466
-47
No files matched your search
@@ -1,5 +1,8 @@
|
||||
# Body Stage 1 failure brief — chest collapse in-headset (2026-10-01)
|
||||
|
||||
**STATUS: RESOLVED — root cause = NaN in the torso orientation servo math (not the solver).
|
||||
See the RESOLUTION section at the end. The hypotheses below are kept as the record.**
|
||||
|
||||
SELF-CONTAINED brief for an external agent (zero project context assumed). Godot 4.7.2 mono,
|
||||
C#, PCVR at 90 Hz (physics tick = 90), physics engine = **Box3D** (bearlikelion's
|
||||
`godot-box3d` GDExtension — a full PhysicsServer3D replacement; locally patched so Godot's
|
||||
@@ -127,3 +130,46 @@ the prop table in chaos (props piled/thrown, panel knobs displaced). Log
|
||||
|
||||
Owner directive: painlessness bar = no multi-iteration fundamental sagas. Deliver the
|
||||
analysis, the repro, and the smallest honest fix — the owner playtests feel afterwards.
|
||||
|
||||
## RESOLUTION (2026-10-01, same day) — root cause found, deterministic repro, fixed
|
||||
|
||||
**The NaN was script math, not the solver.** H1–H3 were all tested headless and did NOT
|
||||
reproduce (`scenes/collapse_spike.tscn`, stacking family: chest 5/15 cm into the static
|
||||
table, 50 kg anvil rigid-welded to two 90 N chase hands, the exact logged grab sequence,
|
||||
the real PropShop with the chest plowing through props — all finite).
|
||||
|
||||
Root cause, verified step by step:
|
||||
1. The orientation servo computed `qErr.GetAngle()` = `2*acos(W)`. Godot C#'s `Mathf.Acos`
|
||||
is UNCLAMPED (docs: input "must be" in [-1, 1]); when float rounding puts the error
|
||||
quaternion's W at **1.0000001**, the angle is NaN. `Mathf.Min(NaN, cap)` passes NaN
|
||||
through, the slew clamp's `wErr.Length() > max` is false for NaN, and
|
||||
`AngularVelocity += NaN` goes straight into Box3D (`b3Body_SetAngularVelocity` only
|
||||
asserts validity in debug builds). The chest transform goes NaN → renderer flood →
|
||||
arm visuals (derived from the chest pose) vanish with it → "head detached from body".
|
||||
2. Why it needed a seated, still player: `Quaternion.GetAxis()` returns the RAW
|
||||
un-normalized xyz once W > 1 − 1e-6 (angle < ~0.16°), so the servo's gain collapsed
|
||||
~1000× inside that band — the body parked a fraction of a degree off target. The
|
||||
rounding hazard (error < ~3e-4 rad) is only reached when the yaw target then DRIFTS
|
||||
SLOWLY across that parked yaw — exactly a seated player holding still (the 50 kg anvil
|
||||
welded to both hands can't move: 2×90 N < 0.95×490 N friction, hence the logged
|
||||
0.01 m/s releases). Normal motion crosses the band too fast. The hands and anvil stayed
|
||||
finite (they kept grabbing after the flood), consistent with a torso-only NaN.
|
||||
3. Deterministic repro: head still at 40 yaws, target drifting 0.2°/s → **7/40 scenarios
|
||||
NaN the chest** on the old code (W = 1.0000001 at the crossing tick, body w = 0).
|
||||
|
||||
Fix (`scripts/BodyMath.cs`): `RotationError()` = shortest-arc axis·angle via
|
||||
`2*atan2(|xyz|, W)` with a properly normalized axis — exact near zero, can't leave its
|
||||
domain. Used by the torso servo AND the hand rotation lock (hands were not observed to NaN
|
||||
— their Basis-product path measured 0/200k — but shared the gain collapse). Escape hatch:
|
||||
`BodyMath.ResetIfNonFinite()` on every driven body (torso, hands) — any future non-finite
|
||||
state resets that body onto its target with a warning instead of poisoning the renderer.
|
||||
After the fix: collapse spike 49/49 clean ×3, yaw error under slow drift 0.026° (was a
|
||||
0.16° stall); physics/weld/grab/body spikes all PASS ×3.
|
||||
|
||||
Mitigations M1–M4 were NOT needed for the NaN. Side observations kept for the owner:
|
||||
- The prop "table chaos" is real and separate: a seated chest sweeping the table plows
|
||||
props off at up to ~3.8 m/s (2400 N body) — honest physics, tune by feel if unwanted.
|
||||
- Seated calibration: head height 1.01 m clamps arm length to the 0.50 m minimum, so
|
||||
reaching saturates `ReachOffset` (0.25 m) and leans the chest INTO the table. The chest
|
||||
then rests saturated on the table edge — stable in every repro, but a calibration that
|
||||
assumes standing height is the deeper issue (see HANDOFF known issues).
|
||||
Reference in new issue
Block a user