~3 min read

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

  1. Project Settings → XR → OpenXR → Enabled = true.
  2. Choose interaction profiles: Oculus Touch, Khronos Simple, Valve Index. By default the main ones are enabled.
  3. 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.

The default VR target is the main scene as a flat window

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:

  1. In Project Settings → Input Map create an action vr_select.
  2. Binding: add OpenXR Action Map → Bind to vr_select (via the OpenXR Action Map editor).
  3. 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:

  1. Project Settings → OpenXR → Extensions → Hand Tracking = enabled.
  2. The XRHandModifier3D node (4.6+) or OpenXRInterface.hand_tracking_enabled in code.
  3. You get access to the 26 joints of each hand via XRPose lookup.
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

  1. Local testing in the editor via Quest Link (USB or AirLink).
  2. After polishing — a standalone build for Quest (.apk via Android export).
  3. 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

GodotUnity
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 VRGoodGood (Single-Pass Instanced)
Magic Leap / Vision ProLimitedBetter (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.