In-VR menu: replace chunky prop-knob panel with Godot Control UI in a SubViewport

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>
This commit is contained in:
FoxyzandClaude Opus 5.5 committed 2026-10-02 05:49:13 +03:00
1 parent 3a990c6dbe
commit 661fe1089a
11 files changed
+1281 -316

No files matched your search

+40 -26
View File
@@ -147,32 +147,45 @@ device only; the editor has an XR simulator for dev. Trackers/action map: see `x
(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 —
- **UI direction — physical prop controls first, ONE dense screen**: buttons, knobs, sliders
as real 3D objects where they live in the world (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). At most ONE SubViewport/composition-layer screen for dense
content — **that screen is now the in-VR menu (`WorldPanel`, below)**; everything else is
props. World-space text is native and cheap: `Label3D` (SDF, crisp at distance) and
`TextMesh` (extruded) — no SubViewport required.
- **`WorldPanel.cs` = the in-VR menu (2026-10-02, replaced the chunky prop-knob panel)**:
real Godot Control UI (`MainMenu.cs`: tabs Player / World / System, normal-sized buttons,
slider, scrollable log, FPS) rendered by `MenuScreen.cs` into a 960×640 SubViewport shown on
a thin 0.36×0.24 m panel (~2.7 px/mm ≈ 1:1 with headset pixels at 45 cm — viewport textures
have no mipmaps, so don't oversample). 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
node paths. Appears 0.3 m in front of the summoning hand facing the head (full 3D facing —
works from lying poses), draggable by grip directly or via laser at fixed ray distance
(continuously re-orients toward the head); "Return to spawn" recalls it in front of the head.
**Input = the touch-first hybrid cursor, injected as mouse events**: each frame both
controller rays are projected onto the panel plane (analytic, no physics query), converted to
SubViewport pixels and pushed with `Viewport.PushInput(event, true)`
(`InputEventMouseMotion`/`InputEventMouseButton`, `NotifyMouseEntered/Exited`). The stylus
tip (12 cm ahead of the controller) within 6 cm of the surface = TOUCH cursor: crossing
0.6 cm presses, backing out past 1.8 cm releases (hysteresis; entering from the side/behind
never presses) — buttons fire on release like a real tap, sliders drag while touching.
Farther away = LASER cursor, trigger clicks/drags (edge-triggered: a trigger already held
never clicks). Thumbstick Y while hovering = mouse wheel (scroll containers). One Godot mouse
per viewport → one owning hand (press holder ≻ touching hand ≻ laser). Cursor dot per hand
(shrinks as the tip nears = proximity cue), haptic tick on press, synthesized click
(code-generated `AudioStreamWav`, no asset). Menu actions run DEFERRED (never from inside
the injected event). "Recalibrate body" calls `Recalibrate()` on the first node in group
`"calibration"` (disabled when absent — the menu never creates it). 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).
owned by `Locomotion.UpdatePointer`: for the menu it asks `WorldPanel.TryGetLaserDistance`
(beam only while that hand is in laser mode, beyond `LaserMinDistance` 0.35 m); markers keep
the raycast path. Regression: `scenes/menu_spike.tscn` "MENU: overall PASS". 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.
@@ -189,8 +202,9 @@ device only; the editor has an XR simulator for dev. Trackers/action map: see `x
movement weird, teleport + lasers broken (owner report 2026-10-01; experiments, not
current issues). Fix-or-replace deferred — owner's candidate replacement is physics
thumbstick movement if the body-physics track lands.
4. ~~UI: first prop panel~~ — **done** (`WorldPanel.cs`); SubViewport/composition-layer screen
deferred until dense content (file browser) is actually needed.
4. ~~UI: first prop panel~~ — **done** (`WorldPanel.cs`), then REPLACED (2026-10-02, owner:
knobs "incredibly chonky") by the SubViewport menu screen (`MenuScreen` + `MainMenu`) —
the one allowed dense screen. Awaiting owner playtest.
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).**
@@ -223,8 +237,8 @@ device only; the editor has an XR simulator for dev. Trackers/action map: see `x
(Nimso/VRChat precedent); props/floor/table mask 1|3|4|5, hands 4/1|4, chest 3/1|3,
arm segments 5/1; panel stays 2/0 (props pass
through even when shown); layers live on
CollisionObject3D, NOT CollisionShape3D. Panel = prop-UI prototype (ax_button summon,
contact-press knobs, Reset). Laser/distant grab REMOVED (user never knew it existed);
CollisionObject3D, NOT CollisionShape3D. Panel = the in-VR menu (ax_button summon,
touch/laser Control UI, Reset props). Laser/distant grab REMOVED (user never knew it existed);
grab = palm-proximity only, per-hand palm-face offset (left +X, right −X — taste).
PinJoint quirks (anchors captured at creation; joint node position READ every tick;
break measures the prop-local anchor vs the palm point) apply to the v5.0–5.2 PIN path