Body Stage 1 failed first playtest (NaN solver blowup: seated chest-in-table + two-handed anvil weld); failure brief for external agent; docs marked, body track blocked pending diagnosis

This commit is contained in:
Foxyz1302 committed 2026-10-01 23:20:40 +03:00
1 parent 4fdfcfd928
commit ce1e0e3b0c
3 files changed
+167 -17

No files matched your search

+129
View File
@@ -0,0 +1,129 @@
# Body Stage 1 failure brief — chest collapse in-headset (2026-10-01)
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
ConeTwistJoint3D maps to Box3D's `b3WeldJoint` with rigid stiffness at bias 0). Everything
below is verified code or log evidence unless marked [H] (hypothesis).
## What was just built (body-physics Stage 1)
The player's virtual chest became a physical body. `PhysicsTorso.cs` (Node) spawns:
- One RigidBody3D, box 0.22×0.5×0.13 m, **Mass 30**, GravityScale 0, CCD on, contact monitor,
CollisionLayer = layer 3 (player body), CollisionMask = layers 1|3 (world+props | body).
- Every physics tick (90 Hz), target = head position (from the HMD camera node, kinematic)
minus 6 cm along flattened camera forward, then offset by the torso-yaw basis (a yaw-only
basis smoothed toward a blend of HMD yaw and hands-midpoint yaw) by (0, −0.42, +0.06),
plus a small "reach offset" fed by the visual rig (≤ 0.25 m horizontal clamp).
- Linear drive — force-clamped PD, the same pattern the hand bodies use:
```csharp
Vector3 desiredVelocity = (target - _body.GlobalPosition) / dt; // dt = 1/90
if (desiredVelocity.Length() > MaxSpeed) desiredVelocity = desiredVelocity.Normalized() * MaxSpeed; // 6
Vector3 dv = desiredVelocity - _body.LinearVelocity;
Vector3 force = dv / dt * TorsoMass; // 30 kg
if (force.Length() > MaxForce) force = force.Normalized() * MaxForce; // 2400 N
_body.LinearVelocity += force / TorsoMass * dt; // DIRECT velocity write
```
- Orientation drive — slewed angular velocity servo toward the yaw target:
```csharp
Quaternion qErr = torsoBasisQ * _body.GlobalTransform.Basis.GetRotationQuaternion().Inverse();
if (qErr.W < 0f) qErr = -qErr; // shortest arc
Vector3 spinDesired = axis(qErr) * min(angle(qErr) * OrientKp /*20*/, OrientMaxSpin /*10*/);
Vector3 wErr = spinDesired - _body.AngularVelocity;
if (wErr.Length() > OrientMaxAlpha * dt /*150/90*/) wErr = wErr.Normalized() * OrientMaxAlpha * dt;
_body.AngularVelocity += wErr; // DIRECT angular velocity write
```
- Snap safety: if body-to-target distance > 2 m → teleport body to target, zero velocities.
- Head/camera stays kinematic; the torso chases the head, never the reverse.
Context bodies:
- **Hands**: two RigidBody3D palms (0.4 kg, layer 4, mask 1|4), same velocity-write chase at
90 N, rotation hard-locked via angular velocity sets (cap 120 rad/s). They chase XR
controllers. Stuck-snap and teleport-snap safeties are SUSPENDED for a hand while it grips.
- **Grab**: grabbing a prop creates a ConeTwistJoint3D (= b3WeldJoint, bias 0 = RIGID,
locks relative pose) between hand body and prop. Two-handed grab = prop welded to BOTH
hands rigidly. Props: layer 1, mask 1|3|4. Statics (floor/table): layer 1.
- **No self-collision**: hands (4) and chest (3) exclude each other both ways. Chest DOES
collide with props (layer 1) and static world (layer 1) — deliberate, chest is the
contact surface for hugs/leaning.
- Headless regression (body spike: track/yaw/5kg-impact/teleport/self-right) PASSES 3× —
but the spike has NO static table and NO welded heavy prop touching the chest.
## The failure (owner playtest, first session with the chest)
Owner report: "it quickly collapsed with my head detaching from the body". Screenshot shows
the prop table in chaos (props piled/thrown, panel knobs displaced). Log
(`godot.log`, ~45.7k lines, 22.8k of them the error below):
- Calibration line: **head height 1.01 m** → the player was SEATED/crouching (standing
calibration would be ~1.6-1.7 m). Chest target therefore ≈ 0.59 m height; chest box spans
≈ 0.34-0.84 m; the prop table top is at 0.70-0.75 m → **the seated chest overlaps the
table geometry** (chest mask includes statics). [H] constant penetration + 2400 N servo.
- Normal play: grabbed/released bowling ball, brick, apple — all fine.
- Then: `grabbed 'Anvil · 50 kg' (50 kg, left)`, released, then
**`grabbed 'Anvil · 50 kg' (50 kg, left, two-handed)`** — and within ticks the log floods:
```
ERROR: Condition "!v.is_finite()" is true.
at: instance_set_transform (servers/rendering/renderer_scene_cull.cpp:1000)
```
×22,829 — some node's transform went NaN/Inf (rendering rejects it). The visual result
matches the chest body (and/or anvil) transforming to garbage → the torso mesh vanishes/
teleports → "head detached from the body" (head cube rides the camera, body went away).
- Owner kept grabbing/releasing the anvil afterwards (releases logged at 0.00-0.01 m/s —
the anvil was braced/nearly immobile), then removed the headset (focus lost).
## Hypotheses (ranked)
- **H1 — solver blowup from stacked authorities [most likely]**: the chest (velocity-write
servo, 2400 N) pressed against the STATIC table (seated overlap) while the 50 kg anvil
rigid-welded to both 90 N velocity-write hand bodies was grabbed/moved into that mess.
Three script-side velocity servos + rigid welds + persistent static contact in one
constraint island. Box3D's solver gets conflicting infinite-stiffness demands → energy
injection → NaN. This project has PRIOR ART for exactly this class: documented in
`research/vr-two-handed-grab.md` — "script velocity SETs + reaction forces = limit
cycle" (grab v5.0-era instability). The hands survive the same pattern only because
they're 0.4 kg, rarely braced, and have stuck-snap escape hatches. The chest is 30 kg,
overlaps the table whenever the owner sits at it, and has no escape.
- **H2 — chest vs table penetration alone**: even without the anvil, seated chest-in-table
= constant deep penetration vs a static body; the velocity servo never yields (position
error persists → max force every tick) → pressure + friction against statics pumps the
solver. The anvil grab was just the straw.
- **H3 — CCD tunneling/spike**: 30 kg body with CCD in deep contact + welded 50 kg chain —
CCD contact generation bug under stress. [H] least likely but cheap to test.
## Candidate mitigations (evaluate, don't just pick one)
- **M1 — ApplyForce instead of velocity writes for the chest** (solver-owned integration;
keeps the 2400 N clamp). The hands keep velocity writes (they have escape hatches).
Boneworks' design rule "no infinite forces" supports solver-integrated forces.
- **M2 — collision-aware target or compliance**: if the chest is in persistent contact
(contact monitor reports statics) with saturated force, soften (reduce effective budget)
or stop pushing (target clamps out of penetration). Generic "yield when braced".
- **M3 — seated reality**: the chest target height derives purely from HMD (−0.42). When
seated at a table the body legitimately overlaps world geometry. Options: chest collision
disabled vs statics (mask 3 only — loses leaning on tables), or per-body contact disabling
when penetration is self-inflicted (chase target inside a static), or pushing the TABLE
(chest becomes heavy enough to matter? 30 kg vs static = never).
- **M4 — saturation watchdog**: like the hands' stuck-snap: if force saturated AND position
error persistent AND in contact for > N ms → log + temporarily reduce budget / exclude the
offending pair. Keeps M1's solver honesty while adding the escape hatch.
- **M5 — reproduce headless first**: extend the body spike with the exact cocktail — chest
target set so the box overlaps a static table slab; a 50 kg box welded rigid to two
0.4 kg "hand" bodies (90 N chases) pressed into the chest; then sweep. A deterministic
NaN repro beats every hypothesis debate. The full grab spike already emulates hands+welds
headless (GrabSpike.cs pattern) — compose it with the chest.
## Questions to answer
1. Can a deterministic headless repro be built (M5)? If yes → does M1 alone stop the NaN?
2. Is the velocity-write chase on a contact-rich 30 kg body categorically unsafe on Box3D,
or only under simultaneous weld+static stacking? (Box3D source available in .vendor/.)
3. What does the solver do with direct LinearVelocity writes on a body that is ALSO in an
active contact manifold — are the writes applied pre- or post-solve, and can the contact
impulse + next-tick write loop escalate?
4. Seated-at-table is a first-class scenario for this product (scene viewer). What's the
right long-term chest-vs-world story (M2/M3) that still allows leaning on tables?
Owner directive: painlessness bar = no multi-iteration fundamental sagas. Deliver the
analysis, the repro, and the smallest honest fix — the owner playtests feel afterwards.