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..39f9b218
--- /dev/null
+++ b/addons/godot-xr-tools/xr/start_xr.gd
@@ -0,0 +1,279 @@
+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 : int = 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:
+ return _setup_for_openxr()
+
+ # Check for WebXR interface
+ xr_interface = ARVRServer.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_warning():
+ if physics_rate_multiplier < 1:
+ return "Physics rate multiplier should be at least 1x the HMD rate"
+
+ return ""
+
+
+# Perform OpenXR setup
+func _setup_for_openxr() -> bool:
+ 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 false
+
+ # Create the OpenXR configuration class
+ _openxr_configuration = openxr_config_res.new()
+
+ # Initialize the OpenXR interface
+ if not xr_interface.interface_is_initialized:
+ print("OpenXR: Initializing interface")
+ if not xr_interface.initialize():
+ push_error("OpenXR: Failed to initialize")
+ return false
+
+ # 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
+
+ # Report success
+ return 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(round(refresh_rate * physics_rate_multiplier))
+ 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() -> bool:
+ 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")
+
+ # 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().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"]