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:
1 parent
3a990c6dbe
commit
661fe1089a
11 files changed
+1281
-316
No files matched your search
+84
-25
@@ -38,8 +38,11 @@ TERMINOLOGY (owner-corrected — use precisely):
|
||||
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).
|
||||
All seven PASS 3× as of this handoff. Headless asset import:
|
||||
`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**.
|
||||
@@ -64,8 +67,11 @@ TERMINOLOGY (owner-corrected — use precisely):
|
||||
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). Also builds the stylus pointer tip per controller. `ReturnToSpawn()`
|
||||
is public (panel Spawn knob calls it).
|
||||
`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
|
||||
@@ -115,17 +121,55 @@ TERMINOLOGY (owner-corrected — use precisely):
|
||||
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()` = Reset knob.
|
||||
- **WorldPanel.cs** — prop-UI panel, 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). Knobs: Spawn (return to spawn + recall panel in front of head), Ping, Reset
|
||||
(PropShop.Respawn), Close. Panel/knob bodies: **CollisionLayer 2, CollisionMask 0** (props pass
|
||||
through); all CollisionShapes disabled while hidden (`SetShapesEnabled`, deferred). Pointer tip is
|
||||
a 12 cm stylus cone; knobs hover-highlight and PRESS ON CONTACT (tip within 4.5 cm, 400 ms
|
||||
cooldown) — physical buttons; trigger still presses laser-hovered knobs. Click = code-generated
|
||||
`AudioStreamWav` PCM (no asset). Draggable direct or via laser at fixed ray distance, re-orients
|
||||
toward head while gripped. `IsBlocking(controller)` (within 0.45 m) makes Locomotion defer
|
||||
grab/teleport; `IsPanelCollider(node)` identifies interactive colliders for lasers.
|
||||
→ 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
|
||||
@@ -227,7 +271,7 @@ plan-B if the patched-wrapper path is ever abandoned (owner pointed at it; adopt
|
||||
leaving the standard RigidBody3D stack).
|
||||
|
||||
Scene wiring (`scenes/main.tscn`): Main → WorldEnvironment, Sun, Floor, XROrigin3D (XRCamera3D,
|
||||
LeftHand, RightHand, WorldPanel), SpawnPoint, 3 TeleportMarkers, Locomotion, AnchorMoveMode,
|
||||
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).
|
||||
@@ -254,7 +298,7 @@ Floor StaticBody mask 1|3|4|5 (collision_mask 29).
|
||||
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. Panel/knob bodies are layer 2 mask 0 (props pass through
|
||||
- **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
|
||||
@@ -318,8 +362,15 @@ Floor StaticBody mask 1|3|4|5 (collision_mask 29).
|
||||
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; WorldPanel's laser-hover knobs still reference
|
||||
the laser system, so a deletion needs that decoupling first.
|
||||
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-
|
||||
@@ -396,8 +447,15 @@ Floor StaticBody mask 1|3|4|5 (collision_mask 29).
|
||||
| `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 |
|
||||
| `PressContactDistance` / `PressCooldownMs` | WorldPanel.cs | 0.045 m / 400 ms | knob press on contact |
|
||||
| `DirectGrabRadius` / `TipHoverRadius` / `TipLength` | WorldPanel.cs | 0.45 / 0.07 / 0.12 m | panel grab / tip hover / stylus reach |
|
||||
| `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)
|
||||
@@ -408,8 +466,8 @@ Floor StaticBody mask 1|3|4|5 (collision_mask 29).
|
||||
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 (WorldPanel's laser-hover knobs
|
||||
must decouple first if deleted).
|
||||
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
|
||||
@@ -420,8 +478,9 @@ Floor StaticBody mask 1|3|4|5 (collision_mask 29).
|
||||
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** — deliberate full action-map pass + SubViewport/composition-layer decision for
|
||||
dense screens (at most ONE such screen; everything else stays props).
|
||||
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
|
||||
|
||||
|
||||
Reference in new issue
Block a user