Skip to content

Commit

Permalink
Added initial WebXR entry UI and export target.
Browse files Browse the repository at this point in the history
Added thread support to handle resource queue loading.

Added new StartXR scene to manage starting the OpenXR or WebXR instance. Fixed loading_screen so it doesn't produce NANs when the headset is off for a while.

Fixed minor gdlint warnings.

Move ARVROrigin/camera/controllers into base staging scene. Move StartXR into base staging scene. Modified base staging scene to emit xr_started/xr_ended signals.

Synchronized with Godot 4 equivalent functionality and events.

Formatting fixes
  • Loading branch information
Malcolmnixon committed Jan 6, 2023
1 parent b2d0d30 commit 9f0cc85
Show file tree
Hide file tree
Showing 9 changed files with 393 additions and 35 deletions.
21 changes: 9 additions & 12 deletions addons/godot-xr-tools/staging/loading_screen.gd
Original file line number Diff line number Diff line change
Expand Up @@ -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()


Expand Down
14 changes: 14 additions & 0 deletions addons/godot-xr-tools/staging/staging.gd
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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")
24 changes: 22 additions & 2 deletions addons/godot-xr-tools/staging/staging.tscn
Original file line number Diff line number Diff line change
@@ -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 )
Expand All @@ -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"]
273 changes: 273 additions & 0 deletions addons/godot-xr-tools/xr/start_xr.gd
Original file line number Diff line number Diff line change
@@ -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")
22 changes: 22 additions & 0 deletions addons/godot-xr-tools/xr/start_xr.tscn
Original file line number Diff line number Diff line change
@@ -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"]
Loading

0 comments on commit 9f0cc85

Please sign in to comment.