Meta Quest: Player starts at center, but objects are where the old guardian was

Godot Version

`Godot 4.4

Question

Meta Quest 2
My Player is an Autoload and starts fine.

Then I set up some objects in the Scene. They appear at the guardian position where I programmed the last time (another room) like 10 meters away.

I link the object to the player root.

To verify I printed all Coordinates of the children.

→ XROrigin3D at position: (0.0, 2.0, 0.0)
09-04 17:47:42.120 16750 16797 I godot : → RightController at position: (0.0, 2.0, 0.0)
09-04 17:47:42.120 16750 16797 I godot : → RightHand at position: (0.0, 0.0, 0.0)
09-04 17:47:42.120 16750 16797 I godot : → Hand_R at position: (0.03, -0.05, 0.15)
09-04 17:47:42.120 16750 16797 I godot : → Armature at position: (0.03, -0.05, 0.15)
09-04 17:47:42.120 16750 16797 I godot : → Skeleton3D at position: (0.03, -0.05, 0.15)
09-04 17:47:42.120 16750 16797 I godot : → mesh_Hand_R at position: (0.03, -0.05, 0.15)
09-04 17:47:42.120 16750 16797 I godot : → AnimationPlayer
09-04 17:47:42.120 16750 16797 I godot : → AnimationTree
09-04 17:47:42.120 16750 16797 I godot : → MovementTurn
09-04 17:47:42.120 16750 16797 I godot : → RightHandSensor at position: (0.0, 2.0, 0.0)
09-04 17:47:42.120 16750 16797 I godot : → CollisionShape3D at position: (0.013742, 2.0, 0.041086)
09-04 17:47:42.121 16750 16797 I godot : → MovementJump
09-04 17:47:42.121 16750 16797 I godot : → LeftController at position: (0.0, 2.0, 0.0)
09-04 17:47:42.121 16750 16797 I godot : → LeftHand at position: (0.0, 0.0, 0.0)
09-04 17:47:42.121 16750 16797 I godot : → Hand_L at position: (-0.03, -0.05, 0.15)
09-04 17:47:42.121 16750 16797 I godot : → Armature at position: (-0.03, -0.05, 0.15)
09-04 17:47:42.121 16750 16797 I godot : → Skeleton3D at position: (-0.03, -0.05, 0.15)
09-04 17:47:42.121 16750 16797 I godot : → mesh_Hand_L at position: (-0.03, -0.05, 0.15)
09-04 17:47:42.121 16750 16797 I godot : → AnimationPlayer
09-04 17:47:42.121 16750 16797 I godot : → AnimationTree
09-04 17:47:42.121 16750 16797 I godot : → MovementDirect
09-04 17:47:42.121 16750 16797 I godot : → LeftHandSensor at position: (0.0, 2.0, 0.0)
09-04 17:47:42.121 16750 16797 I godot : → CollisionShape3D at position: (0.0, 2.0, 0.02811)
09-04 17:47:42.121 16750 16797 I godot : → XRCamera3D at position: (0.0, 2.0, 0.0)
09-04 17:47:42.121 16750 16797 I godot : → XRToolsPlayerBody at position: (0.0, 0.0, 0.0)
09-04 17:47:42.121 16750 16797 I godot : → @CollisionShape3D@4 at position: (0.0, 0.8, 0.0)
09-04 17:47:42.121 16750 16797 I godot : → @ShapeCast3D@5 at position: (0.0, 0.0, 0.0)
09-04 17:47:42.121 16750 16797 I godot : → CutScene_1
09-04 17:47:42.121 16750 16797 I godot : → Bridge_Simple at position: (0.0, 0.0, 0.0)
09-04 17:47:42.121 16750 16797 I godot : → CollisionShape3D at position: (0.0, 0.0, 0.0)
09-04 17:47:42.121 16750 16797 I godot : → MeshInstance3D at position: (0.0, 0.0, 0.0)
09-04 17:47:42.121 16750 16797 I godot : → simple_bridge_model at position: (0.0, 0.0, 0.0)
09-04 17:47:42.121 16750 16797 I godot : → Cube at position: (0.0, -0.05, 0.0)
09-04 17:47:42.121 16750 16797 I godot : → Cylinder at position: (-0.79318, 2.0, -0.0)
09-04 17:47:42.121 16750 16797 I godot : → Sphere at position: (-0.8, 3.0, -0.0)
09-04 17:47:42.121 16750 16797 I godot : → OmniLight3D at position: (-0.744243, 2.90472, 0.0)
09-04 17:47:42.121 16750 16797 I godot : → CollisionShapeLeft at position: (0.790342, 0.904485, 0.0)
09-04 17:47:42.121 16750 16797 I godot : → CollisionShapeRight at position: (-0.743498, 0.904485, 0.0)
09-04 17:47:42.121 16750 16797 I godot : → @Node3D@10 at position: (0.0, 1.25, -3.0)
09-04 17:47:42.121 16750 16797 I godot : → @MeshInstance3D@6 at position: (0.0, 1.25, -3.0)
09-04 17:47:42.121 16750 16797 I godot : → @Node@7
09-04 17:47:42.121 16750 16797 I godot : → @Area3D@9 at position: (0.0, 1.25, -3.0)
09-04 17:47:42.121 16750 16797 I godot : → @CollisionShape3D@8 at position: (0.0, 1.25, -3.0)

Yes, if you move 10 meters away, everything will appear 10 meters away from you.

The XROrigin3D node maps a physical location in your area to a virtual space in your game. Everything is tracked in relation to that location. We have limited control over where that physical location is.

You can influence some of this behaviour by setting the reference space Godot uses in the OpenXR settings.

Stage
By default the option is set to “Stage”.

The Stage reference space is directly linked to the boundary you’ve setup and generally positions the XROrigin3D in the centre of that boundary.

If the Quest is still aware of the previous location of this point, it will keep using that location, even if you restart your application while having moved to a different room. So the behaviour you describe is absolutely expected in this scenario.

If you have multiple boundaries setup, or you change your boundary, a new physical location for the origin point will be set.

If you tell your headset to recentre itself (hold the Meta button on the right controller for 3 seconds), the behaviour is somewhat undefined. I believe the Quest does nothing (as is appropriate in Stage) but some XR runtimes will reorient or even move the origin point.

Local
The Local reference space defines a reference space where the origin point “starts” where your head is.

I put starts between quotes because this starting point can differ from runtime to runtime. Some define the point when the device was turned on, some when the device was last recentred, some when an application starts.

I believe with the Quest the position is determined when last the user did a recentre. So here the same applies. If you’re walked 10 meters away from the place you last recentred, your player will now be 10 meters away from the origin point, even if you’ve quit and restarted your application.

Note: Not a reference space you’d want to use in your use case but I’m adding it here for completeness for anyone who runs into the same problem.

Local floor
Finally we have the local floor reference space. This is a variation on Local but instead of the origin being positioned where the players head is, it is now centred on the floor at that location.

Again the same applies, the positioning is determined when the player recentres. If you’ve moved away from that location by 10 meters, your player will start 10 meters away from the XROrigin3D. But the moment the player recentres, the origin point will move to where the player is currently standing, which in game means the player moves to the origin point.

This seems to be the behaviour you’re expecting with the only footnote that you’d like the recentre to happen when the player starts your application. A wish I have too.
Sadly this is not something OpenXR currently supports. The recentre logic is deemed a system feature that can only be controlled by the user.

Alternative
Luckily this problem predates OpenXR and before we had the system recentre logic, we had to do it all ourselves. The below works best in combination with the default Stage reference space.

You can call XRServer.center_on_hmd. This causes Godot to look at the current headset position and adjust all the tracking so the origin point moves to where the player is.

In your case you want to set the rotation_mode to RESET_BUT_KEEP_TILT and keep_height to true.

You do however need to make sure you time this right. Call this before we have tracking data, or before the player puts on the headset (more an issue for PCVR than Quest), and it won’t work.

You would want to react to the session_visible signal before calling this.

You probably also want to react to the pose_recentered signal so the user can recenter by holding the meta button.

2 Likes

This is an incredible detailed answer.
Yesterday I found “Stage” and “Local” in the Project Settings / XR / OpenXR. I set it to Local and it seems to solve my problem.

I’ll try your other suggestions.

Thank you, very helpful.

The following code might be more clear for beginners (Works only if Project Settings / XR / OpenXR/“Reference Space” is set to Stage):

extends Node3D
# to debug:
# pwd: /Users/<your_user_name>/platform-tools
# ./adb logcat | grep godot

var xr_interface: XRInterface

var g_started:bool=false

func _ready():
	xr_interface = XRServer.find_interface("OpenXR")
	if xr_interface and xr_interface.is_initialized():
		print("OpenXR initialized successfully")

		# Turn off v-sync!
		DisplayServer.window_set_vsync_mode(DisplayServer.VSYNC_DISABLED)

		# Change our main viewport to output to the HMD
		get_viewport().use_xr = true
		
		xr_interface.connect("session_visible", Callable(self, "_on_session_visible"))
	
	else:
		print("OpenXR not initialized, please check if your headset is connected")

	
func _on_session_visible(visble:bool=false):
	if visible and not g_started:
		print("IS VISIBLE .........")
		XRServer.center_on_hmd(1, true)
		#LevelBuilder.run(0)	 
		LevelManager.start_cut_scene(1)
		g_started = true
	else:
		print("not visible yet")
	
	

This topic was automatically closed 30 days after the last reply. New replies are no longer allowed.