@tool class_name XRToolsStartXR extends Node ## XRTools Start XR Class ## ## This class supports both the OpenXR and WebXR interfaces, and handles ## the initialization of the interface as well as reporting when the user ## starts and ends the VR session. ## ## For OpenXR this class also supports passthrough on compatible devices such ## as the Meta Quest 1 and 2. ## This signal is emitted when XR becomes active. For OpenXR this corresponds ## with the 'openxr_focused_state' signal which occurs when the application ## starts receiving XR input, and for WebXR this corresponds with the ## 'session_started' signal. signal xr_started ## This signal is emitted when XR ends. For OpenXR this corresponds with the ## 'openxr_visible_state' state which occurs when the application has lost ## XR input focus, and for WebXR this corresponds with the 'session_ended' ## signal. signal xr_ended ## If true, the XR interface is automatically initialized @export var auto_initialize : bool = true ## Adjusts the pixel density on the rendering target @export var render_target_size_multiplier : float = 1.0 ## If true, the XR passthrough is enabled (OpenXR only) @export var enable_passthrough : bool = false: set = _set_enable_passthrough ## Physics rate multiplier compared to HMD frame rate @export var physics_rate_multiplier : int = 1 ## If non-zero, specifies the target refresh rate @export var target_refresh_rate : float = 0 ## Current XR interface var xr_interface : XRInterface ## XR active flag var xr_active : bool = false # Current refresh rate var _current_refresh_rate : float = 0 # Handle auto-initialization when ready func _ready() -> void: if !Engine.is_editor_hint() and auto_initialize: initialize() ## Initialize the XR interface func initialize() -> bool: # Check for OpenXR interface xr_interface = XRServer.find_interface('OpenXR') if xr_interface: return _setup_for_openxr() # Check for WebXR interface xr_interface = XRServer.find_interface('WebXR') if xr_interface: return _setup_for_webxr() # No XR interface xr_interface = null print("No XR interface detected") return false # Check for configuration issues func _get_configuration_warnings() -> PackedStringArray: var warnings := PackedStringArray() if physics_rate_multiplier < 1: warnings.append("Physics rate multiplier should be at least 1x the HMD rate") return warnings # Perform OpenXR setup func _setup_for_openxr() -> bool: print("OpenXR: Configuring interface") # Set the render target size multiplier - must be done befor initializing interface # NOTE: Only implemented in Godot 4.1+ if "render_target_size_multiplier" in xr_interface: xr_interface.render_target_size_multiplier = render_target_size_multiplier # Initialize the OpenXR interface if not xr_interface.is_initialized(): print("OpenXR: Initializing interface") if not xr_interface.initialize(): push_error("OpenXR: Failed to initialize") return false # Connect the OpenXR events xr_interface.connect("session_begun", _on_openxr_session_begun) xr_interface.connect("session_visible", _on_openxr_visible_state) xr_interface.connect("session_focussed", _on_openxr_focused_state) # Check for passthrough if enable_passthrough and xr_interface.is_passthrough_supported(): enable_passthrough = xr_interface.start_passthrough() # Disable vsync DisplayServer.window_set_vsync_mode(DisplayServer.VSYNC_DISABLED) # Switch the viewport to XR get_viewport().use_xr = true # Report success return true # Handle OpenXR session ready func _on_openxr_session_begun() -> void: print("OpenXR: Session begun") # Get the reported refresh rate _current_refresh_rate = xr_interface.get_display_refresh_rate() if _current_refresh_rate > 0: print("OpenXR: Refresh rate reported as ", str(_current_refresh_rate)) else: print("OpenXR: No refresh rate given by XR runtime") # Pick a desired refresh rate var desired_rate := target_refresh_rate if target_refresh_rate > 0 else _current_refresh_rate var available_rates : Array = xr_interface.get_available_display_refresh_rates() if available_rates.size() == 0: print("OpenXR: Target does not support refresh rate extension") elif available_rates.size() == 1: print("OpenXR: Target supports only one refresh rate") elif desired_rate > 0: print("OpenXR: Available refresh rates are ", str(available_rates)) var rate = _find_closest(available_rates, desired_rate) if rate > 0: print("OpenXR: Setting refresh rate to ", str(rate)) xr_interface.set_display_refresh_rate(rate) _current_refresh_rate = rate # Pick a physics rate var active_rate := _current_refresh_rate if _current_refresh_rate > 0 else 144.0 var physics_rate := int(round(active_rate * physics_rate_multiplier)) print("Setting physics rate to ", physics_rate) Engine.physics_ticks_per_second = physics_rate # Handle OpenXR visible state func _on_openxr_visible_state() -> void: # Report the XR ending if xr_active: print("OpenXR: XR ended (visible_state)") xr_active = false emit_signal("xr_ended") # Handle OpenXR focused state func _on_openxr_focused_state() -> void: # Report the XR starting if not xr_active: print("OpenXR: XR started (focused_state)") xr_active = true emit_signal("xr_started") # Handle changes to the enable_passthrough property func _set_enable_passthrough(p_new_value : bool) -> void: # Save the new value enable_passthrough = p_new_value # Only actually start our passthrough if our interface has been instanced # if not this will be delayed until initialise is successfully called. if xr_interface: if enable_passthrough: # unset enable_passthrough if we can't start it. enable_passthrough = xr_interface.start_passthrough() else: xr_interface.stop_passthrough() var modes = xr_interface.get_supported_environment_blend_modes() if xr_interface.XR_ENV_BLEND_MODE_ALPHA_BLEND in modes: xr_interface.set_environment_blend_mode(xr_interface.XR_ENV_BLEND_MODE_ALPHA_BLEND) # Perform WebXR setup func _setup_for_webxr() -> bool: print("WebXR: Configuring interface") # Connect the WebXR events xr_interface.connect("session_supported", _on_webxr_session_supported) xr_interface.connect("session_started", _on_webxr_session_started) xr_interface.connect("session_ended", _on_webxr_session_ended) xr_interface.connect("session_failed", _on_webxr_session_failed) # WebXR currently has no means of querying the refresh rate, so use # something sufficiently high Engine.physics_ticks_per_second = 144 # If the viewport is already in XR mode then we are done. if get_viewport().use_xr: return true # This returns immediately - our _webxr_session_supported() method # (which we connected to the "session_supported" signal above) will # be called sometime later to let us know if it's supported or not. xr_interface.is_session_supported("immersive-vr") # Report success return true # Handle WebXR session supported check func _on_webxr_session_supported(session_mode: String, supported: bool) -> void: if session_mode == "immersive-vr": if supported: # WebXR supported - show canvas on web browser to enter WebVR $EnterWebXR.visible = true else: OS.alert("Your web browser doesn't support VR. Sorry!") # Called when the WebXR session has started successfully func _on_webxr_session_started() -> void: print("WebXR: Session started") # Hide the canvas and switch the viewport to XR $EnterWebXR.visible = false get_viewport().use_xr = true # Report the XR starting xr_active = true emit_signal("xr_started") # Called when the user ends the immersive VR session func _on_webxr_session_ended() -> void: print("WebXR: Session ended") # Show the canvas and switch the viewport to non-XR $EnterWebXR.visible = true get_viewport().use_xr = false # Report the XR ending xr_active = false emit_signal("xr_ended") # Called when the immersive VR session fails to start func _on_webxr_session_failed(message: String) -> void: OS.alert("Unable to enter VR: " + message) $EnterWebXR.visible = true # Handle the Enter VR button on the WebXR browser func _on_enter_webxr_button_pressed() -> void: # Configure the WebXR interface xr_interface.session_mode = 'immersive-vr' xr_interface.requested_reference_space_types = 'bounded-floor, local-floor, local' xr_interface.required_features = 'local-floor' xr_interface.optional_features = 'bounded-floor' # Initialize the interface. This should trigger either _on_webxr_session_started # or _on_webxr_session_failed if not xr_interface.initialize(): OS.alert("Failed to initialize WebXR") # Find the closest value in the array to the target func _find_closest(values : Array, target : float) -> float: # Return 0 if no values if values.size() == 0: return 0.0 # Find the closest value to the target var best : float = values.front() for v in values: if abs(target - v) < abs(target - best): best = v # Return the best value return best