XR / OpenXR — VR applications in Godot
XROrigin3D, XRCamera3D, XRController3D — a basic VR scene for Meta Quest and SteamVR.
Godot has built-in OpenXR support right in the core, without separate packages. Compared to Unity this is especially nice: launching a VR project in Godot is literally a few nodes.
OpenXR in Godot — built in
OpenXR is the Khronos Group’s open standard for VR/AR. It replaces the SDKs from Oculus, SteamVR, Vive Wave. In Godot 4.x OpenXR is part of the engine, activated in Project Settings → XR.
Activation
- Project Settings → XR → OpenXR → Enabled = true.
- Choose interaction profiles: Oculus Touch, Khronos Simple, Valve Index. By default the main ones are enabled.
- For Android (Quest): Build Profile → Android, in the Export preset enable XR permissions.
Basic scene
Main (Node3D)
└── XROrigin3D ← represents the player's physical room
├── XRCamera3D ← VR camera, current=true
├── XRController3D (Left) ← controller_tracker = "/user/hand/left"
│ └── MeshInstance3D ← visual model of the controller
└── XRController3D (Right) ← controller_tracker = "/user/hand/right"
└── MeshInstance3D
Nodes:
XROrigin3D— the root of the VR rig. Its position = the center of the play room.XRCamera3D— a camera attached to the headset. It moves when the player physically walks.XRController3D— a node that tracks a controller. Its position/rotation update automatically.
Minimal code to launch VR
extends Node3D
var xr_interface: XRInterface
func _ready() -> void:
xr_interface = XRServer.find_interface("OpenXR")
if xr_interface and xr_interface.is_initialized():
print("OpenXR initialized!")
# In Godot, for VR mode you need to set the viewport
get_viewport().use_xr = true
else:
push_warning("OpenXR not available. Falling back to desktop.")
After this, your scene with XROrigin3D will launch in VR mode on the connected headset.
Without viewport.use_xr = true your scene will open as an ordinary desktop window, even if the headset
is connected. This is handy for testing “without putting on the VR”. When you’re ready — set the flag.
Getting input from the controllers
An XR controller exposes senders for buttons/axes via input actions, like the usual Input actions in Godot:
- In Project Settings → Input Map create an action
vr_select. - Binding: add OpenXR Action Map → Bind to vr_select (via the OpenXR Action Map editor).
- In a script:
extends XRController3D
func _ready() -> void:
button_pressed.connect(_on_button)
button_released.connect(_on_button_released)
input_float_changed.connect(_on_float_input)
func _on_button(name: String) -> void:
if name == "trigger_click":
_on_trigger_pressed()
func _on_trigger_pressed() -> void:
print("Right trigger pressed")
# You can fire a raycast / instantiate a projectile
XRController3D has built-in signals:
button_pressed(name),button_released(name)input_float_changed(name, value)— for analog axes (trigger, grip)input_vector2_changed(name, value)— for thumbsticks
Grab — pick up an object
The simplest grab without community plugins:
extends XRController3D
@export var grab_area: Area3D # a child of XRController3D with a ~5cm sphere
var held_object: RigidBody3D
func _ready() -> void:
button_pressed.connect(_on_button)
func _on_button(name: String) -> void:
if name == "grip_click":
if held_object == null:
_try_grab()
else:
_release()
func _try_grab() -> void:
var bodies := grab_area.get_overlapping_bodies()
for body in bodies:
if body is RigidBody3D and body.has_method("on_grabbed"):
held_object = body
held_object.freeze = true
held_object.reparent(self)
return
func _release() -> void:
if held_object == null:
return
held_object.reparent(get_tree().current_scene)
held_object.freeze = false
# You can recompute velocity from the controller's recent movement
held_object = null
For a serious VR project, use the godot-xr-tools plugin (Asset Library) — it has ready-made grabbables, a teleporter, climbing, hand poses. The equivalent of Unity’s XRI.
Hand tracking
Godot 4.6 supports hand tracking via the OpenXR Hand Tracking extension:
- Project Settings → OpenXR → Extensions → Hand Tracking = enabled.
- The
XRHandModifier3Dnode (4.6+) orOpenXRInterface.hand_tracking_enabledin code. - You get access to the 26 joints of each hand via
XRPoselookup.
var xr := XRServer.find_interface("OpenXR")
var left_hand_pose := XRServer.get_tracker("/user/hand/left").get_pose("default")
Performance in VR
The same hard requirements as in Unity:
- 72 FPS on Quest 2/3, 90+ on PCVR.
- A Mobile renderer is mandatory for Quest standalone.
- MSAA 4× — the standard for VR.
- Multiview rendering (single-pass) — enabled via Project Settings → Rendering → Performance → Multiview.
- Foveated rendering — Project Settings → OpenXR → Foveation Level (Low/Med/High).
The standard development pipeline
- Local testing in the editor via Quest Link (USB or AirLink).
- After polishing — a standalone build for Quest (.apk via Android export).
- Installation via adb or Meta Quest Developer Hub.
The godot-xr-tools plugin
Most VR projects in Godot rely on the community plugin godot-xr-tools (via the Asset Library). What it provides:
- XRToolsFunctionPointer — a ray from the controller for distant selection.
- XRToolsFunctionPickup — an advanced grab with throw velocity, snap points.
- XRToolsFunctionTeleport — teleport locomotion with proper fade and validation.
- XRToolsHandPoseController — hand pose animation depending on what is grabbed.
- XRToolsMovementProvider — direct/snap/smooth turn, climbing, gliding.
If you’re planning a serious VR project — install it right away.
Comparing Godot vs Unity for VR
| Godot | Unity | |
|---|---|---|
| OpenXR in core | ✅ built in | ❌ requires the OpenXR Plugin package |
| Interaction toolkit | ⚠️ community (godot-xr-tools) | ✅ official (XRI) |
| Hand tracking | ✅ since 4.6 | ✅ XR Hands package |
| Performance in VR | Good | Good (Single-Pass Instanced) |
| Magic Leap / Vision Pro | Limited | Better (full support via XR Hub) |
| Build size (minimum) | ~40 MB | ~80 MB |
For a hobby Quest 2 project Godot is often simpler. For commercial AAA VR Unity is still ahead thanks to XRI and broader device support.