Files
VR-Mess/AGENTS.md
T

237 lines
18 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# VR Messs
VR anime-character "scene viewer" (Koikatsu-VR-class, but with real physics hands).
Not a game: no gameplay loops, quests, or progression. Gameplay features are the
owner's domain; explore, interact, touch, pose — that's the product.
TERMINOLOGY (owner-corrected, use precisely): **"getting a body"** = the physical rig
(torso/arms/hands — the interaction body). **"embodying"** = literally viewing from the
perspective of a character in an animation (the view-follow + avatar system). They are
separate systems; never call the rig milestone "embodying".
This repo is the experimentation lab (hence the name). The designs below are working hypotheses
to be validated with cheap, throwaway spikes — a failed experiment is progress, not rework. The
embodiment/view system in particular is theory until proven.
## Toolchain pins (do not drift)
- **Godot 4.7.2** (current stable, Forward Plus renderer), **C# / .NET 8**. Desktop-only (PCVR).
- **Physics: Box3D** via the `godot-box3d` GDExtension (server-replacement package by
bearlikelion) — **installed, v0.2.6**, setting value `physics/3d/physics_engine="Box3D
Physics"`. Validation spike (`scenes/physics_spike.tscn`, run headless) PASSES: drop+contact
and velocity-chase-into-wall both work from C#. WARNING: exactly ONE `.gdextension` file may
exist — a stray duplicate (e.g. at `addons/` root) double-loads the library and breaks the
server with "required virtual method" errors. Jolt (built-in) is the fallback: reverting is
the one project-setting line, and all gameplay code must stay standard-Godot-physics-node
based so that flip is lossless.
- Character import (Koikatsu etc.) is a separate pipeline owned by someone else. We consume a
character scene that provides: `Skeleton3D` with humanoid bone mapping + `AnimationPlayer`.
Don't touch import code; if the interface is insufficient, say so instead of hacking around it.
- Dev runtime stack: **WiVRN + WayVR** on Linux. Headset and runtime both support hand tracking —
treat hand tracking as available. View-follow milestone verified working (OpenXR + WiVRN).
- Graphics are deliberately **stylized** — fits the anime look and buys performance headroom.
Renderer choice (currently Forward+; docs recommend Mobile for desktop VR) is deferred until
profiling justifies a change.
## Documentation rules (hard rules — this project's #1 failure mode)
- Local pinned docs: `/home/foxyz/Projects/Games/.shared/godot-docs-4.7` — full manual +
class reference in `.rst` (source of docs.godotengine.org, branch `4.7`). Grep it first.
- For ANY engine API detail, class name, property, or signal: **trust only these 4.7 docs or a
live spike.** Training-memory APIs and older-version knowledge are banned — verify or don't write it.
- Web fallback: `https://docs.godotengine.org/en/4.7/...` only — the version goes IN the URL.
Never `/latest/`, never another version path.
- Code examples: rst sources contain `code-tab::` blocks with BOTH gdscript and csharp — grep for
`code-tab:: csharp`. Class reference lives in `classes/class_*.rst`.
- XR tutorials: `tutorials/xr/` (setting_up_xr, xr_action_map, xr_room_scale,
openxr_body_tracking, basic_xr_locomotion). Physics: `tutorials/physics/` (rigid_body,
ragdoll_system, using_jolt_physics, physics_introduction).
## Godot/C# traps
- Addons that are GDScript-only can't be referenced from C# — **Godot XR Tools is GDScript-only**:
treat it as a reference implementation to port ideas from, never a dependency.
- Default OpenXR action map names (verified from engine source, 4.7): `trigger`/`grip` (Float),
`ax_button` (A/X), `by_button` (B/Y), `menu_button` (menu/system — often eaten by the runtime,
e.g. WiVRN screenshot+anchor-hold), `primary` (thumbstick). `select_button` is Pico-only.
Poses: `default_pose`, `aim_pose`, `grip_pose`, `palm_pose`.
- C# in Godot covers desktop + mobile only (fine — PCVR target).
- Signals: C# `+=`/`-=` events; check the class ref for exact C# signatures.
- The C# binding can diverge from engine class names: `AudioStreamWAV` is `AudioStreamWav`
in C#. When docs and compiler disagree, grep the shipped `GodotSharp.dll` (strings) —
it's at `/usr/lib/godot-mono/GodotSharp/Api/Debug/GodotSharp.dll` on this machine.
## Architecture (working hypotheses — test cheap before committing)
Four layers, bottom-up, applied per frame in this order:
1. **Animation** — `AnimationPlayer` drives the skeleton (any pose, any animation).
2. **Tracked override / view embodiment** — several input modes, all blended over whatever
animation is playing:
- **View-follow (core feature, Koikatsu-VR-style)**: the view follows the character's head
frame including tilt, so weird animations stay correct. The player's HMD look is remapped
through a one-time calibration transform captured relative to the character's chest
("chest calibration", e.g. once while lying flat) and drives a look override on the
head+neck (possibly slight chest) bones. Skeleton-space only — the physics layer stays
separate from this. Pivot behavior (rotation-only snap vs positional head offset) and the
calibration UX are open experiments.
- **Free-view**: ordinary playspace (`XROrigin3D`); "lean back mode" = origin offset +
recline calibration, zero physics involved.
- **Single-controller mode**: one controller as master tracker — hand override, or strapped
to the hip driving the animation root/hip (hip-tracker mode).
- Full-body trackers are a far-future extension of this same layer (more override targets),
not a separate system.
3. **Physics embodiment** — a small set of rigid bodies (hand L/R, head, torso/hip capsule) that
CHASE layer-1/2 targets: velocity-clamped steering (`v = clamp((target - pos) / dt)`), CCD on,
no joints between them. Unobstructed → effectively 1:1 tracking; obstructed → pushes with
tunable strength. The chase deviation is the feedback signal: it clamps the visual hand offset,
pushes the character pose via spring-damped offsets ("pushable from animation"), and fires
logical touch-reaction events. Boneworks calls the general technique "heuristic force limit".
4. **Secondary motion (hair/skirt/cloth) — custom spring-bone solver.** Koikatsu DynamicBones and
VRM springBone are poorly interactive and/or expensive; VRChat PhysBones are the gold standard
but proprietary. So: original PhysBones-*style* math (spring-damper chains on the skeleton,
animation-space) with analytic sphere/capsule colliders (body, hands, floor). The physics
engine contributes only its QUERY API (throttled ray/shape casts) so bones respect scene
geometry — **never rigid-body chains for jiggle**. Performance is a first-class goal of this
project: chain budgets, screen-size/distance LOD, simplified colliders. If the per-bone C#
loop profiles hot, the hot loop moves to a GDExtension — profile first, don't pre-optimize.
Do NOT build anything that needs 6DOF/cone-twist joints between tracked bodies (the wrapper lacks
them; velocity steering replaces them). Scene objects stay standard nodes (`RigidBody3D`,
`StaticBody3D`, `CharacterBody3D`) so arbitrary imported scene content works and the Jolt fallback
stays real.
## XR
OpenXR is built into Godot 4.7 — no plugin. Node set: `XROrigin3D` / `XRCamera3D` /
`XRController3D` (+ `XRBodyTracker`/`XRHandTracker` for body/hand tracking data). OpenXR runs on
device only; the editor has an XR simulator for dev. Trackers/action map: see `xr_action_map.rst`.
## Interaction & UI (design directions — experiments welcome)
- **Touch-first hybrid cursor**: interactive surfaces prefer *touch*. A pointer tip (controller
tip or index fingertip) acts as a cursor when in proximity of a surface — push, hold, slide,
physical press feel. Out of proximity, fall back to a laser ray for distant precision. Lasers
ruin immersion and are hard to aim at distance; they are the fallback, not the default.
- **Hand tracking**: cheap spike early, full support later. Interaction logic must not assume
controllers — controller tip and fingertip feed the same cursor concept.
- **Locomotion** (implemented, play-feel tuning ongoing): spawn re-anchoring
happens ONCE on first focus gained (HMD xz at `SpawnPoint` + spawn yaw; real head height and
pitch kept) — physical room position never matters. `pose_recentered` is deliberately NOT a
placement trigger AND its log is throttled to one line: measured on WiVRN it fires
continuously at refresh rate (~90 Hz, 22k lines/session) — reacting to it was the original
view-locked-forward bug. `Locomotion.cs` is the manager (spawn placement,
teleport markers, lasers, fade); **grab movement lives in `AnchorMoveMode.cs` as its own
movement mode** — scene-viewer-first, other modes (walk, tp-only variants) are future nodes
beside it. Teleport markers (`scenes/teleport_marker.tscn`):
aim + trigger, with fade; near markers (within `MarkerNearDistance` 1.2 m) fully hide
(disc, beam, label) AND are not teleport-targetable. Grab semantics are
ANCHOR-based: hands are anchors to push/pull from — one-hand grip
moves the player OPPOSITE to hand motion in full 3D, both grips = anchor translation +
IN-PLACE yaw turn around the head. All grab deltas and turn pivots are computed in
WORLD space (tracking-space math made turns orbit the map center — do not regress).
Spawn/teleport placement pins arrival height (head height preserved). Locomotion drives
`XROrigin3D` directly; physics reconciliation comes with the embodiment layer. Grip/trigger
are polled Floats with thresholds per docs
(bool actions unreliable across runtimes). Engine logs land in
`~/.local/share/godot/app_userdata/VR Messs/logs/godot.log` — agents: read this after user
playtests instead of guessing.
- **UI direction — physical prop controls first**: buttons, knobs, sliders as real 3D objects
(touch interaction, later Box3D for feel). Godot has NO native world-space widget system —
`Control`s are screen-space; world UI is always Control-rendered-into-3D via `SubViewport`
(optionally `SubViewportCompositionLayer` for XR-native sharpness/perf). Use at most one
SubViewport/composition-layer screen for dense content (file browser, settings) if needed;
everything else is props. World-space text is native and cheap: `Label3D` (SDF, crisp at
distance) and `TextMesh` (extruded) — no SubViewport required.
- **`WorldPanel.cs` is the first prop-UI prototype**: summoned/dismissed via `ax_button` (A/X —
menu_button belongs to the runtime). It is PARENTED UNDER `XROrigin3D` — Koikatsu-VR-style
"fixed in the player's room": IRL movement doesn't move it relative to you, game locomotion
carries it. If that feel is wrong, reverting = reparenting to `Main` and fixing the `_Ready`
node paths. Floats in front of the summoning hand facing the player (full 3D facing — works
from lying poses), draggable directly or via laser at fixed ray distance while gripped
(continuously re-orients toward the head). The Spawn
knob also recalls the panel in front of the head. The pointer
tip is a protruding stylus cone (apex ~12 cm ahead of the controller) and is THE cursor:
knobs hover-highlight and PRESS ON CONTACT (tip within 4.5 cm, with cooldown) — physical
buttons, no trigger needed; trigger still presses laser-hovered knobs (knobs are
laser-targetable). Presses play a synthesized click (code-generated `AudioStreamWav` PCM
— no asset). Spawn/Ping/Close knobs. Locomotion defers
grab/teleport to the panel when a hand is near it (`WorldPanel.IsBlocking`). Laser lines are
owned by `Locomotion.UpdatePointer`: shown only when aiming at something interactive (marker
or panel) AND beyond `LaserMinDistance` (0.35 m) — near targets are touched via the pointer
tip instead, which is the touch-first hybrid cursor in miniature. Grab deltas and turn
pivots are computed in WORLD space (tracking-space math made two-hand turns orbit the map
center — do not regress this).
- **Control scheme is deliberate**: decide what lives on controllers vs. what lives in menus
before stacking bindings — controller button budget is scarce, menus carry the rest.
## Order of work
1. ~~Get VR working at all~~ — **done**: OpenXR + WiVRN verified in-headset, flat preview
fallback works without a headset.
2. ~~Input foundation (partial)~~ — grip/trigger Float polling + `ax_button` summon verified
from engine source; the deliberate full action-map pass happens with the UI milestone.
**Hand tracking spike still TODO** (`XRHandTracker` through WiVRN, see
`openxr_hand_tracking.rst`).
3. ~~Locomotion: anchor-grab mode~~ — **done** (`AnchorMoveMode.cs` + teleport markers +
spawn anchoring); other movement modes are future nodes beside it.
4. ~~UI: first prop panel~~ — **done** (`WorldPanel.cs`); SubViewport/composition-layer screen
deferred until dense content (file browser) is actually needed.
5. ~~Box3D validation spike~~ — **done, PASS** (drop+contact, velocity-chase vs wall, headless
via `scenes/physics_spike.tscn`).
6. **Physics hands & props sandbox — grab v5.3 = SOLVER-OWNED RIGID WELD (patched addon).**
Research first (see `research/vr-grabbing.md`, `vr-two-handed-grab.md` + its dated
CORRECTION, `godot-box3d-weld.md` — sourced taxonomy, Box3D internals, the whole v5.0→5.2
history). The owner rejected every point-joint variant as "hanging on a winch" (ball
orbits the palm, anvil surface drifts, palms clip) — a grab must lock the object's POSE
relative to the hand, not one point + a rotator. **The addon is now locally patched**
(source + recipe in `.vendor/`, originals in `addons/godot-box3d/bin-stock/`, Linux binary
only): Godot's ConeTwistJoint3D maps to Box3D's `b3WeldJoint`, pre-corrected so the weld
locks the relative pose AT CREATION (no identity-target snap; bias semantics INVERTED vs
pin — 0 = max stiffness). `PropGrabber.cs`: grab creates a ConeTwistJoint3D weld at the
palm point (`SwingSpan/TwistSpan 0`, `ExcludeNodesFromCollision`, configured BEFORE
AddChild — mandatory ordering). NO script forces/torques/damp anywhere and NO per-tick
joint re-positioning (the weld is not a hand-following pivot) — the solver owns everything
and weight flows into the hand's 90 N budget honestly. BOTH hands weld RIGID (bias 0 —
v5.3.1: the owner rejected the soft secondary; hand₁+prop+hand₂ welded = one composite
body, conflicting controller demands resolve through hand lag, not object stretch);
takeover is automatic — the survivor's weld persists, and the brief ~3° ring when one
hand releases is stored weld stress discharging (honest physics). Break: stretch-based (rip 0.8 m / 0.25 m for 0.25 s / both-hands
0.4 m) + "grip dragged off" when the hand body lags its controller > `PhysicsHands.SnapDistance`.
Headless regressions: `scenes/weld_spike.tscn` (addon patch), `scenes/physics_spike.tscn`,
`scenes/grab_spike.tscn` (rigid pose under drag + fast spin — the winch regression; don't
tighten rotation limits below ~2°, that's the gravity floor for off-center grabs).
`PhysicsHands.cs`: physical palm bodies (0.4 kg, 90 N chase, rotation lock, stuck-safety
snap — SUSPENDED for a hand while it grips, HAND ghosts > 10 cm lag). Collision layers:
1 = world, 2 = UI (panel mask 0 — props pass through even when shown); layers live on
CollisionObject3D, NOT CollisionShape3D. Panel = prop-UI prototype (ax_button summon,
contact-press knobs, Reset). Research reports live in `research/` (arm IK, IK
stability/deep, grabbing, two-handed grab/Box3D internals, addon weld patch) — READ before
re-researching or changing grab/IK. Stink-O's box3d-godot (custom components) = documented
plan-B. Laser/distant grab REMOVED (user never knew it existed); grab = palm-proximity only,
per-hand palm-face offset (left +X, right −X — taste). Box3D PinJoint quirks: anchors are
CAPTURED AT CREATION; the joint node's position is READ EVERY TICK (keep it on the palm =
hand-following pivot); break must measure the prop-local anchor vs the palm point. Headless
regression tests: `scenes/physics_spike.tscn` AND `scenes/grab_spike.tscn` — both must print
overall PASS. `PhysicsHands.cs`: physical palm bodies (0.4 kg, 90 N chase, rotation lock,
stuck-safety snap, HAND ghosts > 10 cm lag). Collision layers: 1 = world, 2 = UI (panel mask
0 — props pass through even when shown); layers live on CollisionObject3D, NOT
CollisionShape3D. Panel = prop-UI prototype (ax_button summon, contact-press knobs, Reset).
Research reports live in `research/` (arm IK, IK stability/deep, grabbing,
two-handed grab/Box3D joint internals) — READ before re-researching or changing grab/IK.
7. **Getting a body (rig) — placeholder v1.2 (cube character, agent-built).** `BodyRig.cs`:
all-BOX primitives (owner preference) — head 0.15³ behind the eyes, slim torso retracted,
wrist-anchored 2-bone arms. **Soft IK applied**: shoulder→hand distance clamped to
reach×0.98 before the solve (the straight-arm singularity was the overextension shake; if it
persists, lower SoftIKFactor toward 0.95). Elbow pole: torso frame, shoulder→hand axis
PROJECTED OUT, hand weight 0.25, exp-smoothed 10/s (twitchy wrists fixed). **Torso yaw
spring**: HMD yaw base blended toward hands-derived yaw (weight 0.6 × forwardness over
0.3 m), exp-smoothed 12/s, decoupled from the head — the "chest = headset + hands" ask.
Reach budget hysteresis unchanged. Full sourced findings in `research/vr-arm-ik.md` and
`research/vr-arm-ik-deep.md` (HL:Alyx went hands-only on purpose; SkeletonIK3D deprecated →
custom SkeletonModifier3D when a real skeleton arrives; LookAtModifier3D for head→neck).
Physical torso/head capsules later. EMBODYING (view-from-character-perspective) comes after
the head/neck look override exists. **HANDOFF.md at project root is the takeover document**
(constant table verified against code — code wins on drift); keep it updated when systems
change.