543 lines
47 KiB
Markdown
543 lines
47 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),
|
||
`truebody_spike` "TRUEBODY: overall PASS" (no physics; plain `--headless`), `menu_spike` "MENU: overall PASS"
|
||
(in-VR menu input injection, no XR). All nine 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.
|
||
**Swipe vs tap (owner: scrollbar too small to grab, 2026-10-02)**: a touch landing on a
|
||
vertically scrollable `ScrollContainer` (found by walking up from `GuiGetHoveredControl()`; a
|
||
`Range` — slider/scrollbar — on the way keeps the immediate press) is held back. Tip travel
|
||
> `SwipeThreshold` 16 px (~6 mm) before release = swipe: `ScrollVertical` follows the tip,
|
||
release flings (velocity smoothed at 30/s, decay `FlingDecay` 5/s, stops below 30 px/s or at an
|
||
end), and NO click is sent. Released sooner = tap: motion + press + release sent together at
|
||
the contact point. While held back, mouse events carry no button mask.
|
||
**Stylus**: touch point = `TipLength` 0.11 m along the aim ray, drawn by `Locomotion.MakeTip` as
|
||
a cone from `StylusBase` 0.06 (front of the 12 cm hand box) to exactly that tip.
|
||
**Haptics** (`MenuScreen.Haptic(hand, amplitude, seconds)` → `TriggerHapticPulse`): press
|
||
(PointerPressed), hover onto a new enabled button/slider/tab bar 0.12, click release 0.25,
|
||
slider every 1/20 of range, swipe every 48 px, wheel notch.
|
||
- **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 — `Calibration.cs` (main.tscn) provides it. 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, touch swipe scrolls with no click, fast swipe flings, a jittery tap on a button inside
|
||
the log still clicks, the scrollbar itself still drags, 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.
|
||
- **Calibration.cs + BodyProfile.cs (body step 1, 2026-10-02)** — ONE-TIME T-POSE CALIBRATION (owner design:
|
||
VRChat-FBT-like, persists, forced on launch if missing). `BodyProfile` = the player's real metrics (eye height,
|
||
stature = eye/0.936, shoulder half-width = 0.1295·stature, shoulder drop below the eyes, upper arm/forearm in
|
||
the 0.186:0.146 Winter split, stored 1.5 cm SHORT so the real elbow never locks first). `Prefs` stores it in
|
||
**`prefs.json` next to the executable (editor builds incl. headless: the project folder) — gitignored,
|
||
portable (copy the file)**. `Calibration` (node in group "calibration", `Recalibrate()` for the menu) loads it;
|
||
if missing it shows a head-locked Label3D and runs `TPoseDetector`: arms level (±8 cm), at shoulder height,
|
||
symmetric about the head (±10 cm), to the sides (not forward), span 1.0–2.4 m, every point within 3 cm of where
|
||
the hold began (jitter-proof) for 1 s → measure: arm = (wrist span − shoulder width)/2 − margin. Body systems
|
||
(PhysicsTorso, PhysicsArms, BodyRig, TrueBody) WAIT for `BodyProfile.Current`; recalibration rebuilds them.
|
||
Flat preview (no headset) can't calibrate → no body there (expected). Headless spikes that inject a head use
|
||
`BodyProfile.Estimate(eye)`; CollapseSpike sets a fixed standing profile (posture never shrinks the body).
|
||
- **ArmSolver.cs** — the shared analytic 2-bone solve (moved verbatim from PhysicsArms: soft IK 0.98, pole as an
|
||
angle about the arm axis vs torso-frame down/back/out, 85% controller-forward suggestion read nearest the current
|
||
angle, ±110° clamp, 6 rad/s, elbow height cap). NEW: minimum reach |upper − fore| + 1 cm (unequal anatomical
|
||
segments made the law of cosines invalid for hands right at the shoulder).
|
||
- **TrueBody.cs (body step 1)** — `TrueBodySolver`: Nimso's TRUE BODY — the derived pose of the real body from
|
||
head + controllers + profile (trigonometry, not IK): neck pivot 9 cm below / 7 cm behind the eyes (head frame);
|
||
chest yaw = the HMD/hands yaw spring; chest pitch = crouch lean (crouch measured at the NECK PIVOT so nodding
|
||
isn't crouching; up to ~43°) + ¼ of head pitch-down; neck base along chest-up/head-up average; pelvis hangs
|
||
0.288·stature below the shoulder line through a critically damped 2.5 Hz spring (hip lag/sway, hips kick back in
|
||
a crouch); MOVING SHOULDER GIRDLE: past 98% reach the shoulder joint travels toward the hand within anatomical
|
||
limits (out +3/in −2, up +7/down −2, forward +8/back −3 cm) plus a scapulohumeral shrug up to 4 cm overhead;
|
||
arms via ArmSolver. `TrueBody` node solves every physics tick and draws a translucent cyan DEBUG SKELETON
|
||
(`TrueBody.ShowDebug`) for in-headset judgement. Step 2 (muscle ragdoll) will drive its joints toward this pose.
|
||
Spike `scenes/truebody_spike.tscn`.
|
||
- **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, swipe threshold/fling feel,
|
||
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)
|
||
|
||
0. **Owner playtest of body step 1** — T-pose calibration (forced on first launch; delete `prefs.json` to redo,
|
||
or the menu's Recalibrate), arm length should now match; judge the cyan True Body skeleton (shoulders that
|
||
shrug/reach, crouch lean, hip sway, elbows). Then body step 2 = the muscle ragdoll on the 6-DOF joints.
|
||
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.
|