Files
VR-Mess/research/ragdoll-player-step2.md
T
FoxyzandClaude Opus 5.5 6006c5de98 Body step 2.2: fix the 2.1 headset instabilities
Spike now runs real input (90 Hz sample-and-hold tracking + jitter under 144 Hz
physics), which reproduced the playtest. Root causes fixed:
- PoseFilter/RagdollDriver: timestamped extrapolation of tracking, one tick
  pipeline shared by PlayerRig and the spike
- low-pass all feed-forwards (ground control amplified jitter by body mass)
- arm muscles back to local targets (physical-frame aim fed chest rotation back)
- structural hand strength only when braced or holding
- no free-path view chasing while grounded (8 m crouch slide); stick hold-back
- explicit inertia for the Legs body (shape-less tuck tumbled the pelvis)
- world grab: Box3D shape queries return a null collider, use collider_id
- new crouch-hold phases; world-grab detection asserted

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 08:37:56 +03:00

134 lines
15 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Body step 2 — the muscle-driven ragdoll player (2026-10-02)
Implementation record for Nimso's Project TX player rebuilt on Box3D (design source:
`nimso-project-tx.md` §2; joint mapping: `godot-box3d-6dof.md`). Code: `scripts/PlayerRagdoll.cs` (input-agnostic
body), `scripts/PlayerRig.cs` (XR driver + stick locomotion), regression `scenes/ragdoll_spike.tscn`
("RAGDOLL: overall PASS", 14 phases). It replaces PhysicsHands / PhysicsTorso / PhysicsArms / BodyRig in
`main.tscn`. Those scripts stay for their own spikes.
## What was built
| Part | Implementation | Why |
|---|---|---|
| Bodies | 9 boxes, each with its origin at its centre of mass: pelvis (pelvis + abdomen + both legs' Dempster mass, 36 kg at 60 kg total), chest 13 kg, head + neck 4.9 kg, upper arm 1.7, forearm 1.0, hand 0.36 | Legs aren't physical yet (Nimso's leg collision is off by default too), but their mass belongs to the body the ground carries |
| Spine, neck | `Generic6DOFJoint3D` BALL (cone/twist, twist axis = up) + muscle motor | Local muscles, Nimso: "each bone drives itself" |
| Shoulder | `Generic6DOFJoint3D` DRIVE chest→upper arm. The linear velocity motor (2000 N) holds the humeral head on the chest point + the True Body girdle offset; the angular velocity motor (45 N·m) is the shoulder muscle | One joint carries both the moving girdle (shrug/protraction = real reach) and the muscle. See "light links" below |
| Elbow | `HingeJoint3D` 0..150° with a muscle motor (45 N·m) on the flexion angle | A real hinge can't flip or hyper-extend |
| Wrist | BALL, cone 80°, twist ±90°, no motor | The hand guide orients the hand |
| Hand guide | DRIVE chest→hand. Linear velocity motor 40 N with controller-velocity feed-forward, angular 25 N·m | "Tracking is a guide". The reaction goes into the chest, so pushing a wall pushes you |
| Balance | DRIVE from a shapeless world anchor to the pelvis, angular motor only (400 N·m grounded, 30% airborne) | Upright/yaw hold. See "explicit torques" below |
| Ground control | Ray down from the pelvis. If ground is within hip height + 0.3 m (normal.y > 0.6): critically damped 4 Hz PD on the pelvis toward its pose height above the actual ground, with gravity compensation for the whole body mass. Support is clamped to [0, 2 × weight], horizontal force to friction × support | Nimso's "leg strength" scales with mass and only pushes. No ground → falls |
| Muscle law | Motor target velocity = feed-forward (the target's own relative spin) + Kp·RotationError(target, current) in joint frame A, capped at 15 rad/s; force/torque cap = strength | Proven in `sixdof_spike`. Feed-forward keeps moving poses from lagging by speed/Kp |
| Targets | From `TrueBodySolver`: trunk rebuilt top-down (upper chest → lumbar → hips, hips take 50% of the crouch lean). Head = camera orientation tilted so the body's eye lands on the camera. Arms = ArmSolver shoulder/elbow/wrist; hinge axis = Pole × arm direction | A pose the physical chain can actually reach |
| Camera / play space | Camera stays 1:1. Each tick the driver applies `OriginCorrection`: a 3 cm sphere swept from the head body's eye to the camera. If geometry is in the way, the view is put back at the free point (walls stop both stick and real walking). If the path is free, the camera may lead the body by up to 0.2 m (leaning over a table whose edge holds your hips), beyond that it follows. Grounded: horizontal only, the floor eases toward the ground under the pelvis. Airborne: 3D (falls carry the view) | Owner: Bonelab push-back, no fade |
| Discrete moves | Origin jumps > 0.5 m or > 0.1 rad in one tick (spawn, teleport markers) and snap turns carry the body with `TeleportBy` + `TrueBodySolver.Shift` | No body flung across the gap |
| Locomotion | Left stick head-relative 2.2 m/s (8 m/s² accel, no air control); right stick snap 30° (`PlayerRig.SmoothTurn` → 2.5 rad/s). A stick whose hand points at the menu scrolls it instead | Owner scope: Boneworks-style stick only, no jump |
## Measured findings (RagdollSpike, Box3D fork, 90 Hz, 4 substeps)
1. **Light links between two strong motors oscillate.** The first build had a 0.6 kg neck link (two neck joints)
and a 0.8 kg clavicle body per arm (chest→clavicle drive + clavicle→upper-arm ball). The neck pitch oscillated
with growing amplitude (±11 rad/s head spin while standing still). Raising the shoulder to 200 N·m made the arms
*worse* (0.94 m hand lag vs 0.47 m). Box3D runs one solver iteration per substep, and a light body squeezed
between two motors with large caps doesn't converge. **Fix: no light intermediate bodies.** The neck merged into
the head (single joint at the neck base); the clavicle was replaced by one shoulder DRIVE carrying both the girdle
translation and the muscle. Result: standing error ~0, swing lag 0.13 m, random-motion eye error 2.3 cm. Rule
for the legs and anything later: keep every body that sits between two motors ≳ 1 kg, or merge it.
2. **Explicit world torques on the pelvis vs an implicit motor.** The explicit 3 Hz PD torque balance looked fine
until the arms got strong. With the light links gone, the implicit balance motor is stable and holds the pelvis
rigidly against arm reaction torques.
3. **Hand-guide strength is the "cheat budget".** With a 120 N guide the arms looked perfect, but 12 kg at arm's
length sagged only 2 cm (Nimso: it must not be holdable). At 40 N guide + 45 N·m shoulder/elbow (≈ human):
12 kg held close sags 1.3 cm, at arm's length 25 cm. Fast swings still track (0.13 m peak lag at 1.5 Hz ±0.25 m).
4. **Test poses must be anatomically possible.** Controllers pointing forward with the arms out sideways need a 90°
wrist bend. The old strong guide hid that; with honest strength the wrist limit wins. RagdollSpike's reach poses
now aim the controllers along the arm.
5. **Unreachable targets stop at the arm's end.** That is correct, not a bug: a push target 0.91 m from a 0.57 m
arm leaves the hands at full reach.
6. Hinge sign: positive `HingeJoint3D` motor velocity rotates the child about the joint's +Z (spike phase 0 measures
it; `PlayerRagdoll.HingeSign`).
## Not done yet / known gaps
- **Legs**: no physical or visual legs; the pelvis floats on ground control. Step 3 = procedural legs (Nimso:
"feet don't hold the player up" — visuals + optional collision).
- **Shoulder has no cone limit** (Box3D's motor joint has none). A strong external force can bend the arm into the
chest; muscles return it. If it shows, add a BALL limit via a second, collision-only constraint, or a clavicle
heavy enough (≥ 1 kg) to be stable.
- **Climbing**: grabs are props only (`PropGrabber`); welding to static geometry is not wired. The physics would
allow it: the hand guide reacts into the chest.
- **Strength values are guesses** at human scale (Nimso's are unpublished). One global strength scalar (Nimso's
"Crysis mode") is not exposed yet.
- **144 Hz**: not switched. Physics runs at the headset rate. Decoupling it needs physics interpolation, and today
`AnchorMoveMode`/`Locomotion` move the XR origin in `_Process`, which would jitter under interpolation.
- **Anchor-grab / teleport legacy locomotion** still drive the origin. Teleports carry the body. Grab-flying upward
now fights gravity (the body falls and the view follows it), so it effectively only pulls you along the ground.
## Step 2.1 — owner playtest fixes + sandbox (2026-10-02)
Owner playtest of step 2: "feels amazing for the most part". It found three problems:
- You could walk onto the table: the body detached for a second and wobbled before settling.
- You couldn't climb onto the table by pushing yourself up.
- You couldn't crawl under the table.
Changes and the measured reasons:
| Change | Why (RagdollSpike evidence) |
|---|---|
| **Step rule**: ground more than `MaxStepHeight` 0.35 m above the feet isn't stood on unless the hips got up there: airborne/landing, legs tucked, or hips > 60% of hip height above it, and a second ray 12 cm behind the hips also finds it | The single pelvis ray treated a 0.725 m table under the hips as a "step" → walking onto the table |
| **Lower-body collider** (`Legs`, 6 kg box from the hips down to a step above the feet, DRIVE from the pelvis). It slides up when crouching, and its shape is **disabled** (not just layer-cleared) and tucked into the pelvis when crawling or when the arms lift the body (mantle) | Knee-high obstacles and tables now block; steps ≤ 0.35 m pass under it. Box3D kept the old contact when only the collision layer changed, which jammed a crawl against a tunnel roof |
| **Structural hand strength**: guide cap = lerp(40 N, 400 N, axial share of the hand error), with a 3 cm deadband | Force along the arm (pressing down on a ledge, hanging, carrying at your side) is bone-strong; across the arm it stays weak. Measured: two-hand mantle onto 0.9 m ✓, one hand ✗ (5 cm). Pull-up on a grabbed bar: two hands lift the pelvis 0.69 m, one hand 0.24 m. 12 kg at arm's length still sags (6.6 cm vs 0.9 cm close) |
| **Arm aim from the physical shoulder**, horizontal only: the arm reaches for the real hand when the body is held away horizontally (ledge, lean-over), but keeps its True Body pose vertically | Purely camera-relative arms left the hands 26 cm short when the body was held back at a ledge. Purely physical arms fold to wherever a sagging chest is and stop being pillars, so the crawl collapsed (eye 0.14 m vs 0.42). The split fixes both |
| **Ground feed-forward from the GOAL** (pose + ground offset) | When the floor follows up onto a ledge, pose and offset move opposite; differentiating the pose alone launched the body 1 m up |
| **Crawl pose** (`TrueBodySolver`): when the hips would go below 0.15 m, the trunk tips toward ~83°, hips kick back; ragdoll hips take the full lean in a crawl | Crouch lean capped at 43° put the hips through the floor, so the head could never get low |
| **Crawl carry** 300 N + torso friction 0.3 | With the torso resting on the floor, μ·weight exceeded the support-scaled carry, so stick crawling stalled (Nimso has no stick crawl at all; ours is a comfort concession) |
| **Braced air control** 150 N, **AirSlack** 0.05 m | While mantling/hanging, the hips drift after the head; airborne, the view rides the body tightly |
| **Climbing grabs**: `PropGrabber` welds the hand to any `StaticBody3D` under the palm (5 cm sphere) except group `no_grab` (the floor) | Pull-ups, ledges, climbing-wall holds, monkey bars |
| **Procedural legs** (`ProceduralLegs.cs`): visual only. Planted feet, alternate stepping on 20 cm / 34° error, stride leads velocity by 0.25 s, 0.2–0.32 s steps; airborne they dangle, crawling they trail | Nimso: feet don't hold the body up |
| **144 Hz physics + physics interpolation**. `XROrigin3D`, `TrueBody` and `PlayerRig` (ghosts) are excluded; the ragdoll is explicitly interpolated. Legacy chase-body spikes pin 90 Hz | Owner's earlier choice; the old origin-moving code runs per frame, so its subtree must not interpolate |
| **Strength slider** (0.25–3×, scales every muscle + hand guides), **Spectator** toggle (no body, the old grab-drag flight), **Turning** snap/smooth toggle | Owner: drag movement "doesn't fit this anymore", keep it as a spectator mode |
| **Playground** (`Playground.cs`): mantling ledges 0.3–1.5 m, a 1 m platform with stairs, an 18° ramp and a 40° ramp, a climbing wall, a pull-up bar, monkey bars, a plank bridge, loose planks, a seesaw, a hanging swing board, a crate pyramid, a 25 kg box, barrels, a balance beam, a 0.6 m crawl tunnel, a duck bar, a doorway. "Reset props" resets it | Owner: a Nimso-sandbox-like level |
Ground steeper than `MaxSlopeDegrees` 35° gives no support, so the 40° ramp can't be walked up; the 18° one can.
Still open: the shoulder cone limit (needs a fork addition). `tools/playground_shots.tscn` renders overview PNGs for
layout checks.
## Step 2.2 — the 2.1 playtest was unstable; root causes (2026-10-02)
The owner reported everything "all over the place":
- Mantling swung the chest out of control and the hands "wobbled more and more".
- Crouching made the body slide around and spin.
- Nothing static was grabbable.
- The steep ramp slipped the body out from under them.
RagdollSpike passed all of it, because its input was IDEAL: smooth poses every 144 Hz tick, no jitter. Fixes:
1. **The spike now runs on REAL input by default**: tracking sample-and-hold at 90 Hz under 144 Hz physics, plus
0.5 mm / 0.1° jitter per sample (`-- ideal` / `nonoise` / `nohold` isolate the factors). Lesson: a body-physics
test with perfect input proves nothing about the headset.
2. **`PoseFilter` + `RagdollDriver`** (`scripts/RagdollDriver.cs`). Tracked poses are timestamped per new sample
and extrapolated between samples in play-space-local coordinates. One driver runs the tick pipeline for
both `PlayerRig` and the spike, so the test runs the game's code path.
3. **Low-pass every feed-forward**: muscles/elbow τ 0.05 s, ground control τ 0.12 s. Ground control's velocity
term multiplies by the whole body mass, so 1.5 mm of jitter became hundreds of newtons of buzz (pelvis
0.27 → 0.04 m/s at rest).
4. **Arm muscles stay LOCAL to the chest's target pose.** 2.1 aimed them in the physical chest frame, which closes a
rotation feedback loop: the chest turns, the world-fixed arm targets push back on the chest, and the chest turns
further. Jitter excited it into a slow twist (eye drifting 15 cm): the mantle "chest slipping". Only a
horizontal translation of the reach is kept (body held back from the camera).
5. **Structural hand strength only when the hand pushes on or holds something.** A hand reaching past its arm's
length into free air pulled the whole body down by up to 400 N (crouch: eye 11 cm off).
6. **No free-path view chasing while grounded.** Any eye/camera mismatch (crouch poses can't match exactly) moved
the view, the pose target followed, the body followed, and so on: an 8 m slide while holding a crouch. The view
is now only pushed back when geometry blocks the head. Instead, the stick stops pushing in the direction the
body is held back (`PlayerRagdoll.HeldBack`, `PlayerRig.HoldBack`).
7. **Explicit inertia on the `Legs` body.** Disabling its only collision shape (tuck) left it with no rotational
inertia; its 400 N·m drive then tumbled the pelvis forward, which flipped the "yaw". This was the crouch spin.
Rule: a body whose shapes can all be disabled needs explicit `Inertia`.
8. **World grabs: the Box3D wrapper returns a null `collider` from `IntersectShape`**, only `collider_id`. Resolve it
with `GodotObject.InstanceFromId`. The grab sphere is now 7 cm, checked at the palm and then the hand centre.
The spike asserts the game's own `PropGrabber.WorldUnderPalm` finds the bar.
New spike phases: hold a crouch (eyes 1.0 m) and a deep crouch (0.7 m) without drift (< 5 cm) or spin (< 5°),
eye on camera (< 5 cm). 29 phases total, all on real input.