WorldPanel is now the project's one SubViewport screen: MainMenu (tabs Player /
World / System; Return to spawn, Recalibrate body, Reset props, Ping, click
volume slider, scrollable log, FPS, Close) rendered by MenuScreen into a
960x640 SubViewport on a thin 0.36x0.24 m panel.
Input = touch-first hybrid cursor injected as mouse events: both controller
rays are projected analytically onto the panel plane, converted to viewport
pixels and pushed via Viewport.PushInput (MouseMotion/MouseButton, wheel from
thumbstick). Stylus tip within 6 cm = touch cursor (press crossing 0.6 cm from
the front, release past 1.8 cm); farther = laser cursor with trigger edge. One
owning hand per viewport. Haptic tick + synthesized click on press.
Kept: ax_button summon/dismiss, XROrigin3D parenting, grip drag (direct/laser)
with head re-orient, IsBlocking/IsPanelCollider. Locomotion's beam now takes
its length from WorldPanel.TryGetLaserDistance (no beam while touching) and
starts at the stylus tip. Recalibrate calls Recalibrate() on the first node in
group "calibration" (disabled when absent).
New headless regression scenes/menu_spike.tscn ("MENU: overall PASS").
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
500 lines
42 KiB
Markdown
500 lines
42 KiB
Markdown
# VR Messs — Agent Handoff
|
||
|
||
Read `AGENTS.md` in the repo root first — it is the authoritative project brain. This file
|
||
orients you; AGENTS.md governs. Verify every claim here against the code when you touch it
|
||
(code wins over both docs if they disagree).
|
||
|
||
## 1. What this project is
|
||
|
||
VR anime-character **scene viewer** — Koikatsu-VR-class, but with real physics hands.
|
||
**Not a game**: no loops, quests, progression. Explore, interact, touch, pose — that's the product.
|
||
This repo is the experimentation lab: designs are hypotheses validated with cheap throwaway spikes.
|
||
|
||
TERMINOLOGY (owner-corrected — use precisely):
|
||
- **"getting a body"** = the physical rig (torso/arms/hands, the interaction body).
|
||
- **"embodying"** = viewing from a character's perspective in an animation (view-follow + avatar).
|
||
Two separate systems. Never call the rig milestone "embodying".
|
||
|
||
## 2. Toolchain & environment
|
||
|
||
- **Godot 4.7.2 mono** (binary `godot-mono`), **C# / .NET 8** (dotnet SDK 10 present). PCVR, desktop-only.
|
||
- **Physics: Box3D** via `addons/godot-box3d` GDExtension (bearlikelion server-replacement, v0.2.6);
|
||
setting `physics/3d/physics_engine="Box3D Physics"`. Exactly ONE `.gdextension` file may exist —
|
||
a stray duplicate double-loads the library and breaks the server ("required virtual method" errors).
|
||
**The addon is LOCALLY PATCHED** (grab v5.3 depends on it): the wrapper now maps Godot's
|
||
ConeTwistJoint3D → Box3D's b3WeldJoint (rigid grab weld). Originals in `addons/godot-box3d/bin-stock/`;
|
||
source + rebuild recipe + patch in `.vendor/` (`.vendor/box3d-weld.patch`,
|
||
`.vendor/godot-box3d/`, see `research/godot-box3d-weld.md`) — **only the Linux binary is
|
||
patched**; Windows/macOS binaries are stock (ConeTwist still errors there until rebuilt).
|
||
**Second patch (2026-10-02): `Generic6DOFJoint3D` → Box3D ball (spherical, cone/twist limits, torque-capped
|
||
motor = MUSCLE) / weld / drive (motor joint)** — fork branch `generic-6dof-joint`, `.vendor/box3d-generic-6dof.patch`,
|
||
details + C# muscle recipe in `research/godot-box3d-6dof.md`. Spring stiffness/damping are Box3D Hz/ratio.
|
||
Jolt fallback = flip that one project-setting line; all gameplay code stays standard
|
||
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: `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",
|
||
`sixdof_spike` "6DOF: overall PASS" (run FIRST with `weld_spike` whenever the addon binary changes),
|
||
`menu_spike` "MENU: overall PASS" (in-VR menu input injection, no XR). All eight PASS as of this handoff.
|
||
**Fresh checkout / new worktree: run `godot-mono --headless --import` FIRST** (it may segfault at
|
||
exit — harmless) — without `.godot/extension_list.cfg` the Box3D extension silently doesn't load,
|
||
Godot falls back to its default physics and weld/grab/arm/6dof spikes report CHECK. 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**.
|
||
- **Docs rules (project's #1 failure mode)**: only the local pinned 4.7 docs at
|
||
`/home/foxyz/Projects/Games/.shared/godot-docs-4.7` (full manual + class ref in `.rst`; rg it).
|
||
Web fallback only `https://docs.godotengine.org/en/4.7/...` — version goes IN the URL, never `/latest/`.
|
||
NEVER trust training-memory API names. Known traps: `AudioStreamWAV` is `AudioStreamWav` in C#;
|
||
collision layer/mask live on `CollisionObject3D`, NOT `CollisionShape3D`; `SkeletonIK3D` is
|
||
deprecated in 4.7. If docs and compiler disagree, grep the shipped
|
||
`/usr/lib/godot-mono/GodotSharp/Api/Debug/GodotSharp.dll`.
|
||
|
||
## 3. Current systems (scripts/)
|
||
|
||
- **Main.cs** — OpenXR startup: finds `OpenXRInterface`, `UseXR`, VSync off, VRS XR mode; wires
|
||
session signals (begun/visible/focussed/stopping/`PoseRecentered` → re-emitted as C# signals
|
||
FocusLost/FocusGained/PoseRecentered). On session begin: picks highest refresh rate ≤
|
||
`MaximumRefreshRate` (export, 90) and sets `Engine.PhysicsTicksPerSecond` to it. Pauses tree on
|
||
focus lost. No OpenXR → flat preview fallback (PushWarning, scene still runs).
|
||
- **Locomotion.cs** — locomotion manager. Spawn anchoring ONCE on first `FocusGained` (HMD xz to
|
||
`SpawnPoint` + spawn yaw; real head height and pitch kept — `PlacePlayer` pins arrival height).
|
||
`PoseRecentered` is deliberately IGNORED for placement (WiVRN spams it at ~90 Hz; logged once).
|
||
Teleport: aim + trigger edge, fade to black 0.12 s in / 0.25 s out, near markers within
|
||
`MarkerNearDistance` (1.2 m) hide fully (disc/beam/label) and are not targetable. Lasers via
|
||
`UpdatePointer`: shown only when aiming at something interactive (marker or panel) AND beyond
|
||
`LaserMinDistance` (0.35 m); for the menu the beam length comes from
|
||
`WorldPanel.TryGetLaserDistance` (beam only while that hand is in LASER mode — a hand touching the
|
||
menu shows no beam); the beam starts at the stylus tip (`LaserStart` 0.12 m) and ends at the hit.
|
||
Also builds the stylus pointer tip per controller. `ReturnToSpawn()` is public (menu "Return to
|
||
spawn" calls it).
|
||
- **AnchorMoveMode.cs** — grab-world movement, ANCHOR semantics. One-hand grip moves the player
|
||
OPPOSITE to hand motion (full 3D); both grips = anchor translation + in-place yaw turn around the
|
||
head. Grip is suppressed while that hand prop-grabs (`PropGrabber.IsGrabbing`) or the panel is
|
||
near (`WorldPanel.IsBlocking`). **All deltas and turn pivots in WORLD space** (tracking-space math
|
||
made turns orbit the map center — do not regress). Drives `XROrigin3D` directly.
|
||
- **PhysicsHands.cs** — physical palm bodies chasing the controller poses: 0.4 kg boxes (flat slab,
|
||
thumb-side face), gravity off, CCD, **layer 4 (player hands)/mask 1|4** — NOT colliding with the
|
||
chest (layer 3), excluded both ways. Chase = desired velocity `(target-pos)/dt`
|
||
force-clamped to `HandMaxForce` 90 N; rotation hard-locked by setting angular velocity to the
|
||
controller's rotation error (clamped to 120 rad/s). Safety: stuck (> 0.2 m for 1 s) → snap +
|
||
log; > `SnapDistance` 2 m → silent teleport snap — BOTH SUSPENDED for a hand while
|
||
`PropGrabber.IsGrabbing(controller)` (a joint legitimately holds the hand back; teleporting a
|
||
jointed hand yanks the prop — the grabber's grip break runs first instead, rip at 0.8 m).
|
||
Hand ghosts: translucent palm boxes shown at the controller pose when the physical hand lags
|
||
> 0.1 m — ghosts belong to the HANDS, not props. Static `GetBody(controller)` lookup used by
|
||
grabber and rig. The chase target passes through `PhysicsArms.ConstrainHand` first (arm
|
||
coupling); the stuck snap is also suspended while that coupling is active.
|
||
- **PropGrabber.cs** — grab v5.3: SOLVER-OWNED RIGID WELD via a LOCALLY PATCHED addon (needs
|
||
`research/godot-box3d-weld.md` + the v5.2 history in `research/vr-two-handed-grab.md` — the
|
||
pin joint's hidden spring targets IDENTITY and made every earlier iteration a "winch":
|
||
point constraint + rotator, which the owner rejected as not-how-grab-works: ball orbiting
|
||
the palm, anvil surface drifting under the palm). Grip edge grabs nearest prop in group
|
||
"prop" from palm point (`GrabRadius` 0.16 m, fallback hand center `CenterGrabRadius` 0.12 m)
|
||
— palm-proximity only. On grab: creates a **`ConeTwistJoint3D`** (mapped by the patch to
|
||
Box3D's `b3WeldJoint`) parented under PropGrabber, NodeA = physical hand body, NodeB = prop,
|
||
`SwingSpan = 0`, `TwistSpan = 0`, `Position` = palm point, `ExcludeNodesFromCollision = true`.
|
||
The weld LOCKS THE RELATIVE POSE AT CREATION (the patch pre-corrects frame B with both
|
||
bodies' rotations — no identity-target snap). Configure Position + NodeA + NodeB + spans +
|
||
bias BEFORE `AddChild` — mandatory ordering (configuring after raced Box3D and produced dead
|
||
joints). **NO script forces/torques/damp anywhere** — v5.2's whole servo (PD, gravity
|
||
joints) — v5.2's whole servo (PD, gravity
|
||
feed-forward, MaxHeldSpin, AngularDamp raise) is DELETED; the solver owns everything and
|
||
weight flows through the weld into the hand's 90 N budget honestly. **NO per-tick joint
|
||
re-positioning** — the weld is not a hand-following pivot (moving the node after creation
|
||
does nothing). **BOTH hands weld RIGID (bias 0) — v5.3.1** (the owner rejected the soft
|
||
secondary: it gave ~10-15 cm, "feels like a joint again"; hand₁+prop+hand₂ rigidly welded
|
||
is one composite body, and incompatible controller demands resolve through the HANDS
|
||
lagging, not the object stretching — spike-measured anchors 2–3 mm under full opposite
|
||
tug). If a future jitter appears under wrist-conflict, reintroducing a soft secondary bias
|
||
is the fallback. NO promotion machinery — with both welds rigid from grab, takeover is
|
||
automatic (the survivor's weld simply persists). Grip break:
|
||
prop-local anchor vs live palm point > `GripRipDistance` (0.8 m) instant rip;
|
||
> `BreakDistance` (0.25 m) sustained > `BreakTime` (0.25 s); both hands > `BreakDistanceBoth`
|
||
(0.4 m) breaks two-handed (no promotion on simultaneous break); with a rigid weld these
|
||
rarely fire — they are the stretch/anomaly safety net; when a two-handed hold releases one
|
||
hand, the composite rings briefly (~3° peak, decays < 1 s — stored weld stress discharging). NEW escape hatch: hand body lagging
|
||
its controller beyond `PhysicsHands.SnapDistance` (2 m) → "grip dragged off" instant break
|
||
(heavy props can drag the hand since PhysicsHands' snap is suspended while gripping).
|
||
- **PropShop.cs** — builds table (StaticBody3D) + spawns prop table from `Specs[]` (ping pong 2.7 g
|
||
→ anvil 50 kg), Label3D captions, CCD for < 0.1 kg, group "prop". `Respawn()` = menu "Reset props".
|
||
- **WorldPanel.cs** — the in-VR MENU (2026-10-02; replaced the chunky 3D knob panel). XR glue only:
|
||
PARENTED UNDER `XROrigin3D` (fixed in player's room; revert = reparent to Main + fix node paths).
|
||
Summoned/dismissed via `ax_button` (A/X; menu_button belongs to the runtime), appears
|
||
`SummonDistance` 0.3 m in front of the summoning hand facing the head; grip-drag direct (hand
|
||
within `DirectGrabRadius` 0.45 m) or along the laser at fixed ray distance, re-orients toward the
|
||
head while gripped; "Return to spawn" recalls it `HeadRecallDistance` 0.45 m ahead / 0.12 m below
|
||
the head. Feeds both controllers (ray = `-Basis.Z`, `trigger`, `primary` stick; a hand dragging
|
||
the panel doesn't point) into MenuScreen every frame, wires MainMenu events (deferred) to
|
||
`Locomotion.ReturnToSpawn`, `PropShop.Respawn`, ping tone, close; synthesized click + ping
|
||
(`AudioStreamWav` PCM, no asset), haptic tick (`"haptic"` action) on press. `IsBlocking(controller)`
|
||
(hand within 0.45 m of the shown panel) makes Locomotion/AnchorMoveMode defer grab/teleport;
|
||
`IsPanelCollider(node)` = the screen's StaticBody (**layer 2, mask 0**, props pass through; shapes
|
||
disabled while hidden); `TryGetLaserDistance(controller)` drives the laser beam.
|
||
- **MenuScreen.cs** — generic "Control UI on a 3D panel" + the touch-first hybrid cursor,
|
||
input-agnostic (spike drives it without XR). SubViewport 960×640 (`Disable3D`, MSAA 2D 4×,
|
||
update Always while shown / Disabled while hidden) on an explicit-UV quad 0.36×0.24 m (unshaded,
|
||
linear filter — viewport textures have no mipmaps, resolution chosen ≈ 1:1 with headset pixels at
|
||
~45 cm) + backing frame + StaticBody. Per hand, in panel-local space (analytic plane math, no
|
||
raycast): TOUCH if the stylus tip (`TipLength` 0.12 m ahead) is over the panel (+1.5 cm edge
|
||
margin) between `HoverDepth` 6 cm in front and `PushThroughDepth` 8 cm behind, controller in
|
||
front — press when the tip crosses `ContactDepth` 0.6 cm FROM ABOVE (armed only by hovering in
|
||
front: sideways/behind entry never presses), release past `ReleaseDepth` 1.8 cm; else LASER if
|
||
the ray hits the panel from the front — trigger rising edge presses, release on trigger up
|
||
(trigger state tracked even while hidden/reset, so a held trigger never clicks). One mouse per
|
||
viewport → one OWNER hand: the hand holding a press keeps it; else the touching hand (smallest
|
||
depth); else the previous laser owner; else any laser hand. Owner change / leaving the panel
|
||
releases the button at the last position (Godot then cancels the click, like dragging off a
|
||
button). Injection: `Viewport.PushInput(InputEventMouseMotion{Position, Relative, ButtonMask},
|
||
true)` on cursor move, `InputEventMouseButton{Left}` on press/release, `NotifyMouseEntered/Exited`
|
||
on owner gain/loss; owner's stick Y beyond 0.3 → wheel notches at 14/s (`WheelUp/Down`, press +
|
||
release). Cursor dot per hand (touch: blue, shrinks as the tip nears; laser: orange; non-owner
|
||
half-transparent). Buttons fire on release (Godot default) = tap semantics.
|
||
- **MainMenu.cs** — the menu content (`PanelContainer`, built in code, own `Theme`: 26 px font,
|
||
64 px buttons ≈ 24 mm, round 36 px slider grabber, wide scrollbar, hover/pressed styles, no focus
|
||
boxes). Header: title, FPS (4 Hz), Close. Tabs: **Player** (Return to spawn, Recalibrate body),
|
||
**World** (Reset props, Ping sound), **System** (click volume slider, Close menu, scrollable log).
|
||
**Calibration contract**: "Recalibrate body" calls `Call("Recalibrate")` on
|
||
`GetTree().GetFirstNodeInGroup("calibration")` if it `HasMethod("Recalibrate")`; otherwise the
|
||
button is disabled with "Not available" (re-checked on summon and at 4 Hz). The menu never creates
|
||
that node. Button actions are C# events invoked via `CallDeferred` (never from inside the
|
||
injected input event). `Log(line)` appends to the System log (last 40).
|
||
- **MenuSpike.cs** + `scenes/menu_spike.tscn` — headless menu regression without XR: MenuScreen +
|
||
MainMenu on a tilted panel; asserts pixel mapping round trip, touch path (hover, contact press,
|
||
Return-to-spawn fires once while the other hand idles as a laser = arbitration), side entry
|
||
behind the surface does NOT press, laser + trigger (tab switch, Reset props, laser distance),
|
||
held trigger arriving does NOT click, touch tab switch, laser slider drag, thumbstick scroll of
|
||
the log, Recalibrate disabled without / enabled + called with a stub "calibration" node, header
|
||
Close by touch. Must print "MENU: overall PASS".
|
||
- **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; 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 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
|
||
covers teleports. Camera stays KINEMATIC — the torso chases the head, never the reverse
|
||
(Boneworks sickness lesson). Yaw spring (HMD + hands blend) MOVED here from BodyRig.
|
||
Collision: layer 3 (player body), mask 1|3 — props/floor touch it, own hands do NOT
|
||
(hands moved to layer 4; excluded both ways — no self-collision, VRChat/Nimso precedent).
|
||
Owns its torso mesh (hidden until the body first arrives at target). Spike:
|
||
`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).
|
||
- **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; elbow picked on its circle as an ANGLE around the arm axis relative to a torso-frame
|
||
down/back/out reference: 85% of the angle the HAND suggests — the forearm continues behind
|
||
the wrist along the CONTROLLER's (aim pose) forward axis, not the physics palm's, so a palm
|
||
fighting contact can't swing the elbow — clamped to ±110° (never through the inward/up side)
|
||
and rate-limited to 6 rad/s (no 180° flips; spike: 1.8 cm/tick max vs 13.8 unclamped);
|
||
capped at max(shoulder, wrist) + 5 cm height). **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 torso LEAN
|
||
toward overreaching hands → `PhysicsTorso.ReachOffset`, computed OPEN-LOOP from
|
||
`PhysicsTorso.NominalTarget/TargetBasis` (the head-derived pose without the lean). The old
|
||
closed loop (lean from the body's own shoulders) made the chest hunt back and forth —
|
||
owner-reported, spike-measured 1.1 m/s → 0.019 m/s after the fix. Props held by a hand get
|
||
a collision exception with that side's arm segments (`SetHeld`, called by PropGrabber) —
|
||
a 180 N forearm shoving a prop welded to a 90 N hand was a fight. **Stage 2b hand coupling**
|
||
(`ConstrainHand`, called by PhysicsHands before chasing): while an arm segment is in
|
||
contact, the hand may lead the forearm's end by at most `HandArmSlack` 3 cm — a blocked
|
||
forearm holds the hand back and the hand ghost shows the controller. Free arms never limit
|
||
the hand. Fixes the owner's "palm behind the anvil, forearm rests on it" clip (drawn
|
||
forearm 3.9 cm into the anvil → 0; the collider was always fine at ≤ 6 mm). Exposes
|
||
`Shoulder/Elbow/Wrist/ElbowTarget/WristTarget(side)`; `Elbow` = where the two physical
|
||
segments meet. Spike: `scenes/arm_spike.tscn` (stretch-beyond-reach chest settle, hug elbows out
|
||
0.17 m (old pole: 0.02), held 50 kg anvil stable + forearm out of it, 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
|
||
relative pose (grab-rotation error < 2° drag / < 3.5° spin, anchor separation < 1 cm — the
|
||
winch/orbit regression; 2° is the gravity-on floor for an off-center grab, don't tighten
|
||
below that); two-handed phase (second weld soft, bias 0.3): primary anchor < 1 cm rigid
|
||
under full tug, tug give bounded < 0.25 m (recovers), follow-regime anchor < 0.05 m; then
|
||
primary releases, survivor weld recreated rigid (takeover) and rigidity re-asserted.
|
||
Reach-timeout guards; must pass 3× in a row (never accept flaky).
|
||
- **WeldSpike.cs** + `scenes/weld_spike.tscn` — regression for the PATCHED ADDON itself: two
|
||
bodies welded via ConeTwistJoint3D (bias 0 rigid, then bias 0.5 soft); asserts relative-pose
|
||
rigidity under torque/force (rot < 2°, sep < 1 cm), bounded excursion + full recovery for
|
||
the soft weld. Run this FIRST whenever the addon binaries change.
|
||
- **BodySpike.cs** + `scenes/body_spike.tscn` — headless regression for the physical torso:
|
||
pair-rule proofs on Box3D (one-way floor contact; hand-config body passes through the chest
|
||
with the chest undisturbed; prop-config body transfers contact), then track (walk pattern,
|
||
error < 6 cm), yaw (90°/1 s turn: peak < 15°, settled < 4°), impact (5 kg at 3 m/s: dip
|
||
< 0.2 m, recovers < 2 cm, never tunnels, rebounds), teleport snap, self-right (< 3° after
|
||
a pitch impulse). Must print **"BODY: overall PASS"** 3× deterministic.
|
||
|
||
Research (`research/`): **`nimso-project-tx.md`** (exhaustive study of Nimso's Project TX player from 82 of his
|
||
video transcripts in `research/sources/nimso-transcripts/` — the design being recreated; read first for body work),
|
||
`vr-physics-rig-hexabody.md` (Boneworks/BIMOS rig reference + Box3D native joint mapping), and seven full sourced reports — `vr-arm-ik.md`, `vr-arm-ik-deep.md`,
|
||
`vr-grabbing.md`, `vr-two-handed-grab.md` (grab v5.0→5.2 history + the verified Box3D pin
|
||
joint internals + dated CORRECTION section), `godot-box3d-weld.md` (the addon patch: weld
|
||
mapping, param table, build recipe, risks), `vr-body-physics-hugs.md` (player-body physics +
|
||
hug prior art: Boneworks' one-powered-body lesson, active-ragdoll recipes, hug = wrap +
|
||
mutual force + reactivity; phased design), `vr-body-physics-godot.md` (verified Box3D
|
||
capabilities: no Generic6DOF, hinge VELOCITY motors work, PD-follow proven by the hands).
|
||
**READ these before re-researching those topics or changing the grab/IK/body implementations**
|
||
— they carry the reasoning behind the current designs. Stink-O's box3d-godot (custom components, 8 joint types) remains the documented
|
||
plan-B if the patched-wrapper path is ever abandoned (owner pointed at it; adopting it means
|
||
leaving the standard RigidBody3D stack).
|
||
|
||
Scene wiring (`scenes/main.tscn`): Main → WorldEnvironment, Sun, Floor, XROrigin3D (XRCamera3D,
|
||
LeftHand, RightHand, WorldPanel — builds its MenuScreen/SubViewport/MainMenu children in code), SpawnPoint, 3 TeleportMarkers, Locomotion, AnchorMoveMode,
|
||
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
|
||
|
||
- **World-space grab/turn math**: all grab deltas and two-hand turn pivots computed in WORLD space.
|
||
Tracking-space math made two-hand turns orbit the map center. Do not regress.
|
||
- **Box3D PinJoint3D quirks (grab history v5.0–v5.2)**: Box3D's PinJoint3D is a TRUE ball joint
|
||
with ANCHORS CAPTURED AT CREATION (not vanilla Godot origin-pinning). The joint node's
|
||
position is READ EVERY TICK (re-setting `GlobalPosition` each tick = hand-following pivot).
|
||
Its hidden rotational spring (hertz = bias × 30) targets IDENTITY — aligns the two bodies'
|
||
world orientations, uncapped, hits both bodies; that was the root of the v5.0–v5.2 grab
|
||
misbehavior (see the CORRECTION section of `research/vr-two-handed-grab.md`). The positional
|
||
(point-to-point) tuning (engine default 60 Hz/ζ2.0 clamped to 0.25×tick) is NOT reachable
|
||
from the wrapper. IMPULSE_CLAMP is a warned no-op.
|
||
- **Box3D WeldJoint via the local patch (grab v5.3 lives on it)**: the patch maps Godot's
|
||
ConeTwistJoint3D → `b3WeldJoint`, PRE-CORRECTING frame B with both bodies' rotations so the
|
||
weld locks the relative pose at creation. Bias semantics are INVERTED vs the pin joint:
|
||
bias 0 = MAX stiffness (rigid), bias 0.3 = 9 Hz give. Swing/twist spans > 0 and
|
||
softness/relaxation are warned no-ops (the weld has no limits). The weld is NOT a
|
||
hand-following pivot — moving the joint node after creation does nothing, and none is needed.
|
||
Only the Linux binary is patched (`bin-stock/` holds originals; `.vendor/` the source,
|
||
patch, and rebuild recipe). Pin/hinge/slider paths untouched — grab v5.2.1 remains runnable
|
||
as fallback (git history). Stink-O's box3d-godot (custom components, more joint types) is
|
||
the plan-B.
|
||
- **Collision layers**: 1 = world, 2 = UI. The menu panel body is layer 2 mask 0 (props pass through
|
||
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);
|
||
reacting to it was the original view-locked-forward bug.
|
||
- Panel parented under `XROrigin3D` (deliberate feel); grip thresholds polled as Floats (0.6),
|
||
bool actions unreliable across runtimes.
|
||
|
||
## 5. Known issues / open questions
|
||
|
||
- **Body Stage 1 chest collapse — ROOT-CAUSED + FIXED (2026-10-01), awaiting owner re-playtest.**
|
||
The first in-headset session NaN'd the chest ("head detaching from the body"). Cause was
|
||
SCRIPT MATH, not the solver: the orientation servo used `Quaternion.GetAngle()` = `2*acos(W)`
|
||
(Godot C# `Mathf.Acos` is unclamped) → NaN when rounding put W at 1.0000001, which a seated
|
||
still player reaches because `GetAxis()` (raw xyz when W > 1−1e-6) collapsed the servo gain
|
||
inside 0.16° and parked the body where a slowly drifting target crosses it. Fixed with
|
||
`BodyMath.RotationError` (atan2-based) in torso + hands, plus `BodyMath.ResetIfNonFinite`
|
||
escape hatch on every driven body. Deterministic repro + regression:
|
||
`scenes/collapse_spike.tscn` (old code 42/49, fixed 49/49). Solver stacking (chest in
|
||
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 arms — owner playtest 1 (2026-10-02)**: "works fine for the most part";
|
||
reported forearm clipping through a held anvil, chest jitter when stretching one arm,
|
||
elbows mostly pointing down / unnatural. Fixed: open-loop lean (jitter), hand-driven
|
||
elbow + height cap, held-prop exceptions. The exact screenshot clip (forearm through the
|
||
held anvil) was NOT reproduced headless across 3 anvil heights — the drawn forearm is a
|
||
straight line from the PHYSICAL elbow to the hand wrist, so any elbow below a held object's
|
||
near edge can still draw through it; re-check in-headset.
|
||
- **Body Stage 2 arms — owner playtest 2 (2026-10-02)**: jitter fixed, elbows "pretty
|
||
good". Reported: occasional 180° elbow flip → angle clamp + rate limit, elbow now from the
|
||
controller pose; forearm clipping when RESTING the forearm on the anvil with the palm behind
|
||
it → measured: collider fine, DRAWN forearm 3.9 cm in (the hand was somewhere a straight
|
||
forearm can't connect to) → hand coupling. Owner raised max_physics_steps_per_frame to
|
||
12–14: harmless but NOT a quality knob (only catch-up ticks); Box3D's solver quality is
|
||
compiled in (`SUB_STEP_COUNT` 4 in `src/spaces/box3d_space_3d.cpp`, contactHertz 30) — the
|
||
fork could expose them if contact stiffness ever matters.
|
||
- **Body Stage 2 (physical arms) — remaining watch-list.** 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.
|
||
- **Arm length was too short** (owner, playtest) — the placeholder rig derives arm length from the head height at
|
||
first frame (×0.46, clamped 0.50–0.72 m). Fix with the ragdoll rebuild: saved calibration profile (stand straight +
|
||
"arm circles" sphere fit), see `research/vr-avatar-arm-length.md`.
|
||
- **Chest plows table props** when seated at the table (props thrown up to ~3.8 m/s by the
|
||
2400 N chest). Honest physics; tune by feel.
|
||
- **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
|
||
thumbstick movement if the body-physics track lands (Nimso-style) — see next-steps item
|
||
about the locomotion decision; don't over-invest in anchor-loco until that's decided.
|
||
- **Teleport + lasers are BROKEN** — owner report; both were experiments anyway and are
|
||
NOT current issues. Locomotion.cs's marker/laser machinery may be resurrected or deleted
|
||
when the locomotion direction is decided. The menu's laser cursor itself lives in MenuScreen
|
||
(analytic, no raycast); only the drawn BEAM is in `Locomotion.UpdatePointer` — move the beam
|
||
drawing along if that file is deleted.
|
||
- **In-VR menu (MenuScreen/MainMenu, 2026-10-02) — awaiting owner playtest.** Things to check
|
||
in-headset: text sharpness/shimmer at normal distance (no mipmaps — if it shimmers when far,
|
||
lower the resolution or try `SubViewportCompositionLayer`; if blurry up close, raise
|
||
`SizePixels`), touch press depth/feel (`ContactDepth`/`ReleaseDepth`), accidental presses when
|
||
reaching through the panel, laser/touch hand-over near the panel, thumbstick scroll direction,
|
||
the stylus tip vs the drawn cursor dot alignment, and whether 0.3 m summon distance is in reach.
|
||
- **Locomotion direction undecided** — owner's lean: full physics thumbstick movement if
|
||
the body track goes the Nimso way (stick pushes the physics body; sway/jog detection
|
||
class of solutions). Anchor-grab locomotion was a scene-viewer-era design; the fix-or-
|
||
replace decision rides on the body-physics track outcome.
|
||
- **Project direction note (owner, 2026-10-01)** — initially a scene viewer; messing with
|
||
physics made gameplay-first a live possibility (scene viewer second). NOT decided —
|
||
hinges on whether the body-physics track works painlessly. Affects prioritization only;
|
||
nothing to change in code/docs until decided.
|
||
|
||
- **Grab v5.3.1 — VALIDATED in-headset (owner: "it just works")** — the grab saga is closed.
|
||
Solver-owned rigid weld via the locally patched addon (ConeTwistJoint3D → b3WeldJoint),
|
||
rigid two-handed, takeover automatic. Headless: grab/weld/physics spikes all PASS 3×.
|
||
Known honest physics, not bugs: ~1.2° elastic sag on off-center grabs under gravity
|
||
(lever-dependent, mass-independent), brief ~3° ring when one hand of a two-handed hold
|
||
releases, and a heavy prop genuinely strains/drags the 0.4 kg hand (that IS the weight
|
||
feel). Angular hold strength on heavy props one-handed = Box3D's solver stiffness floor
|
||
(0.25×tick clamp ≈ 22.5 Hz at 90 Hz) — the documented next lever is raising
|
||
`Engine.PhysicsTicksPerSecond` above the refresh rate (global experiment, only with
|
||
owner go-ahead).
|
||
- **Hand reaction / weight-through-hand** — restored for real in v5.3: the weld transmits
|
||
prop weight/forces through the solver into the 0.4 kg hand's 90 N chase budget. Validate
|
||
the heft in-headset.
|
||
- Palm offset signs (left +, right −) are taste choices — flip if grabs feel mirrored.
|
||
- Near-extension arm shake should be fixed by soft IK (`SoftIKFactor` 0.98) — unvalidated
|
||
in-headset; lower toward 0.95 if shake persists.
|
||
- One-frame arm jumps on hand-body snap/teleport (stuck snap, SnapDistance, teleport).
|
||
- IK research applied except anything marked open in `research/vr-arm-ik*.md`.
|
||
- Hand tracking spike not started (`XRHandTracker` through WiVRN, see `openxr_hand_tracking.rst`).
|
||
- Head cube front face can peek at extreme down-angles (bump `HeadBackOffset` if it bothers).
|
||
- Real character model: import pipeline owned elsewhere (consumes Skeleton3D + AnimationPlayer).
|
||
Migration path for IK: `SkeletonIK3D` deprecated in 4.7 → custom `SkeletonModifier3D`
|
||
(`_process_modification_with_delta`) with our own analytic 2-bone solve (no built-in supports an
|
||
elbow pole) + `LookAtModifier3D` for head→neck look distribution (head authoritative, neck
|
||
cosmetic, stacked above head's). HL:Alyx shipped hands-only deliberately (arms mislead without
|
||
proprioception) — keep arms cheap and honest.
|
||
|
||
## 6. Tuning knobs
|
||
|
||
| Constant | File | Value | Controls |
|
||
|---|---|---|---|
|
||
| `HandMaxForce` | PhysicsHands.cs | 90 N | hand chase force; also used by GrabSpike stand-in |
|
||
| `HandMass` | PhysicsHands.cs | 0.4 kg | hand body mass |
|
||
| `HandMaxSpin` | PhysicsHands.cs | 120 rad/s | hand rotation-lock clamp |
|
||
| `StuckDistance` / `StuckTime` | PhysicsHands.cs | 0.2 m / 1.0 s | stuck snap (logged) — SUSPENDED while that hand grips |
|
||
| `SnapDistance` | PhysicsHands.cs | 2 m | silent teleport snap of hand body — SUSPENDED while that hand grips |
|
||
| `GhostLagDistance` | PhysicsHands.cs | 0.1 m | when the hand ghost shows |
|
||
| `RigidWeldBias` | PropGrabber.cs | 0 | weld bias for BOTH hands (v5.3.1): 0 = MAX stiffness (rigid) — INVERTED vs pin-joint bias semantics; raise only as a fallback if two-handed wrist-conflict jitters |
|
||
| `GripRipDistance` | PropGrabber.cs | 0.8 m | instant release, no timer (checked before the break timer) |
|
||
| `BreakDistance` / `BreakTime` / `BreakDistanceBoth` | PropGrabber.cs | 0.25 m / 0.25 s / 0.4 m | grip break: sustained anchor separation (single hand) / both-hands threshold |
|
||
| `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` | PhysicsArms.cs | 0.98 | shoulder→hand distance clamp before IK solve (lower toward 0.95 if shake persists) |
|
||
| `PoleHandWeight` / `PoleSmoothRate` | PhysicsArms.cs | 0.85 / 12 | share of the hand-suggested elbow angle (from the controller's forward axis); exp smoothing of the angle |
|
||
| `PoleMaxAngle` / `PoleMaxSpeed` | PhysicsArms.cs | 110° / 6 rad/s | elbow angle clamp around the down/back/out reference + rate limit (anti-flip) |
|
||
| `HandArmSlack` | PhysicsArms.cs | 0.03 m | how far a hand may lead its forearm's end while the arm is in contact |
|
||
| `ElbowRiseAllowance` | PhysicsArms.cs | 0.05 m | elbow capped at max(shoulder, wrist) height + this (no chicken wings) |
|
||
| `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` | PhysicsArms.cs | clamp(0.46×headHeight, 0.5, 0.72) | total arm length, 50/50 split |
|
||
| `LeanReachFactor` / `LeanSmoothRate` | PhysicsArms.cs | 0.95 / 6 per s | torso lean starts past 95% reach (open-loop from the nominal chest pose), exp-smoothed ≈170 ms |
|
||
| `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 |
|
||
| `LaserStart` | Locomotion.cs | 0.12 m | beam starts at the stylus tip; beam length = hit distance − this |
|
||
| `DirectGrabRadius` | WorldPanel.cs | 0.45 m | direct grip-drag range + `IsBlocking` range |
|
||
| `SummonDistance` / `HeadRecallDistance` / `HeadRecallDrop` | WorldPanel.cs | 0.3 / 0.45 / 0.12 m | menu placement on summon (ahead of hand) / on Return to spawn (ahead of + below head) |
|
||
| `SizeMeters` / `SizePixels` | MenuScreen.cs | 0.36×0.24 m / 960×640 | menu panel size / SubViewport resolution (~2.7 px/mm) |
|
||
| `TipLength` | MenuScreen.cs | 0.12 m | stylus tip ahead of the controller = touch cursor |
|
||
| `HoverDepth` / `PushThroughDepth` / `TouchEdgeMargin` | MenuScreen.cs | 0.06 / 0.08 / 0.015 m | touch-cursor zone in front of / behind / around the panel |
|
||
| `ContactDepth` / `ReleaseDepth` | MenuScreen.cs | 0.006 / 0.018 m | touch press / release hysteresis |
|
||
| `ScrollDeadzone` / `ScrollNotchesPerSecond` | MenuScreen.cs | 0.3 / 14 | thumbstick → wheel scrolling |
|
||
| `FontSize` / `ButtonHeight` | MainMenu.cs | 26 px / 64 px | menu text and button size |
|
||
| `MaximumRefreshRate` | Main.cs | 90 | physics tick = chosen refresh rate |
|
||
|
||
## 7. Next steps (in order)
|
||
|
||
1. **Owner re-playtest of the body (chest fix + Stage 2 arms)** — the Stage-1 collapse is
|
||
fixed (`collapse_spike` 49/49); feel is unvalidated in-headset. Read the log after.
|
||
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
|
||
movement if the body track lands. Fix-or-replace rides on item 1's outcome; teleport/
|
||
laser machinery gets resurrected-or-deleted with it (the menu's laser cursor is
|
||
self-contained in MenuScreen; only its drawn beam lives in `Locomotion.UpdatePointer`).
|
||
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. **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).
|
||
7. **EMBODYING** — view-follow + avatar remapping (chest calibration transform); experiments only
|
||
after head/neck look override exists. Theory until proven.
|
||
8. **UI milestone** — the ONE SubViewport screen now exists (in-VR menu, `MenuScreen`); remaining:
|
||
owner playtest of it, deliberate full action-map pass, fingertip (hand tracking) feeding the same
|
||
`MenuScreen.PointerInput` as the stylus, `SubViewportCompositionLayer` only if sharpness demands.
|
||
|
||
## 8. Working agreements
|
||
|
||
- Read `AGENTS.md` fully before working; it overrides this file where they differ.
|
||
- Agent-orchestration workflow: the orchestrator delegates to subagents and reviews their output.
|
||
- **Escalation channel**: the owner has a smart external agent for hard problems/number
|
||
crunching — the orchestrator says so when stuck or when a problem exceeds its own
|
||
capability, and the owner forwards. It starts with ZERO project context, so problem
|
||
statements must be fully self-contained (constants, constraints, desired output; no
|
||
repo references). Likely first candidates: per-joint PD gain derivation for the
|
||
hierarchical muscle budgets; Box3D stiffness-clamp vs physics-tick-rate math.
|
||
- The USER playtests in VR and reports feel — nothing "works" until the owner confirms in-headset.
|
||
- Engine logs (`~/.local/share/godot/app_userdata/VR Messs/logs/godot.log`) are the diagnosis
|
||
source after playtests — read them instead of guessing.
|
||
- Spikes are cheap and throwaway; a failed experiment is progress, not rework.
|
||
- No GDScript-only addon dependencies (Godot XR Tools is reference-only); C# everywhere.
|