Quick answer: This happens when the XR reference space type does not match your floor level setup. If using XRServer.REFERENCE_SPACE_LOCAL, the origin is at the headset's position when tracking started, not the floor. Switch to XRServer.
Here is how to fix Godot XR origin tracking offset wrong. You launch your VR game in Godot 4, put on the headset, and find yourself either floating above the floor, stuck underground, or offset several meters from where you should be. XR tracking position issues in Godot stem from the relationship between XROrigin3D, XRCamera3D, the reference space type, and the headset’s guardian/boundary calibration. This guide covers every common cause and fix.
Understanding XR Origin, Camera, and Reference Spaces
In Godot 4’s XR system, XROrigin3D defines the center of your play space in the virtual world. XRCamera3D is a child of the origin and receives tracking data from the headset. The camera’s local position relative to the origin reflects the player’s real-world head position relative to their play space center.
The reference space determines what “origin” means in physical space. The two main types are:
Local (eye-level): The origin is at the headset’s position when tracking started. Y = 0 is at eye level, not the floor. This is used for seated VR experiences.
Stage (floor-level): The origin is at the center of the calibrated play area, and Y = 0 is at floor level. This is used for room-scale VR.
Most tracking offset problems come from using the wrong reference space or not accounting for the difference between eye-level and floor-level origins.
Fix 1: Use the Correct Reference Space
If your player is floating 1.5–1.8 meters above the ground, you are likely using the local reference space in a game that expects floor-level tracking. The local space places Y = 0 at eye level, so the floor in your scene is 1.7 meters below the camera.
extends Node3D
@onready var xr_origin := $XROrigin3D
func _ready() -> void:
var xr_interface := XRServer.find_interface("OpenXR")
if xr_interface:
xr_interface.initialize()
# Use stage (floor-level) for room-scale VR
var viewport := get_viewport()
viewport.use_xr = true
# Check the reference space in OpenXR project settings
# Project > Project Settings > XR > OpenXR > Reference Space
# Set to "Stage" for floor-level tracking
print("XR initialized with reference space: stage")
In the Godot project settings, navigate to XR > OpenXR > Reference Space and set it to Stage for room-scale games or Local for seated experiences. This setting determines the default reference frame when OpenXR initializes.
Fix 2: Adjust the XROrigin3D Position
If you cannot control the reference space (for example, when supporting multiple headset types with different defaults), you can compensate by adjusting the XROrigin3D node’s position.
extends XROrigin3D
@export var expected_player_height := 1.7
func _ready() -> void:
# Wait one frame for XR to initialize and report position
await get_tree().process_frame
await get_tree().process_frame
var camera := $XRCamera3D
var camera_y := camera.global_position.y
# If camera is near 0, we are in local space (eye-level)
# Offset origin down so floor aligns
if abs(camera_y) < 0.3:
global_position.y = -expected_player_height
print("Detected local reference space, offsetting origin")
else:
print("Floor-level tracking detected, camera Y: ", camera_y)
This approach detects whether the camera starts near Y = 0 (local/eye-level) or at head height (stage/floor-level) and adjusts accordingly. It is a pragmatic workaround when you need to support headsets that report different reference spaces.
Fix 3: Set the Correct World Scale
If the tracking position is technically correct but everything feels wrong—the room seems too large or too small, or head movements feel exaggerated or muted—the issue is world_scale on XROrigin3D.
extends XROrigin3D
func _ready() -> void:
# 1.0 = 1 game unit = 1 meter (correct for most games)
world_scale = 1.0
# If your game uses centimeters as the base unit:
# world_scale = 100.0
# If your game uses a stylized scale (e.g., miniature world):
# world_scale = 0.5 # Player feels twice as large
The world_scale property multiplies all tracking positions and distances. A value of 1.0 means 1 meter of real-world movement equals 1 unit of in-game movement. If your game’s geometry was built at a different scale (which is surprisingly common when importing assets from Blender or other tools), adjust world_scale to match.
A telltale sign of wrong world_scale: the player’s hands appear to be the wrong distance from their body, or doorways feel too narrow or too wide.
Fix 4: Handle Recentering and Tracking Loss
XR headsets can recenter their tracking space at any time—when the user presses a recenter button on the headset, when the guardian boundary is reset, or when tracking is lost and recovered. This causes a sudden position jump that can break your game if you are not handling it.
extends Node3D
func _ready() -> void:
# Listen for reference frame changes
XRServer.pose_recentered.connect(_on_pose_recentered)
func _on_pose_recentered() -> void:
print("XR tracking recentered")
# Re-align the player to the expected position
# This depends on your game's teleportation/movement system
_realign_player()
func _realign_player() -> void:
var origin := $XROrigin3D
var camera := $XROrigin3D/XRCamera3D
# Keep the origin at the game's player position
# but adjust for the camera's new local offset
var camera_offset := camera.position
camera_offset.y = 0 # Keep vertical position from tracking
origin.global_position -= camera_offset
You should also provide a manual recenter option in your game’s pause menu. This is essential for seated VR experiences where the player may shift in their chair:
func recenter_player() -> void:
# Reset the reference frame to current headset position
XRServer.center_on_hmd(XRServer.RESET_FULL_ROTATION, true)
Fix 5: Verify the Scene Tree Structure
The XR node hierarchy in Godot 4 must follow a specific structure. XRCamera3D and XRController3D nodes must be direct children of XROrigin3D. If they are nested under other nodes, tracking data is applied incorrectly.
# CORRECT scene tree structure:
# XROrigin3D
# ├── XRCamera3D
# ├── XRController3D (left hand, tracker = "left_hand")
# └── XRController3D (right hand, tracker = "right_hand")
# WRONG — camera nested under another node:
# XROrigin3D
# └── Node3D (player model)
# └── XRCamera3D ← tracking offset will be wrong
If you need to attach additional nodes (like a player model or collision shape), make them siblings of the XRCamera3D under XROrigin3D, not parents.
Debugging XR Tracking Issues
When tracking positions seem wrong, add diagnostic output to identify the source:
extends XROrigin3D
func _process(_delta: float) -> void:
var camera := $XRCamera3D
if Input.is_action_just_pressed("debug_xr"):
print("Origin global pos: ", global_position)
print("Camera local pos: ", camera.position)
print("Camera global pos: ", camera.global_position)
print("World scale: ", world_scale)
var xr := XRServer.find_interface("OpenXR")
if xr:
print("XR play area mode: ", xr.get_play_area_mode())
Key things to check: if the camera’s local Y position is near 0, you are in local (eye-level) space. If it is around 1.5–1.9, you are in stage (floor-level) space. If the origin’s global position is not where you expect it, something is moving the origin node unexpectedly—check for scripts or animations that modify its transform.
“XR tracking issues are almost always about reference spaces. Understand the difference between local and stage, verify your world_scale matches your game units, and always provide a recenter button. Everything else is edge cases.”
Related Issues
For other Godot 3D rendering issues, see Fix: Godot MultiMeshInstance Not Visible or Showing. If your VR players are reporting tracking bugs, set up automated bug collection with How to Add a Bug Reporter to a Godot Game—screenshots from inside VR are invaluable for debugging spatial issues.
XR tracking is only as good as the headset’s calibration. Your code can be perfect and the player will still report offset issues if their guardian is not set up correctly. Provide in-game recenter and floor-height adjustment options.