diff --git a/AGENTS.md b/AGENTS.md index a4e40b6..7f877f3 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -228,18 +228,25 @@ device only; the editor has an XR simulator for dev. Trackers/action map: see `x (arm IK, IK stability/deep, grabbing, two-handed grab/Box3D internals, addon weld patch, body physics hugs, body physics Godot facts) — READ before re-researching or changing grab/IK/body. Stink-O's box3d-godot (custom components) = documented plan-B. -8. **Body physics Stage 1 — PHYSICAL CHEST, implemented 2026-10-01.** `PhysicsTorso.cs`: +8. **Body physics Stage 1 — PHYSICAL CHEST, implemented 2026-10-01, FAILED first playtest.** + `PhysicsTorso.cs`: one chase-driven RigidBody3D (30 kg box, gravity off, CCD) toward an HMD-derived target — the PhysicsHands PD pattern at body mass (2400 N budget), orientation = slewed angular-velocity servo (self-rights, holds yaw), 2 m snap safety, torso-yaw spring moved here from BodyRig. Camera stays KINEMATIC — torso chases the head, never the reverse (Boneworks sickness lesson). No self-collision with own hands (layer scheme above). - `BodyRig.cs` is VISUALS-only now (head cube + 2-bone analytic arms, soft IK, elbow pole, + **First in-headset session NaN'd the solver** (owner seated: chest overlapped the table; + two-handed 50 kg anvil weld pressed into it → `!v.is_finite()` transform flood, "head + detaching from the body"). **DO NOT extend the body track until diagnosed** — analysis, + hypotheses and the headless-repro plan live in `research/body-stage1-failure-brief.md` + (self-contained, forwardable to the owner's external agent). `BodyRig.cs` is + VISUALS-only (head cube + 2-bone analytic arms, soft IK, elbow pole, reach budget feeding `PhysicsTorso.ReachOffset`). Headless regression: `scenes/body_spike.tscn` must print "BODY: overall PASS" (also proves the Box3D pair - rules: one-way contact, hand-through-chest exclusion, prop contact). Stage 2 (physical - arm chains + hierarchical per-joint budgets, Nimso/Boneworks tier) awaits the owner's - Stage-1 feel verdict; design + prior art in `research/vr-body-physics-hugs.md`, + rules: one-way contact, hand-through-chest exclusion, prop contact) — note its repro gap: + no static-table overlap, no welded heavy props. Stage 2 (physical + arm chains + hierarchical per-joint budgets, Nimso/Boneworks tier) is BLOCKED on the + Stage-1 fix; design + prior art in `research/vr-body-physics-hugs.md`, stack facts in `research/vr-body-physics-godot.md`. 9. **Getting a body (rig) — placeholder v1.3 (cube character, agent-built).** `BodyRig.cs`: all-BOX primitives (owner preference) — head 0.15³ behind the eyes, slim torso retracted, diff --git a/HANDOFF.md b/HANDOFF.md index cab3b16..431a67c 100644 --- a/HANDOFF.md +++ b/HANDOFF.md @@ -120,7 +120,10 @@ TERMINOLOGY (owner-corrected — use precisely): grab/teleport; `IsPanelCollider(node)` identifies interactive colliders for lasers. - **TeleportMarker.cs** — `scenes/teleport_marker.tscn`: disc + beam + Label3D, group "teleport_marker", StaticBody pick box. `SetNear` hides visuals, `SetHighlighted` swaps colors. -- **PhysicsTorso.cs** — the player's PHYSICAL CHEST (body-physics Stage 1, implemented 2026-10-01). +- **PhysicsTorso.cs** — the player's PHYSICAL CHEST (body-physics Stage 1). **Implemented + 2026-10-01; FAILED first in-headset playtest (seated chest-vs-table overlap + two-handed + 50 kg anvil weld → NaN transform flood) — see §5 known issues +** + **`research/body-stage1-failure-brief.md` before touching anything here.** One RigidBody3D box (0.22×0.5×0.13, 30 kg, gravity off, CCD, contact monitor), chase-driven toward an HMD-derived target: target = head − 6 cm back + torso-yaw basis·(0, −0.42, +0.06) + `ReachOffset` (fed by BodyRig's reach budget). Linear drive = the PhysicsHands pattern at @@ -213,6 +216,19 @@ Floor StaticBody scans layer 3 (collision_mask 13). ## 5. Known issues / open questions +- **Body Stage 1 (chest) FAILED in-headset on first playtest (2026-10-01) — DO NOT extend the + body track until diagnosed.** Owner report: "it quickly collapsed with my head detaching + from the body"; screenshot = prop table chaos. Log evidence: session calibrated at head + height **1.01 m (seated)** → chest box overlaps the table; the NaN flood + (`!v.is_finite()` at `instance_set_transform`, 22.8k lines) starts right after + **"grabbed 'Anvil · 50 kg' (50 kg, left, two-handed)"** — i.e. chest-vs-statics overlap + + 50 kg prop rigid-welded to both 90 N hands pressed into that mess. Headless body spike + passes (it lacks static-table overlap + welded heavy props — that's the repro gap). + Full self-contained analysis for the owner's external agent: + **`research/body-stage1-failure-brief.md`** (evidence chain, hypotheses H1–H3, mitigation + candidates M1–M5, headless-repro plan). Working suspicion: the documented + "script velocity SETs + reaction forces = limit cycle" lesson (grab v5.0) resurfacing at + body scale — 30 kg velocity-write servo braced against statics. - **AnchorMoveMode NEEDS FIXING (owner playtest report, 2026-10-01)** — two-hand turning is NOT working as intended (not 2-anchor based) and simultaneous two-hand movement acts weird. NOTE before fixing: owner is considering replacing grab-locomotion with physics @@ -295,17 +311,15 @@ Floor StaticBody scans layer 3 (collision_mask 13). ## 7. Next steps (in order) -1. **Full physics body track — Stage 1 (chest body) IMPLEMENTED, validate in-headset** — - `PhysicsTorso.cs` is live: chase chest (30 kg, 2400 N), yaw spring moved in, collision - layers reworked (3 = body, 4 = hands, no self-collision). Owner playtests: chest contact - with props (lean a crate on it, catch the bowling ball against it), shove response - (props push the chest, it recovers — camera never moves), turn following (torso lags - head naturally), weight feel. Hug testing on props as agreed. Headless: `body_spike` - PASS 3×; all other spikes still PASS 3×. **Stage 2 (arm chains, hierarchical budgets, - Nimso/Boneworks tier) awaits Stage-1 feel verdict** — see - `research/vr-body-physics-hugs.md` §5/§7 + `research/vr-body-physics-godot.md`. - Tuning knobs live in HANDOFF §6 (PhysicsTorso rows); first levers if feel is off: - `MaxForce` (gives more/less), `OrientKp` (torso yaw stiffness). +1. **Diagnose + fix the body Stage-1 collapse (BLOCKS the body track)** — first playtest of + the physical chest NaN'd the solver (seated chest-in-table overlap + two-handed 50 kg + anvil weld pressed into it; "head detaching from the body"). All context, hypotheses, + mitigation candidates and the headless-repro plan are in + `research/body-stage1-failure-brief.md` — written self-contained so the owner can forward + it to their external agent verbatim. First move per the brief: extend `body_spike` with + the exact cocktail (chest target overlapping a static slab + two welded 90 N hands + dragging a 50 kg box into the chest) and get a deterministic NaN repro before touching + PhysicsTorso.cs. Owner re-playtests feel only after the fix. 2. **Locomotion decision** — owner report 2026-10-01: two-hand anchor turning broken (not 2-anchor based), simultaneous two-hand movement acts weird, teleport + lasers broken (experiments, not current issues). Owner's candidate replacement: physics thumbstick diff --git a/research/body-stage1-failure-brief.md b/research/body-stage1-failure-brief.md new file mode 100644 index 0000000..57bf7a3 --- /dev/null +++ b/research/body-stage1-failure-brief.md @@ -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.