Body physics Stage 2: physical arm segments (chase-driven upper arm + forearm, hierarchical budgets, layer 5)
- PhysicsArms: per side 2.0 kg upper arm + 1.2 kg forearm RigidBody3D PD-chasing their analytic 2-bone IK pose, solved per physics tick from the physical chest shoulders to the physical hands; no joints (Box3D has no usable shoulder/wrist joint; AGENTS forbids joints between tracked bodies). Budgets from a 60 kg mass stat: hand 90 < forearm 180 < upper arm 360 < chest 2400 N. Owns calibration + reach budget (moved from BodyRig); 1 m snap, non-finite reset, BodyMath.RotationError servo. - Layer 5 = player arms (mask 1); props/table/floor masks include it; no self-collision. - BodyRig: visuals only, arms drawn shoulder -> physical elbow -> wrist. - ArmSpike: track gap 0, swing lag <= 6 cm, arm pushes prop with hand clear, forearm blocked by bar without tunneling and quiet when pinned, teleport snap, NaN reset — PASS x3. - CollapseSpike now runs the full body (49/49 x3); all six spikes PASS x3. - HANDOFF/AGENTS: Stage 2 entry (item 10), layers, tuning table, Stage 2b next steps. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
cb518999e5
commit
7589b13aba
11 files changed
+708
-174
No files matched your search
+75
-24
@@ -30,9 +30,12 @@ TERMINOLOGY (owner-corrected — use precisely):
|
||||
RigidBody3D/StaticBody3D/CharacterBody3D so the flip is lossless.
|
||||
- Runtime stack: **WiVRN + WayVR** on Linux. Headset + runtime support hand tracking (treat as available).
|
||||
- Renderer **Forward+** (Mobile is the docs' VR recommendation; deferred until profiling justifies).
|
||||
- Build: `dotnet build`. Headless validation — physics: `godot-mono --headless --path . scenes/physics_spike.tscn`
|
||||
must print **"SPIKE: overall PASS"**; grab: `godot-mono --headless --path . scenes/grab_spike.tscn`
|
||||
must print **"GRAB: overall PASS"**. Both PASS as of this handoff. Headless asset import:
|
||||
- Build: `dotnet build`. Headless validation: `godot-mono --headless --fixed-fps 90 --path . scenes/<spike>.tscn`
|
||||
(`--fixed-fps 90` runs physics faster than real time at the real 90 Hz tick — the 49-scenario
|
||||
collapse spike takes seconds instead of minutes). Spikes and their pass lines:
|
||||
`physics_spike` "SPIKE: overall PASS", `weld_spike` "WELD: …", `grab_spike` "GRAB: …",
|
||||
`body_spike` "BODY: …", `collapse_spike` "COLLAPSE: …", `arm_spike` "ARM: overall PASS".
|
||||
All six PASS 3× as of this handoff. Headless asset import:
|
||||
`godot-mono --headless --import`.
|
||||
- Engine logs: `~/.local/share/godot/app_userdata/VR Messs/logs/godot.log` — **read after user
|
||||
playtests instead of guessing**.
|
||||
@@ -124,7 +127,7 @@ TERMINOLOGY (owner-corrected — use precisely):
|
||||
2026-10-01; the first playtest's NaN collapse is root-caused + fixed (servo math, see §5).
|
||||
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
|
||||
+ `ReachOffset` (fed by PhysicsArms' reach budget). Linear drive = the PhysicsHands pattern at
|
||||
body mass (force-clamped PD, `MaxForce` 2400 N ⇒ 80 m/s², `MaxSpeed` 6 m/s). Orientation =
|
||||
slewed angular-velocity servo toward the yaw target (Kp 20 rad/s per rad, spin cap 10 rad/s,
|
||||
alpha cap 150 rad/s²) — self-rights when shoved, holds yaw. `SnapDistance` 2 m snap safety
|
||||
@@ -136,11 +139,39 @@ TERMINOLOGY (owner-corrected — use precisely):
|
||||
`scenes/body_spike.tscn` must print **"BODY: overall PASS"** (pair-rule proofs: one-way
|
||||
floor contact, hand-through-chest exclusion, prop-contact two-way; track/yaw/impact/snap/
|
||||
self-right asserts).
|
||||
- **BodyRig.cs** — placeholder rig VISUALS only now (v1.3): head cube (HMD-driven), 2-bone
|
||||
analytic arms (soft IK 0.98, pole as before), shoulders derived from the PhysicsTorso BODY
|
||||
pose (+ ShoulderRise 0.25, ± body X). The torso mesh and the torso yaw spring moved into
|
||||
PhysicsTorso; BodyRig computes the reach budget (hysteresis unchanged) and feeds it to
|
||||
`PhysicsTorso.ReachOffset`. Hidden until calibrated.
|
||||
- **PhysicsArms.cs** — body-physics **Stage 2: physical arm segments** (2026-10-02). Per side an
|
||||
upper-arm (2.0 kg) and forearm (1.2 kg) RigidBody3D box, gravity off, CCD, **layer 5 (player
|
||||
arms) / mask 1** — collide with world + props, never with the own chest, hands or each other.
|
||||
NO joints (Box3D has no usable 3-DOF shoulder/wrist joint — PinJoint's identity spring, no
|
||||
Generic6DOF — and AGENTS.md forbids joints between tracked bodies): each segment PD-chases
|
||||
its analytic 2-bone IK pose, solved EVERY PHYSICS TICK from the PHYSICAL chest shoulders to
|
||||
the PHYSICAL hand bodies (IK moved here from BodyRig: soft IK 0.98, torso-frame pole with
|
||||
the shoulder→hand axis projected out, hand-forward push, exp-smoothed). **Hierarchical
|
||||
budgets** (Nimso muscle model) from `BodyMassStat` 60 kg: hand 90 N ≺ forearm 180 N ≺ upper
|
||||
arm 360 N ≺ chest 2400 N. Orientation = slewed angular-velocity servo via
|
||||
`BodyMath.RotationError`; segment roll follows the body's current X axis (no flips).
|
||||
Safety: 1 m snap (teleports), `ResetIfNonFinite`. Also owns calibration (arm lengths +
|
||||
shoulder width from first head height > 0.5 m; segments spawn then) and the REACH BUDGET
|
||||
(torso lean toward overreach → `PhysicsTorso.ReachOffset`, hysteresis unchanged). Exposes
|
||||
`Shoulder/Elbow/Wrist/ElbowTarget/WristTarget(side)`; `Elbow` = where the two physical
|
||||
segments meet. Spike: `scenes/arm_spike.tscn` (track gap, fast swing, arm pushes a prop
|
||||
with the hand clear, forearm blocked by a bar without tunneling + quiet while pinned,
|
||||
teleport snap, NaN-injection reset, chest undisturbed by own arms).
|
||||
- **BodyRig.cs** — placeholder rig VISUALS only: head cube (HMD-driven) + arm boxes drawn
|
||||
shoulder (PhysicsArms, from the chest body) → PHYSICAL elbow → hand wrist, stretched to stay
|
||||
connected (an obstructed segment shows as a displaced elbow). Hidden until PhysicsArms
|
||||
calibrates. IK, calibration and reach budget moved to PhysicsArms; torso mesh + yaw spring
|
||||
live in PhysicsTorso.
|
||||
- **BodyMath.cs** — shared servo math. `RotationError(target, current)` = shortest-arc
|
||||
axis·angle via `2·atan2(|xyz|, W)` — **the only allowed way to compute servo rotation
|
||||
errors** (`Quaternion.GetAngle()` NaNs at W = 1.0000001, `GetAxis()` is un-normalized near
|
||||
identity — the Stage-1 collapse). `ResetIfNonFinite(body, target, who)` — escape hatch run
|
||||
by every driven body (hands, chest, arm segments).
|
||||
- **CollapseSpike.cs** + `scenes/collapse_spike.tscn` — Stage-1 collapse regression with the
|
||||
full body (chest + arms + hands): 9 solver-stacking scenarios (seated chest in the static
|
||||
table, two-hand-welded 50 kg anvil + the logged grab sequence, real PropShop sweep) + 40
|
||||
slow-yaw-drift scenarios (the root-cause repro: old code NaN'd 7/40). Must print
|
||||
"COLLAPSE: overall PASS" (49/49).
|
||||
- **GrabSpike.cs** + `scenes/grab_spike.tscn` — headless grab regression (v5.3): a hand stand-in
|
||||
(0.4 kg body, `HandMaxForce` 90 N chase) grabs a 1 kg prop via ConeTwist weld (server type
|
||||
verified, configure-before-AddChild); drag 2 m + 360° fast-spin sub-phase — asserts RIGID
|
||||
@@ -175,8 +206,9 @@ leaving the standard RigidBody3D stack).
|
||||
|
||||
Scene wiring (`scenes/main.tscn`): Main → WorldEnvironment, Sun, Floor, XROrigin3D (XRCamera3D,
|
||||
LeftHand, RightHand, WorldPanel), SpawnPoint, 3 TeleportMarkers, Locomotion, AnchorMoveMode,
|
||||
PropShop, PropGrabber, PhysicsHands, PhysicsTorso, BodyRig, FadeLayer/Fade. Action map: `openxr_action_map.tres`.
|
||||
Floor StaticBody scans layer 3 (collision_mask 13).
|
||||
PropShop, PropGrabber, PhysicsHands, PhysicsTorso, PhysicsArms, BodyRig, FadeLayer/Fade
|
||||
(physics order matters: hands → chest → arms, so the arm IK reads this tick's bodies). Action map: `openxr_action_map.tres`.
|
||||
Floor StaticBody mask 1|3|4|5 (collision_mask 29).
|
||||
|
||||
## 4. Architecture decisions that must not regress
|
||||
|
||||
@@ -201,10 +233,14 @@ Floor StaticBody scans layer 3 (collision_mask 13).
|
||||
as fallback (git history). Stink-O's box3d-godot (custom components, more joint types) is
|
||||
the plan-B.
|
||||
- **Collision layers**: 1 = world, 2 = UI. Panel/knob bodies are layer 2 mask 0 (props pass through
|
||||
the menu); **1 = world+props, 2 = UI, 3 = player body (chest), 4 = player hands** — chest↔own
|
||||
hands excluded BOTH ways (no self-collision; props mask 1|3|4, floor mask 1|3|4, hands 4/1|4,
|
||||
chest 3/1|3). Layers/masks live on `CollisionObject3D`, never `CollisionShape3D`. The
|
||||
the menu); **1 = world+props, 2 = UI, 3 = player body (chest), 4 = player hands, 5 = player arms** —
|
||||
no self-collision between any own body parts (props/table/floor mask 1|3|4|5, hands 4/1|4,
|
||||
chest 3/1|3, arm segments 5/1). Layers/masks live on `CollisionObject3D`, never `CollisionShape3D`. The
|
||||
body_spike proves the pair rules on Box3D.
|
||||
- **Servo rotation errors only via `BodyMath.RotationError`** — never `Quaternion.GetAngle()`
|
||||
/ `GetAxis()` (NaN at W = 1.0000001 killed the chest in the first body playtest).
|
||||
- **No joints between player body parts** — arms are chase segments; Box3D can't do a
|
||||
shoulder/wrist joint cleanly (see PhysicsArms).
|
||||
- **Ghosts belong to hands, not props** — the whole arm gets a ghost later.
|
||||
- **Standard-RigidBody3D-only** for scene objects and tracked bodies, so the Jolt fallback stays lossless.
|
||||
- **`pose_recentered` is never used for placement** — WiVRN fires it continuously (~90 Hz);
|
||||
@@ -226,6 +262,13 @@ Floor StaticBody scans layer 3 (collision_mask 13).
|
||||
static table + two-hand-welded 50 kg anvil) was tested and is stable. Full record:
|
||||
`research/body-stage1-failure-brief.md` (RESOLUTION section). **RULE: never use
|
||||
`Quaternion.GetAngle()/GetAxis()` for servo errors — use `BodyMath.RotationError`.**
|
||||
- **Body Stage 2 (physical arms) — unvalidated in-headset.** Things to watch in the first
|
||||
playtest: elbow drawn displaced when a segment is blocked (that's the honest physics view),
|
||||
arms sweeping props when seated at the table, any stutter at fast swings (spike: ≤ 6 cm
|
||||
joint lag at 2 Hz × 0.4 m). Known gaps by design (Stage 2b, feel-driven): a blocked forearm
|
||||
does NOT hold the hand back, and forces on the arms don't reach the chest — both need a
|
||||
coupling decision (script-side feedback vs joints) the owner should weigh in on after
|
||||
feeling 2a.
|
||||
- **Seated calibration** — BodyRig calibrates proportions from the head height at first
|
||||
frame; seated (1.01 m) clamps arm length to the 0.50 m minimum, so normal reaches saturate
|
||||
`ReachOffset` and lean the chest into the table (stable, but wrong proportions). Needs a
|
||||
@@ -293,18 +336,25 @@ Floor StaticBody scans layer 3 (collision_mask 13).
|
||||
| `GrabRadius` / `CenterGrabRadius` | PropGrabber.cs | 0.16 m / 0.12 m | palm-point / hand-center grab range |
|
||||
| `PalmGrabOffset` | PropGrabber.cs | ±0.045 m along hand +X (left +, right −) | grab point on the palm (taste choice) |
|
||||
| Grip/trigger thresholds | several files | 0.6 | polled Float edges |
|
||||
| `SoftIKFactor` | BodyRig.cs | 0.98 | shoulder→hand distance clamp before IK solve (lower toward 0.95 if shake persists) |
|
||||
| `PoleHandWeight` / `PoleSmoothRate` | BodyRig.cs | 0.25 / 10 | elbow pole: hand-forward push / exp smoothing |
|
||||
| `SoftIKFactor` | PhysicsArms.cs | 0.98 | shoulder→hand distance clamp before IK solve (lower toward 0.95 if shake persists) |
|
||||
| `PoleHandWeight` / `PoleSmoothRate` | PhysicsArms.cs | 0.25 / 10 | elbow pole: hand-forward push / exp smoothing |
|
||||
| `TorsoYawHandsWeight` / `TorsoYawForwardness` / `TorsoYawSmoothRate` | PhysicsTorso.cs | 0.6 / 0.3 m / 12 | torso yaw spring: hands blend / forwardness normalizer / exp smoothing (≈200 ms) |
|
||||
| `TorsoMass` / `MaxForce` / `MaxSpeed` | PhysicsTorso.cs | 30 kg / 2400 N / 6 m/s | chest chase budget (force-clamped PD) |
|
||||
| `OrientKp` / `OrientMaxSpin` / `OrientMaxAlpha` | PhysicsTorso.cs | 20 / 10 rad/s / 150 rad/s² | chest orientation servo (yaw hold + self-right) |
|
||||
| `SnapDistance` | PhysicsTorso.cs | 2 m | chest teleport snap safety |
|
||||
| `armTotal` | BodyRig.cs | clamp(0.46×headHeight, 0.5, 0.72) | total arm length, 50/50 split |
|
||||
| `ReachPushSpeed` / `ReachDecaySpeed` | BodyRig.cs | 0.8 / 0.4 m/s | torso lean toward / back from overreach |
|
||||
| `ReachDecayFactor` | BodyRig.cs | 0.92 | hysteresis band (decay only below 92% of reach) |
|
||||
| `ReachBudgetMax` | BodyRig.cs | 0.25 m | horizontal torso offset clamp |
|
||||
| `ReachVerticalMin/Max` | BodyRig.cs | −0.05 / +0.12 m | vertical torso offset clamp (stops upward oscillation) |
|
||||
| `HeadBackOffset` / `TorsoBackOffset` / `TorsoDrop` | BodyRig.cs | 0.06 / 0.06 / 0.42 m | keeps boxes out of the view |
|
||||
| `armTotal` | PhysicsArms.cs | clamp(0.46×headHeight, 0.5, 0.72) | total arm length, 50/50 split |
|
||||
| `ReachPushSpeed` / `ReachDecaySpeed` | PhysicsArms.cs | 0.8 / 0.4 m/s | torso lean toward / back from overreach |
|
||||
| `ReachDecayFactor` | PhysicsArms.cs | 0.92 | hysteresis band (decay only below 92% of reach) |
|
||||
| `ReachBudgetMax` | PhysicsArms.cs | 0.25 m | horizontal torso offset clamp |
|
||||
| `ReachVerticalMin/Max` | PhysicsArms.cs | −0.05 / +0.12 m | vertical torso offset clamp (stops upward oscillation) |
|
||||
| `HeadBackOffset` | BodyRig.cs / PhysicsTorso.cs | 0.06 m | keeps the head cube out of the view |
|
||||
| `TorsoBackOffset` / `TorsoDrop` | PhysicsTorso.cs | 0.06 / 0.42 m | chest target below/behind the head |
|
||||
| `BodyMassStat` | PhysicsArms.cs | 60 kg | scales the arm budgets (N/kg below) |
|
||||
| `ForearmForcePerKg` / `UpperArmForcePerKg` | PhysicsArms.cs | 3 / 6 N/kg (180 / 360 N) | arm segment chase budgets (hand 90 ≺ fore ≺ upper ≺ chest 2400) |
|
||||
| `ForearmMass` / `UpperArmMass` | PhysicsArms.cs | 1.2 / 2.0 kg | arm segment masses |
|
||||
| `SegmentMaxSpin` / `ForearmMaxAlpha` / `UpperArmMaxAlpha` | PhysicsArms.cs | 40 rad/s / 1500 / 1000 rad/s² | arm segment orientation servo caps |
|
||||
| `SnapDistance` | PhysicsArms.cs | 1 m | arm segment teleport snap |
|
||||
| `ShoulderRise` | PhysicsArms.cs | 0.25 m | shoulder height above the chest body center |
|
||||
| `LaserMinDistance` | Locomotion.cs | 0.35 m | laser hidden nearer than this (tip touch instead) |
|
||||
| `MarkerNearDistance` | Locomotion.cs | 1.2 m | near markers hide + become untargetable |
|
||||
| Fade times | Locomotion.cs | 0.12 s in / 0.25 s out | teleport fade |
|
||||
@@ -324,8 +374,9 @@ Floor StaticBody scans layer 3 (collision_mask 13).
|
||||
must decouple first if deleted).
|
||||
3. **Grab points on special objects** — grab is palm-proximity to the prop origin today;
|
||||
special objects may want authored grab anchors (see `research/vr-grabbing.md` taxonomy).
|
||||
4. **Physical torso/head capsules** — folded into the body-physics track (item 1): the torso
|
||||
chase body IS the capsule replacement; head/body colliders join it in the same stage.
|
||||
4. **Body Stage 2b (feel-driven)** — after the owner feels the 2a arms: couple obstruction
|
||||
back into the hands (blocked forearm limits the hand) and arm forces into the chest; then
|
||||
head collider. Decide calibration for seated sessions (see §5).
|
||||
5. **Hand tracking spike** — `XRHandTracker` through WiVRN; controller tip and fingertip must feed
|
||||
the same cursor concept (interaction must not assume controllers).
|
||||
6. **Neck/head look system** — `LookAtModifier3D` head→neck distribution (needs a real skeleton).
|
||||
|
||||
Reference in new issue
Block a user