Install
openclaw skills install @blueboyzx/godot-vrBuild & headless-verify Godot 4.7 OpenXR/VR apps — room-scale workflow, Pimax Crystal tested in-visor, controller-binding pitfalls covered.
openclaw skills install @blueboyzx/godot-vrBuild a Godot 4 (OpenXR) VR app and verify it headlessly on a Windows host
(no editor, no headset needed during the build/verify loop). Deliver a real,
running project verified by execution — not a description. (Sibling of
blender-headless; different engine, same headless-verify discipline.)
_console exe for scriptable runs. Reuse an existing install (confirm with --version) before downloading.C:\Program Files\Pimax\, Pimax OpenXR 0.1.0). Pimax Crystal controllers work in Godot on BOTH Pimax OpenXR 0.1.0 and PimaxXR (Unofficial) 0.4.4 once the action map + tracker binding are correct (see references/xr-controller-troubleshooting.md); SteamVR unverified.C:/...) to native binaries (Godot, Blender, python) — MSYS /c/... forms work for bash builtins (cd, ls) only.project.godot: config/features=PackedStringArray("4.7","Forward Plus") (do NOT include "OpenXR" — it triggers a false-positive editor warning; runtime OpenXR is driven by [xr] openxr/enabled=true, not the feature tag), [rendering] renderer/rendering_method="forward_plus", an [xr] block with openxr/enabled=true, openxr/always_in_xr=true, shaders/enabled=true, and a 1600x900 viewport.main.gd on the root node builds: Environment (glow/bloom + fog), a directional "moon" light, OmniLight3D point lights for the glow, and the geometry (trees, props, a focal glowing centerpiece).XROrigin3D → children XRCamera3D + two XRController3D, each tracker="left_hand" / "right_hand" (the tracker names OpenXRInterface::find_tracker() registers with XRServer), pose="grip" (or "aim"), show_when_tracked=true. ⚠️ XRController3D has NO auto-detect: an empty tracker → get_tracker("") → null → the node never binds (frozen pose, hand=0, invisible hands). Set tracker explicitly to "left_hand"/"right_hand". Never pose="default" (unreliably bound → controllers silently stop tracking). See references/xr-controller-troubleshooting.md.XRController3D + a world-space ray (get_world_3d().direct_space_state.intersect_ray(PhysicsRayQueryParameters3D.create(from,to))). The GodotVR XR-Interaction plugin is unavailable (no such repo exists in the GodotVR org — its maintained grab toolkit is godot-xr-tools, but the native ray grab avoids the dependency) — do not block on it._ready, if no XR session, add a Camera3D pointed at the scene; if the run was given a marker arg (e.g. --preview), wait ~12 frames then get_viewport().get_texture().get_image().save_png(path) and get_tree().quit().XROrigin3D with its XRCamera3D + both controllers.--xr-mode off (on any machine with an OpenXR runtime installed, the runtime may init without a headset and silently blank the window) → PNG, then vision-check it (real geometry/lighting, not a blank/solid-color frame). Confirm the run exited and left no orphan godot process (see the reference's process-hygiene note).openxr/enabled=true; leave "OpenXR" out of config/features). The in-visor feel is the user's final check.--headless stalls in the main loop, AFTER _ready — scene load and every _ready run and print normally, then the engine enters the main loop, detects the HMD, and hangs to timeout (no display/form factor). So a short-timeout headless run IS a valid no-headset check that a code edit landed: timeout 20 <bin> --headless --path <proj> 2>&1 | grep -iE "Parse Error|SCRIPT ERROR|not found in base|ready" surfaces scene-load parse errors and your _ready prints (e.g. [LeftHand] ready (tracker=… pose=…)) before the stall — use it to confirm a change took effect (and it is the only reliable way to catch native-method parse errors, since --import does not parse scene scripts against the native API). Use a full run with OpenXR OFF for real 0-error / self-test verification, then re-enable OpenXR for delivery. Do not chase a ticking "headless VR" main loop — it won't run past session start without a display.--headless never renders. Any visual check must run NON-headless (marker arg) so a window opens and GPU draws; the scene saves its own PNG.references/godot-47-api.md before writing render/particles/physics/XR code; it lists the working forms and the errors they prevent.write_file refuses overwrite even after a full read. rm -f the target first, then write to the fresh path (same workaround works for re-writing scene .tscn/.gd).--import does not surface.references/xr-controller-troubleshooting.md — do not re-tune the scene or chase a headless VR run.XRController3D.tracker must be set explicitly to "left_hand"/"right_hand" (no auto-detect — empty never binds), and (2) the OpenXR action map must be trimmed to only the profiles the runtime recognizes — for Crystal keep only oculus/touch_controller, drop the other three stock profiles (generic, pico4, hand) and every grip_surface/pose (OpenXR 1.1 palm-pose) binding. One unsupported path fails the whole xrSuggestInteractionProfileBindings call (XR_ERROR_PATH_UNSUPPORTED) → no controller binds, on any runtime. No openxr/default_action_map pin needed — the engine default is already res://openxr_action_map.tres (probe-verified); just keep the edited file at that path. (Early "no hands" on PiOpenXR/SteamVR predated the binding fix; the runtime switch that once seemed to fix it was coincidental. SteamVR was never re-tested after the fix — unverified.)OpenXR not supported by this version" is a 4.7.2 editor false-positive when "OpenXR" is in config/features (the build has OpenXR; it initializes at runtime). Dismissable ("Open anyway"), but you can avoid it by leaving "OpenXR" out of config/features — runtime OpenXR is driven by [xr] openxr/enabled=true, not the feature tag. Confirm OpenXR actually loaded via the Running on OpenXR runtime: … Output line, not the feature list.timeout <N> — the scene self-quits on the marker arg, but a run that doesn't leaves a windowed Godot holding the user's machine; the orphan check after is tasklist | grep -ic godot = 0 (see references/godot-47-api.md). To kill a survivor, taskkill /PID <pid> /F — MSYS kill <pid> cannot reach a native Windows process at all, and when argument path conversion is off the double-slash form //PID is passed through literally and rejected..glb/.gltf assets are NOT re-imported by ordinary runs. The import cache (.godot/imported/*.scn) survives, and a normal --preview/headless run silently uses the STALE cached import — so after regenerating any imported asset you must run timeout N <bin> --path <proj> --headless --import and confirm the .scn mtimes moved (or that the stored source_md5 matches the source md5sum). Symptom: asset fixed and regenerated on disk, but screenshots still show the old broken geometry, and all further "fixes" debug phantom bugs (this cost a full investigation cycle in the forest project).-s standalone scripts: a script error before quit() runs the SceneTree forever — a windowed black Godot lingers on the user's machine. Standalone debug scripts must reach quit() even on failure (check results before the only quit() call, or run with --headless under timeout so a stall is bounded)._mirror_root Node3D (scale (1,-1,1), at the waterline) so the reflection is real geometry seen through the TRANSLUCENT water (roughness ~0.15, alpha ~0.5); the twin is only visible in the style that uses it (hidden under opaque water/ground otherwise). Verified in-visor (user-confirmed). If you do use a probe instead, 4.7 has three silent killers (all defaults!): blend_distance defaults to 1.0 m and max_distance to 0.0 (older 4.x used 100) — a reflective surface farther than 1 m from the probe receives nothing from it; set both explicitly (e.g. blend 30, max 10). Second: a flat water surface only mirrors content ABOVE the probe's own height (reflected rays always point upward from the probe) — a probe at head height can never show mushrooms/rocks/low crystals; put it just above the water (~0.4 m). Third: the water albedo sets contrast (metal: reflection = reflected radiance × albedo) — a bright albedo makes a featureless pale disc in a dark scene; a deep dark-teal albedo makes the reflected glow shapes pop (night-pond look). Also: a strong point light directly above a near-mirror makes a wide specular wash that buries the mirror image (lower its energy). 4.7 exposes NO cubemap API on the probe (no cubemap property, no getter) — to verify the probe actually reflects, place a bright test sphere ABOVE probe height and look for its mirrored dot on the water. And capture water from standing eye-height close-ups (add a dedicated eye-height mode, e.g. --preview-close <azimuth_deg> <distance_m> — the camera stands ~1.55 m up, a couple of metres out, looking at the water), never the high overview camera.--preview screenshot showed it (despite show_when_tracked=true). Fix: short beam (≈0.5 m, hand scale), dim (emission ≈0.2), brighten (≈2.5) only while hovering a grabbable; the grab RAY (raycast) length is separate from the visual beam length.set_meta("hides", "<tag>") at every creation site; add a --hide <tag1>,<tag2> CLI flag that frees every tagged node before the screenshot. A class is guilty only if hiding it makes the artifact disappear.n.get_meta("hides", ""); tag every instance of a logical class, including unnamed twins/duplicate meshes (the style-2 ground twin needed its own tag). GDScript working forms: var stack: Array = get_children().duplicate() (:= infers Variant from an untyped Array) and for i in kill.size(): (kill[i] as Node).free().emission_energy_multiplier × glow settings before concluding the mesh is broken.fragment() function VERTEX is view space (model/object space only in vertex()), so MODEL_MATRIX * VERTEX AND raw _fbm3(VERTEX*…) in the fragment both crawl with the head — synchronized across every object sharing the shader (4.7 has no WORLD_MATRIX). True world-space noise: wp = MODEL_MATRIX * v_local, where v_local is a varying set to VERTEX in vertex(); INV_VIEW_MATRIX * VERTEX in the fragment is ALSO world space (it is the form the forest ground shader shipped with and it is correct). Verified in-visor: this is the difference between a texture that crawls as you turn your head and one that sits still. (b) glow bleed — a bright non-emissive surface (a ground washed by point lights) exceeds glow_hdr_threshold, so its mottled pattern is additively smeared onto nearby rocks/mushrooms/trees; levers: raise glow_hdr_threshold (0.9→~1.4 keeps emissive-1.5+ objects glowing while lit non-emissive surfaces stop blooming), tighten glow_bloom (~0.35–0.4), and dim light energy until the ground sits below the threshold. Verify with close-up captures of the affected objects, not the overview camera.componentType 5123 (uint32) while the index bufferView holds 2-byte indices — do not treat this as corruption when a file looks broken on disk (Godot infers the index type from the bufferView size). When a mesh renders wrong (spikes, streaks, stray far geometry), scan triangles directly with scripts/scan_glb.py <file.glb> (flags degenerate triangles and edges longer than N× the bbox extent); a clean scan means the geometry is fine and the artifact is elsewhere (shaders, bloom, controller visuals).references/godot-47-api.md — confirmed 4.7.2 API forms (Environment glow, GPUParticles3D, RigidBody3D, raycast, XR session state, XRController3D) + the XROrigin3D scene structure + the error→fix table that actually fired during the build + the exact verification commands.references/xr-controller-troubleshooting.md — controllers don't show / no interaction in the visor: the decisive diagnostic (which of 3 failure cases), the case A two-sub-fix detail (explicit tracker + trimmed action map — the real Pimax Crystal fix, works on both Pimax runtimes), OpenXR runtime selection on Windows (registry / env override / restart / verify line), and the benign warnings to ignore.references/style-swap-system.md — runtime-swappable graphics styles on one scene (e.g. B-button cycle): the typed-entry registry + _set_style walk, the hidden-twin MeshInstance3D pattern for alternate geometry with per-object transforms, the per-style ENV table, --style N verification, and the Blender-side canonical-asset pipeline.scripts/scan_glb.py — re-runnable GLB triangle scanner (degenerate + over-long-edge flags; tolerates the Blender 5.3 uint32/uint16 index quirk). Run it before blaming imported geometry for a visual artifact.