Xr Development
Published by jame581 in godotprompter
What this skill does
Use when building VR/AR/XR applications — OpenXR setup, XROrigin3D, hand tracking, controllers, passthrough, and Meta Quest deployment in Godot 4.3+
Add Xr Development to your agent
Review the source and files first. When you are ready, copy the prompt instruction or use the CLI command supported by your environment.
Install with a prompt
Paste this into a compatible coding agent:
add this skill "xr-development" from https://github.com/jame581/godotprompterInstall with the CLI
Run this command in a controlled environment after reviewing the repository:
npx skills add https://github.com/jame581/godotprompter --skill xr-developmentSkill instructions
XR Development in Godot 4.3+
All examples target Godot 4.3+ with no deprecated APIs. GDScript is shown first, then C#.
Related skills: 3d-essentials for 3D rendering and environment, physics-system for 3D physics interactions, input-handling for non-XR input patterns, export-pipeline for platform exports.
1. XR Setup
Enabling OpenXR
- Project Settings → Plugins → Enable:
OpenXR(orOpenXR Plugindepending on version) - Project Settings → XR → OpenXR → Enabled →
true - Project Settings → XR → Shaders → Enabled →
true(for XR shader support) - Rendering:
- Use Forward+ or Mobile renderer (Compatibility also works for simpler scenes)
- Set Project Settings → Display → Window → VSync Mode to
Disabled(the XR runtime controls frame timing)
Core Scene Structure
Main (Node3D)
├── XROrigin3D ← Player's physical space origin
│ ├── XRCamera3D ← Head-mounted display
│ ├── XRController3D (left) ← Left controller
│ │ └── LeftHandModel (MeshInstance3D or hand tracking)
│ ├── XRController3D (right) ← Right controller
│ │ └── RightHandModel
│ └── (XRBodyTracker via XRServer — optional full body tracking)
├── WorldEnvironment
└── GameWorld (Node3D)
└── ... level geometry
Starting XR Session
extends Node3D
func _ready() -> void:
var xr_interface: XRInterface = XRServer.find_interface("OpenXR")
if xr_interface and xr_interface.is_initialized():
get_viewport().use_xr = true
else:
push_error("OpenXR not available")
public partial class XRMain : Node3D
{
public override void _Ready()
{
var xrInterface = XRServer.FindInterface("OpenXR");
if (xrInterface != null && xrInterface.IsInitialized())
GetViewport().UseXR = true;
else
GD.PushError("OpenXR not available");
}
}
2. Controllers and Input
XRController3D nodes (one per hand, child of XROrigin3D) expose buttons via the OpenXR action map. Common buttons: trigger (select_button), grip (grip_button), thumbstick (primary_axis Vector2). Apply thumbstick locomotion velocity to XROrigin3D (not the camera).
See references/controllers-and-input.md for full XRController3D wiring, OpenXR button reference, and thumbstick locomotion recipe.
3. Hand Tracking
When the headset supports hand tracking (Quest 2+, Pico 4, some SteamVR setups), XRController3D nodes can be configured to track hand joints. Sample finger positions for gesture detection, or bind to standard select / grip events when the user pinches.
See references/hand-tracking.md for hand-tracking node setup, joint sampling, and gesture-driven interactions.
4. Grabbing Objects
Standard pattern: an Area3D on the controller detects nearby RigidBody3D objects; on grip-press, parent the body to the controller and freeze it; on grip-release, restore the parent and apply the controller's velocity to launch.
See references/grabbing-objects.md for the full physics-based grabbing implementation (GDScript + C#).
5. XR UI Interaction
For UI in VR, render a Control tree to a SubViewport, map its texture onto a Quad mesh placed in 3D space. Pointer: a RayCast3D from the controller hits the quad, the hit position is converted back to 2D viewport coords, an InputEventMouseMotion is forwarded into the SubViewport.
See references/xr-ui.md for the SubViewport-on-quad recipe and pointer/ray interaction.
6. Passthrough (Mixed Reality)
For headsets that support it (Quest 2+, Pico 4), set OpenXRInterface.environment_blend_mode = XR_ENVIRONMENT_BLEND_MODE_ALPHA_BLEND and clear the WorldEnvironment background to transparent. The user sees the real world with virtual content composited on top.
See references/passthrough.md for the full enabling steps and Quest-specific notes.
7. Meta Quest Export
Setup
- Install Android Build Template: Project → Install Android Build Template
- Install OpenXR Vendors plugin: Project → Project Settings → Plugins → Enable
Godot OpenXR Vendors(or install from AssetLib) - Export → Add → Android
- In the Android export preset:
- XR Features → XR Mode →
OpenXR - XR Features → Hand Tracking →
OptionalorRequired - XR Features → Passthrough →
Optional(if needed) - Architectures → arm64 → enabled (Quest is ARM)
- XR Features → XR Mode →
- Set minimum API level to 29+
Performance Settings for Quest
| Setting | Recommended Value |
|---|---|
| Renderer | Mobile |
| MSAA | 2x or 4x (VR needs antialiasing) |
| Texture Compression | ETC2/ASTC |
| Target FPS | 72 (Quest 2) or 90 (Quest 3) |
Critical: VR must maintain consistent frame rate. Dropped frames cause nausea. Profile aggressively and keep draw calls low.
⚠️ Changed in Godot 4.7: New project settings
xr/openxr/foveation_eye_trackedandxr/openxr/foveation_with_subsampled_imagesboth default totrue— when the foveation level is not "Disabled", eye-tracked foveation is used where the headset supports it, and subsampled images are used on Vulkan for a bigger foveation win. Subsampled images are incompatible with many screen-space features (e.g., FXAA, glow); if any are enabled, subsampled images are automatically disabled with a log warning. Set either setting tofalseto opt out. See GH-117868.
8. Godot 4.5+ XR Features
Godot 4.5 adds a D3D12 OpenXR backend on Windows (Quest Link / SteamVR alternative to Vulkan), foveated rendering on the Mobile Vulkan renderer, Application SpaceWarp frame synthesis for Quest/Pico, OpenXR Render Models for platform-native controller meshes, and windowed visionOS export via a dedicated visionOS preset. All are core Project Settings except SpaceWarp, which needs the Godot OpenXR Vendors plugin — no engine-level code changes.
See references/godot-4-5-features.md for enabling steps, GDScript + C# render-model snippets, and per-feature caveats.
9. Godot 4.6+ XR Features
OpenXR 1.1 Support (Godot 4.6+)
Godot 4.6 requests OpenXR 1.1 from the runtime, unlocking 1.1 features (promoted interaction profiles and paths, etc.) without any project-level change, and falls back to 1.0 if the runtime rejects it.
Note: To force a version, set
xr/openxr/target_api_version(e.g.1.0.48; empty by default).
Spatial Entities — Anchors, Plane Tracking, Marker Tracking (Godot 4.6+)
Godot 4.6 adds the OpenXR Spatial Entities extensions (experimental API), enabling:
- Spatial anchors — persist virtual object positions across sessions (
OpenXRSpatialAnchorCapability,OpenXRAnchorTracker,XRAnchor3D) - Plane detection — detect floor, wall, and ceiling surfaces from the environment scan
- Marker tracking — track QR codes or image markers in the scene
Basic spatial anchor usage:
# Under XR > OpenXR > Extensions > Spatial Entity enable: Enabled, Enable Spatial Anchors, and
# Enable Builtin Anchor Detection (the default RID() context needs it). All are off by default.
# The capability is a singleton only while OpenXR is enabled; without it this script will not parse.
extends XROrigin3D
func place_anchor_at(local_transform: Transform3D) -> XRAnchor3D:
if not OpenXRSpatialAnchorCapability.is_spatial_anchor_supported():
push_error("Spatial anchors are not supported on this device.")
return null
# The transform is in this XROrigin3D's local space.
var tracker: OpenXRAnchorTracker = OpenXRSpatialAnchorCapability.create_new_anchor(local_transform, RID())
if tracker == null:
return null
# An XRAnchor3D follows the tracker it names; the runtime keeps it on the real-world spot.
var anchor := XRAnchor3D.new()
anchor.tracker = tracker.name
add_child(anchor)
return anchor
// The capability is an engine singleton registered only while OpenXR is running.
public partial class SpatialAnchorManager : XROrigin3D
{
public XRAnchor3D PlaceAnchorAt(Transform3D localTransform)
{
var anchors = Engine.GetSingleton("OpenXRSpatialAnchorCapability") as OpenXRSpatialAnchorCapability;
if (anchors == null || !anchors.IsSpatialAnchorSupported())
{
GD.PushError("Spatial anchors are not supported on this device.");
return null;
}
// The transform is in this XROrigin3D's local space.
OpenXRAnchorTracker tracker = anchors.CreateNewAnchor(localTransform);
if (tracker == null)
return null;
// An XRAnchor3D follows the tracker it names; the runtime keeps it on the real-world spot.
var anchor = new XRAnchor3D { Tracker = tracker.Name };
AddChild(anchor);
return anchor;
}
}
To keep an anchor across sessions, turn on Enable Persistent Anchors, check is_spatial_persistence_supported(), then call persist_anchor(tracker, RID(), callback) and store tracker.uuid with your own data; persisted anchors come back as XRServer.TRACKER_ANCHOR trackers on the next launch. See the OpenXR spatial entities tutorial.
Note: The spatial entities classes arrived in Godot 4.6 and
OpenXRSpatialAnchorCapabilityis still marked experimental — its API may change. There is noXRSpatialAnchorclass; the scene node isXRAnchor3D.
10. Godot 4.7+ XR Features
User Presence Detection (Godot 4.7+)
OpenXRInterface exposes the OpenXR user presence extension: the user_presence_changed(is_user_present: bool) signal fires when the user puts on or removes the headset, is_user_presence_supported() reports whether the extension is supported and enabled, and is_user_present() polls the current state (both only return valid values after OpenXR is initialized). Typical use: pause the game and mute audio when the headset comes off.
Note: The signal is not emitted during application startup or shutdown — assume user presence is gained on startup and lost on shutdown.
func _ready() -> void:
var xr_interface: OpenXRInterface = XRServer.find_interface("OpenXR")
if xr_interface and xr_interface.is_user_presence_supported():
xr_interface.user_presence_changed.connect(_on_user_presence_changed)
func _on_user_presence_changed(is_user_present: bool) -> void:
get_tree().paused = not is_user_present
public override void _Ready()
{
var xrInterface = XRServer.FindInterface("OpenXR") as OpenXRInterface;
if (xrInterface != null && xrInterface.IsUserPresenceSupported())
xrInterface.UserPresenceChanged += OnUserPresenceChanged;
}
private void OnUserPresenceChanged(bool isUserPresent)
{
GetTree().Paused = !isUserPresent;
}
Composition Layer Eye Visibility (Godot 4.7+)
OpenXRCompositionLayer gains eye_visibility (EyeVisibility enum: EYE_VISIBILITY_BOTH = 0 default, EYE_VISIBILITY_LEFT = 1, EYE_VISIBILITY_RIGHT = 2) — the eye(s) the composition layer is visible to. Renders a quad/cylinder/equirect layer to one eye only, e.g. for per-eye calibration screens or stereo content authored per eye.
$OpenXRCompositionLayerQuad.eye_visibility = OpenXRCompositionLayer.EYE_VISIBILITY_LEFT
GetNode<OpenXRCompositionLayerQuad>("OpenXRCompositionLayerQuad").EyeVisibility =
OpenXRCompositionLayer.EyeVisibilityEnum.Left;
Note: Not all composition layer types or runtimes support restricting visibility to a single eye.
Spatial Anchor Extensibility (Godot 4.7+)
OpenXRSpatialAnchorCapability.create_new_anchor() gains an optional next parameter — full signature: create_new_anchor(transform: Transform3D, spatial_context: RID = RID(), next: OpenXRStructureBase = null) -> OpenXRAnchorTracker. next must be a valid next object for the XrSpatialAnchorCreateInfoEXT chain, letting vendor-specific create-info structs be appended when creating an anchor. Existing calls are unaffected (compatible change, GH-118128).
Note:
OpenXRSpatialAnchorCapabilityis still marked experimental — the class may change in future versions. Section 9 shows typical anchor placement.
11. Common Pitfalls
| Symptom | Cause | Fix |
|---|---|---|
| Black screen in headset | use_xr = true not set on viewport | Set in _ready() after checking XR interface |
| Controller input not firing | Wrong signal name for the platform | Check OpenXR action map bindings in Project Settings |
| Objects scale wrong in VR | Scene not built at real-world scale | Use 1 unit = 1 meter throughout the scene |
| Motion sickness from locomotion | Smooth rotation | Use snap turning (30° increments) or add a vignette during movement |
| UI unreadable in VR | Panel too far away or too small | Place UI at 1–2m distance, use SubViewport at 1024+ resolution |
| Hand tracking jittery | Raw joint data used directly | Apply smoothing (lerp toward new position each frame) |
| Export fails on Quest | Missing Android build template or wrong architecture | Install Android Build Template; enable arm64; set API level 29+ |
12. Implementation Checklist
- OpenXR is enabled in Project Settings
- Scene uses
XROrigin3D→XRCamera3D+XRController3Dhierarchy - XR session is started with
get_viewport().use_xr = trueafter interface check - World is built at 1 unit = 1 meter scale
- Controller input uses OpenXR action names (
trigger_click,grip_click, etc.) - Fallback exists for hand tracking → controller tracking
- UI panels use SubViewport rendered on a 3D mesh
- Locomotion includes comfort options (snap turn, vignette)
- VSync is disabled (XR runtime handles frame timing)
- Quest export uses Mobile renderer, arm64 architecture, API level 29+
- On Windows Quest Link / SteamVR builds, consider D3D12 backend if Vulkan drivers are problematic (Godot 4.5+)
- Foveated rendering enabled via
xr/openxr/foveation_levelon the Mobile renderer for standalone Quest/Pico targets (Godot 4.5+) - Application SpaceWarp evaluated for performance budget on Meta Quest / Pico targets (Godot 4.5+)
-
OpenXRRenderModelManager+xr/openxr/extensions/render_modelused for controller visuals where supported (Godot 4.5+) - visionOS export uses the dedicated visionOS preset; windowed apps only through 4.7 (Godot 4.5+)
- Spatial anchors are created with
OpenXRSpatialAnchorCapabilityand shown withXRAnchor3D, with the spatial entities extension enabled (Godot 4.6+) - Headset on/off handled via
OpenXRInterface.user_presence_changed— pause/mute when the user is away (Godot 4.7+) - Foveation defaults reviewed —
xr/openxr/foveation_eye_trackedandxr/openxr/foveation_with_subsampled_imagesare on by default; disable subsampled images if you rely on FXAA/glow (Godot 4.7+)
Files included
- references/controllers-and-input.md
- references/godot-4-5-features.md
- references/grabbing-objects.md
- references/hand-tracking.md
- references/passthrough.md
- references/xr-ui.md
- SKILL.md

