344 lines
		
	
	
	
		
			9.8 KiB
		
	
	
	
		
			GDScript
		
	
	
	
	
	
			
		
		
	
	
			344 lines
		
	
	
	
		
			9.8 KiB
		
	
	
	
		
			GDScript
		
	
	
	
	
	
| @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.
 | |
| 
 | |
| 
 | |
| ## 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
 | |
| 
 | |
| ## This signal is emitted if XR fails to initialize.
 | |
| signal xr_failed_to_initialize
 | |
| 
 | |
| 
 | |
| ## XR active flag
 | |
| static var _xr_active : bool = false
 | |
| 
 | |
| 
 | |
| ## Optional viewport to control
 | |
| @export var viewport : Viewport
 | |
| 
 | |
| ## 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 frame rate
 | |
| var xr_frame_rate : float = 0
 | |
| 
 | |
| # Is a WebXR is_session_supported query running
 | |
| var _webxr_session_query : bool = false
 | |
| 
 | |
| 
 | |
| # Handle auto-initialization when ready
 | |
| func _ready() -> void:
 | |
| 	if !Engine.is_editor_hint():
 | |
| 		_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")
 | |
| 	xr_failed_to_initialize.emit()
 | |
| 	return false
 | |
| 
 | |
| 
 | |
| ## End the XR experience
 | |
| func end_xr() -> void:
 | |
| 	# For WebXR drop the interactive experience and go back to the web page
 | |
| 	if xr_interface is WebXRInterface:
 | |
| 		# Uninitialize the WebXR interface
 | |
| 		xr_interface.uninitialize()
 | |
| 		return
 | |
| 
 | |
| 	# Terminate the application
 | |
| 	get_tree().quit()
 | |
| 
 | |
| 
 | |
| ## Test if XR is active
 | |
| static func is_xr_active() -> bool:
 | |
| 	return _xr_active
 | |
| 
 | |
| 
 | |
| ## Get the XR viewport
 | |
| func get_xr_viewport() -> Viewport:
 | |
| 	# Use the specified viewport if set
 | |
| 	if viewport:
 | |
| 		return viewport
 | |
| 
 | |
| 	# Use the default viewport
 | |
| 	return get_viewport()
 | |
| 
 | |
| 
 | |
| # 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
 | |
| 	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")
 | |
| 			xr_failed_to_initialize.emit()
 | |
| 			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_xr_viewport().transparent_bg = enable_passthrough
 | |
| 	get_xr_viewport().use_xr = true
 | |
| 
 | |
| 	# Report success
 | |
| 	return true
 | |
| 
 | |
| 
 | |
| # Handle OpenXR session ready
 | |
| func _on_openxr_session_begun() -> void:
 | |
| 	print("OpenXR: Session begun")
 | |
| 
 | |
| 	# Set the XR frame rate
 | |
| 	_set_xr_frame_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
 | |
| 		xr_ended.emit()
 | |
| 
 | |
| 
 | |
| # 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
 | |
| 		xr_started.emit()
 | |
| 
 | |
| 
 | |
| # 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()
 | |
| 
 | |
| 		# Update transparent background
 | |
| 		get_xr_viewport().transparent_bg = enable_passthrough
 | |
| 
 | |
| 
 | |
| # 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)
 | |
| 
 | |
| 	# If the viewport is already in XR mode then we are done.
 | |
| 	if get_xr_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.
 | |
| 	_webxr_session_query = true
 | |
| 	xr_interface.is_session_supported('immersive-ar' if enable_passthrough else 'immersive-vr')
 | |
| 
 | |
| 	# Report success
 | |
| 	return true
 | |
| 
 | |
| 
 | |
| # Handle WebXR session supported check
 | |
| func _on_webxr_session_supported(session_mode: String, supported: bool) -> void:
 | |
| 	# Skip if not running session-query
 | |
| 	if not _webxr_session_query:
 | |
| 		return
 | |
| 
 | |
| 	# Clear the query flag
 | |
| 	_webxr_session_query = false
 | |
| 
 | |
| 	# Report if not supported
 | |
| 	if not supported:
 | |
| 		OS.alert("Your web browser doesn't support " + session_mode + ". Sorry!")
 | |
| 		xr_failed_to_initialize.emit()
 | |
| 		return
 | |
| 
 | |
| 	# WebXR supported - show canvas on web browser to enter WebVR
 | |
| 	$EnterWebXR.visible = true
 | |
| 
 | |
| 
 | |
| # Called when the WebXR session has started successfully
 | |
| func _on_webxr_session_started() -> void:
 | |
| 	print("WebXR: Session started")
 | |
| 
 | |
| 	# Set the XR frame rate
 | |
| 	_set_xr_frame_rate()
 | |
| 
 | |
| 	# Hide the canvas and switch the viewport to XR
 | |
| 	$EnterWebXR.visible = false
 | |
| 	get_xr_viewport().transparent_bg = enable_passthrough
 | |
| 	get_xr_viewport().use_xr = true
 | |
| 
 | |
| 	# Report the XR starting
 | |
| 	_xr_active = true
 | |
| 	xr_started.emit()
 | |
| 
 | |
| 
 | |
| # 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_xr_viewport().transparent_bg = false
 | |
| 	get_xr_viewport().use_xr = false
 | |
| 
 | |
| 	# Report the XR ending
 | |
| 	_xr_active = false
 | |
| 	xr_ended.emit()
 | |
| 
 | |
| 
 | |
| # 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-ar' if enable_passthrough else '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'
 | |
| 
 | |
| 	# Add hand-tracking if enabled in the project settings
 | |
| 	if ProjectSettings.get_setting_with_override("xr/openxr/extensions/hand_tracking"):
 | |
| 		xr_interface.optional_features += ", hand-tracking"
 | |
| 
 | |
| 	# 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")
 | |
| 
 | |
| 
 | |
| # Set the XR frame rate to the configured value
 | |
| func _set_xr_frame_rate() -> void:
 | |
| 	# Get the reported refresh rate
 | |
| 	xr_frame_rate = xr_interface.get_display_refresh_rate()
 | |
| 	if xr_frame_rate > 0:
 | |
| 		print("StartXR: Refresh rate reported as ", str(xr_frame_rate))
 | |
| 	else:
 | |
| 		print("StartXR: No refresh rate given by XR runtime")
 | |
| 
 | |
| 	# Pick a desired refresh rate
 | |
| 	var desired_rate := target_refresh_rate if target_refresh_rate > 0 else xr_frame_rate
 | |
| 	var available_rates : Array = xr_interface.get_available_display_refresh_rates()
 | |
| 	if available_rates.size() == 0:
 | |
| 		print("StartXR: Target does not support refresh rate extension")
 | |
| 	elif available_rates.size() == 1:
 | |
| 		print("StartXR: Target supports only one refresh rate")
 | |
| 	elif desired_rate > 0:
 | |
| 		print("StartXR: Available refresh rates are ", str(available_rates))
 | |
| 		var rate = _find_closest(available_rates, desired_rate)
 | |
| 		if rate > 0:
 | |
| 			print("StartXR: Setting refresh rate to ", str(rate))
 | |
| 			xr_interface.set_display_refresh_rate(rate)
 | |
| 			xr_frame_rate = rate
 | |
| 
 | |
| 	# Pick a physics rate
 | |
| 	var active_rate := xr_frame_rate if xr_frame_rate > 0 else 144.0
 | |
| 	var physics_rate := int(round(active_rate * physics_rate_multiplier))
 | |
| 	print("StartXR: Setting physics rate to ", physics_rate)
 | |
| 	Engine.physics_ticks_per_second = physics_rate
 | |
| 
 | |
| 
 | |
| # 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
 |