diff --git a/addons/godot-xr-tools/staging/loading_screen.gd b/addons/godot-xr-tools/staging/loading_screen.gd index a38232af..da0d99b2 100644 --- a/addons/godot-xr-tools/staging/loading_screen.gd +++ b/addons/godot-xr-tools/staging/loading_screen.gd @@ -68,26 +68,23 @@ func _process(delta): if !_camera: return - var camera_dir = _camera.global_transform.basis.z + # Get the camera direction (horizontal only) + var camera_dir := _camera.global_transform.basis.z camera_dir.y = 0.0 camera_dir = camera_dir.normalized() - var loading_screen_dir = global_transform.basis.z + # Get the loading screen direction + var loading_screen_dir := global_transform.basis.z - # Calculate the rotation-axis to rotate the screen in front of the camera - var cross = loading_screen_dir.cross(camera_dir) - if cross.is_equal_approx(Vector3.ZERO): + # Get the angle + var angle := loading_screen_dir.signed_angle_to(camera_dir, Vector3.UP) + if angle == 0: return - # Calculate the angle to rotate the screen in front of the camera - cross = cross.normalized() - var dot = loading_screen_dir.dot(camera_dir) - var angle = acos(dot) - # Do rotation based on the curve global_transform.basis = global_transform.basis.rotated( - cross, - follow_speed.interpolate_baked(angle / PI) * delta + Vector3.UP * sign(angle), + follow_speed.interpolate_baked(abs(angle) / PI) * delta ).orthonormalized() diff --git a/addons/godot-xr-tools/staging/staging.gd b/addons/godot-xr-tools/staging/staging.gd index 3c4e18d8..9322ffa7 100644 --- a/addons/godot-xr-tools/staging/staging.gd +++ b/addons/godot-xr-tools/staging/staging.gd @@ -40,6 +40,12 @@ signal scene_loaded(scene) ## New scene is now visible signal scene_visible(scene) +## XR interaction started +signal xr_started + +## XR interaction ended +signal xr_ended + ## Main scene file export (String, FILE, '*.tscn') var main_scene : String @@ -259,3 +265,11 @@ func _on_exit_to_main_menu(): func _on_load_scene(p_scene_path : String): load_scene(p_scene_path) + + +func _on_StartXR_xr_started(): + emit_signal("xr_started") + + +func _on_StartXR_xr_ended(): + emit_signal("xr_ended") diff --git a/addons/godot-xr-tools/staging/staging.tscn b/addons/godot-xr-tools/staging/staging.tscn index e69be8a5..307fa1f2 100644 --- a/addons/godot-xr-tools/staging/staging.tscn +++ b/addons/godot-xr-tools/staging/staging.tscn @@ -1,8 +1,10 @@ -[gd_scene load_steps=8 format=2] +[gd_scene load_steps=10 format=2] +[ext_resource path="res://addons/godot-xr-tools/misc/vr_common_shader_cache.tscn" type="PackedScene" id=1] [ext_resource path="res://addons/godot-xr-tools/staging/staging.gd" type="Script" id=2] [ext_resource path="res://addons/godot-xr-tools/staging/loading_screen.tscn" type="PackedScene" id=3] [ext_resource path="res://addons/godot-xr-tools/staging/fade.gdshader" type="Shader" id=4] +[ext_resource path="res://addons/godot-xr-tools/xr/start_xr.tscn" type="PackedScene" id=5] [sub_resource type="QuadMesh" id=4] custom_aabb = AABB( -5000, -5000, -5000, 10000, 10000, 10000 ) @@ -29,9 +31,27 @@ material/0 = SubResource( 3 ) environment = SubResource( 2 ) [node name="LoadingScreen" parent="." instance=ExtResource( 3 )] -splash_screen = null progress = 0.0 [node name="Scene" type="Spatial" parent="."] [node name="Tween" type="Tween" parent="."] + +[node name="ARVROrigin" type="ARVROrigin" parent="."] + +[node name="ARVRCamera" type="ARVRCamera" parent="ARVROrigin"] +transform = Transform( 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1.8, 0 ) + +[node name="VRCommonShaderCache" parent="ARVROrigin/ARVRCamera" instance=ExtResource( 1 )] + +[node name="LeftHand" type="ARVRController" parent="ARVROrigin"] +transform = Transform( 1, 0, 0, 0, 1, 0, 0, 0, 1, -0.5, 1, -0.5 ) + +[node name="RightHand" type="ARVRController" parent="ARVROrigin"] +transform = Transform( 1, 0, 0, 0, 1, 0, 0, 0, 1, 0.5, 1, -0.5 ) +controller_id = 2 + +[node name="StartXR" parent="." instance=ExtResource( 5 )] + +[connection signal="xr_ended" from="StartXR" to="." method="_on_StartXR_xr_ended"] +[connection signal="xr_started" from="StartXR" to="." method="_on_StartXR_xr_started"] diff --git a/addons/godot-xr-tools/xr/start_xr.gd b/addons/godot-xr-tools/xr/start_xr.gd new file mode 100644 index 00000000..c0bd743d --- /dev/null +++ b/addons/godot-xr-tools/xr/start_xr.gd @@ -0,0 +1,273 @@ +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 + +## If true, the XR passthrough is enabled (OpenXR only) +export var enable_passthrough : bool = false setget _set_enable_passthrough + +## Physics rate multiplier compared to HMD frame rate +export var physics_rate_multiplier : float = 1 + + +## Current XR interface +var xr_interface : ARVRInterface + +## XR active flag +var xr_active : bool = false + +# OpenXR configuration (of type OpenXRConfig.gdns) +var _openxr_configuration + +# OpenXR enabled extensions +var _openxr_enabled_extensions : Array + + +# Handle auto-initialization when ready +func _ready() -> void: + if !Engine.editor_hint and auto_initialize: + initialize() + + +## Initialize the XR interface +func initialize() -> bool: + # Check for OpenXR interface + xr_interface = ARVRServer.find_interface('OpenXR') + if xr_interface: + _setup_for_openxr() + return true + + # Check for WebXR interface + xr_interface = ARVRServer.find_interface('WebXR') + if xr_interface: + _setup_for_webxr() + return true + + # No XR interface + xr_interface = null + print("No XR interface detected") + return false + + +# Check for configuration issues +func _get_configuration_warning(): + if physics_rate_multiplier < 1.0: + return "Physics rate multiplier should be at least 1x the HMD rate" + + return "" + + +# Perform OpenXR setup +func _setup_for_openxr() -> void: + print("OpenXR: Configuring interface") + + # Load the OpenXR configuration resource + var openxr_config_res := load("res://addons/godot-openxr/config/OpenXRConfig.gdns") + if not openxr_config_res: + push_error("OpenXR: Unable to load OpenXRConfig.gdns") + return + + # Create the OpenXR configuration class + _openxr_configuration = openxr_config_res.new() + + # Initialize the OpenXR interface + if not xr_interface.initialize(): + push_error("OpenXR: Failed to initialize") + return + + # Connect the OpenXR events + ARVRServer.connect("openxr_session_begun", self, "_on_openxr_session_begun") + ARVRServer.connect("openxr_visible_state", self, "_on_openxr_visible_state") + ARVRServer.connect("openxr_focused_state", self, "_on_openxr_focused_state") + + # Read the OpenXR enabled extensions + _openxr_enabled_extensions = _openxr_configuration.get_enabled_extensions() + + # Check for passthrough + if enable_passthrough and _openxr_is_passthrough_supported(): + enable_passthrough = _openxr_start_passthrough() + + # Switch the viewport to XR + get_viewport().arvr = true + + +# Handle OpenXR session ready +func _on_openxr_session_begun() -> void: + print("OpenXR: Session begun") + + # Our interface will tell us whether we should keep our render buffer in linear color space + get_viewport().keep_3d_linear = _openxr_configuration.keep_3d_linear() + + # increase our physics engine update speed + var refresh_rate : float = _openxr_configuration.get_refresh_rate() + if refresh_rate > 0: + # Report provided frame rare + print("OpenXR: HMD refresh rate is set to ", str(refresh_rate)) + else: + # None provided, assume a standard rate + print("OpenXR: No refresh rate given by XR runtime") + refresh_rate = 144 + + # Pick a physics rate + var physics_rate := int(refresh_rate * physics_rate_multiplier + 0.5) + print("Setting physics rate to ", physics_rate) + Engine.iterations_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 and _openxr_configuration: + if enable_passthrough: + # unset enable_passthrough if we can't start it. + enable_passthrough = _openxr_start_passthrough() + else: + _openxr_stop_passthrough() + + +# Test if passthrough is supported +func _openxr_is_passthrough_supported() -> bool: + return _openxr_enabled_extensions.find("XR_FB_passthrough") >= 0 + + +# Start OpenXR passthrough +func _openxr_start_passthrough() -> bool: + # Set viewport transparent background + get_viewport().transparent_bg = true + + # Enable passthrough + return _openxr_configuration.start_passthrough() + + +# Stop OpenXR passthrough +func _openxr_stop_passthrough() -> void: + # Clear viewport transparent background + get_viewport().transparent_bg = false + + # Disable passthrough + _openxr_configuration.stop_passthrough() + + +# Perform WebXR setup +func _setup_for_webxr() -> void: + print("WebXR: Configuring interface") + + # Connect the WebXR events + xr_interface.connect("session_supported", self, "_on_webxr_session_supported") + xr_interface.connect("session_started", self, "_on_webxr_session_started") + xr_interface.connect("session_ended", self, "_on_webxr_session_ended") + xr_interface.connect("session_failed", self, "_on_webxr_session_failed") + + # WebXR currently has no means of querying the refresh rate, so use + # something sufficiently high + Engine.iterations_per_second = 144 + + # 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") + + +# 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().arvr = 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().arvr = 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' + xr_interface.xr_standard_mapping = true + + # 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") diff --git a/addons/godot-xr-tools/xr/start_xr.tscn b/addons/godot-xr-tools/xr/start_xr.tscn new file mode 100644 index 00000000..f600145d --- /dev/null +++ b/addons/godot-xr-tools/xr/start_xr.tscn @@ -0,0 +1,22 @@ +[gd_scene load_steps=2 format=2] + +[ext_resource path="res://addons/godot-xr-tools/xr/start_xr.gd" type="Script" id=1] + +[node name="StartXR" type="Node"] +script = ExtResource( 1 ) + +[node name="EnterWebXR" type="CanvasLayer" parent="."] +visible = false + +[node name="EnterVRButton" type="Button" parent="EnterWebXR"] +anchor_left = 0.5 +anchor_top = 0.5 +anchor_right = 0.5 +anchor_bottom = 0.5 +margin_left = -132.0 +margin_top = -52.5 +margin_right = 132.0 +margin_bottom = 52.5 +text = "Enter VR" + +[connection signal="pressed" from="EnterWebXR/EnterVRButton" to="." method="_on_enter_webxr_button_pressed"] diff --git a/export_presets.cfg b/export_presets.cfg index 54594e6d..62578984 100644 --- a/export_presets.cfg +++ b/export_presets.cfg @@ -271,3 +271,41 @@ permissions/write_sms=false permissions/write_social_stream=false permissions/write_sync_settings=false permissions/write_user_dictionary=false + +[preset.3] + +name="WebXR" +platform="HTML5" +runnable=true +custom_features="" +export_filter="all_resources" +include_filter="" +exclude_filter="" +export_path="" +script_export_mode=1 +script_encryption_key="" + +[preset.3.options] + +custom_template/debug="" +custom_template/release="" +variant/export_type=1 +vram_texture_compression/for_desktop=true +vram_texture_compression/for_mobile=true +html/export_icon=true +html/custom_html_shell="" +html/head_include=" +" +html/canvas_resize_policy=2 +html/focus_canvas_on_start=true +html/experimental_virtual_keyboard=false +progressive_web_app/enabled=false +progressive_web_app/offline_page="" +progressive_web_app/display=1 +progressive_web_app/orientation=0 +progressive_web_app/icon_144x144="" +progressive_web_app/icon_180x180="" +progressive_web_app/icon_512x512="" +progressive_web_app/background_color=Color( 0, 0, 0, 1 ) diff --git a/project.godot b/project.godot index 2a45ca9c..5a50dcea 100644 --- a/project.godot +++ b/project.godot @@ -269,6 +269,11 @@ _global_script_classes=[ { "language": "GDScript", "path": "res://addons/godot-xr-tools/staging/staging.gd" }, { +"base": "Node", +"class": "XRToolsStartXR", +"language": "GDScript", +"path": "res://addons/godot-xr-tools/xr/start_xr.gd" +}, { "base": "Reference", "class": "XRToolsVelocityAverager", "language": "GDScript", @@ -347,6 +352,7 @@ _global_script_class_icons={ "XRToolsSceneBase": "", "XRToolsSnapZone": "", "XRToolsStaging": "", +"XRToolsStartXR": "", "XRToolsVelocityAverager": "", "XRToolsVelocityAveragerLinear": "", "XRToolsVignette": "", diff --git a/staging.gd b/staging.gd index b7f5e173..2bcf35e6 100644 --- a/staging.gd +++ b/staging.gd @@ -22,7 +22,7 @@ extends XRToolsStaging # note that most XR runtimes stop giving us controller # tracking data at this point. -var scene_is_loaded = false +var scene_is_loaded : bool = false func _on_Staging_scene_loaded(_scene): # We only show the press to continue the first time we load a scene @@ -36,8 +36,8 @@ func _on_Staging_scene_exiting(_scene): scene_is_loaded = false -func _on_FPController_focused_state(): - # We get the focussed state when the user puts on their headset, +func _on_Staging_xr_started(): + # We get the 'xr_started' signal when the user puts on their headset, # or returns from the system menus. # If the user did so while we were already scene switching # we leave our prompt for continue on, @@ -49,9 +49,9 @@ func _on_FPController_focused_state(): # This would be a good moment to unpause your game -func _on_FPController_session_synchronized(): - # We get the synchronized state at startup and whenever the player - # removes their headset (or goes into the menu system). +func _on_Staging_xr_ended(): + # We get the 'xr_ended' whenever the player removes their headset (or goes + # into the menu system). # # If the user doesn't put their headset on again before we load a # new scene, we'll want to show the prompt so we don't load the @@ -61,4 +61,3 @@ func _on_FPController_session_synchronized(): if scene_is_loaded: # This would be a good moment to pause your game pass - diff --git a/staging.tscn b/staging.tscn index 8fd21a26..0cf57c05 100644 --- a/staging.tscn +++ b/staging.tscn @@ -1,10 +1,8 @@ -[gd_scene load_steps=6 format=2] +[gd_scene load_steps=4 format=2] [ext_resource path="res://addons/godot-xr-tools/staging/staging.tscn" type="PackedScene" id=1] -[ext_resource path="res://addons/godot-openxr/scenes/first_person_controller_vr.tscn" type="PackedScene" id=2] [ext_resource path="res://assets/godot/splash.png" type="Texture" id=3] [ext_resource path="res://staging.gd" type="Script" id=4] -[ext_resource path="res://addons/godot-xr-tools/misc/vr_common_shader_cache.tscn" type="PackedScene" id=9] [node name="Staging" instance=ExtResource( 1 )] script = ExtResource( 4 ) @@ -13,16 +11,7 @@ main_scene = "res://scenes/main_menu/main_menu_level.tscn" [node name="LoadingScreen" parent="." index="2"] splash_screen = ExtResource( 3 ) -[node name="FPController" parent="." index="5" instance=ExtResource( 2 )] - -[node name="ARVRCamera" parent="FPController" index="1"] -far = 1000.0 - -[node name="VRCommonShaderCache" parent="FPController/ARVRCamera" index="0" instance=ExtResource( 9 )] - [connection signal="scene_exiting" from="." to="." method="_on_Staging_scene_exiting"] [connection signal="scene_loaded" from="." to="." method="_on_Staging_scene_loaded"] -[connection signal="focused_state" from="FPController" to="." method="_on_FPController_focused_state"] -[connection signal="session_synchronized" from="FPController" to="." method="_on_FPController_session_synchronized"] - -[editable path="FPController"] +[connection signal="xr_ended" from="." to="." method="_on_Staging_xr_ended"] +[connection signal="xr_started" from="." to="." method="_on_Staging_xr_started"]