From 32584e5a2ea01f4d18cc27e148ea32caa1a42a5e Mon Sep 17 00:00:00 2001 From: Bastiaan Olij Date: Sat, 13 Jan 2024 23:33:04 +1100 Subject: [PATCH] Add a chapter to the XR manual that improves the startup script for OpenXR --- tutorials/xr/a_better_xr_start_script.rst | 482 ++++++++++++++++++++++ tutorials/xr/index.rst | 1 + 2 files changed, 483 insertions(+) create mode 100644 tutorials/xr/a_better_xr_start_script.rst diff --git a/tutorials/xr/a_better_xr_start_script.rst b/tutorials/xr/a_better_xr_start_script.rst new file mode 100644 index 00000000000..7f8aeaecbd0 --- /dev/null +++ b/tutorials/xr/a_better_xr_start_script.rst @@ -0,0 +1,482 @@ +.. _doc_a_better_xr_start_script: + +A better XR start script +======================== + +In :ref:`doc_setting_up_xr` we introduced a startup script that initialises our setup which we used as our script on our main node. +This script performs the minimum steps required for any given interface. + +When using OpenXR there are a number of improvements we should do here. +For this we've created a more elaborate starting script. +You will find these used in our demo projects. + +Alternatively, if you are using XR Tools (see :ref:`doc_introducing_xr_tools`) it contains a version of this script updated with some features related to XR tools. + +Below we will detail out the script used in our demos and explain the parts that are added. + +Signals for our script +---------------------- + +We are introducing 3 signals to our script so that our game can add further logic: + +- ``focus_lost`` is emitted when the player takes off their headset or when the player enters the menu system of the headset. +- ``focus_gained`` is emitted when the player puts their headset back on or exists the menu system and returns to the game. +- ``pose_recentered`` is emitted when the headset requests the players position to be reset. + +Our game should react accordingly to these signals. + +.. tabs:: + .. code-tab:: gdscript GDScript + + extends Node3D + + signal focus_lost + signal focus_gained + signal pose_recentered + + ... + + .. code-tab:: csharp + + using Godot; + + public partial class MyNode3D : Node3D + { + [Signal] + public delegate void FocusLostEventHandler(); + + [Signal] + public delegate void FocusGainedEventHandler(); + + [Signal] + public delegate void PoseRecenteredEventHandler(); + + ... + + +Variables for our script +------------------------ + +We introduce a few new variables to our script as well: + +- ``maximum_refresh_rate`` will control the headsets refresh rate if this is supported by the headset. +- ``xr_interface`` holds a reference to our XR interface, this already existed but we now type it to get full access to our :ref:`XRInterface ` API. +- ``xr_is_focussed`` will be set to true whenever our game has focus. + +.. tabs:: + .. code-tab:: gdscript GDScript + + ... + + @export var maximum_refresh_rate : int = 90 + + var xr_interface : OpenXRInterface + var xr_is_focussed = false + + ... + + .. code-tab:: csharp + + ... + + [Export] + public int MaximumRefreshRate { get; set; } = 90; + + private OpenXRInterface _xrInterface; + + private bool _xrIsFocused; + + ... + +Our updated ready function +-------------------------- + +The ready function mostly remains the same but we hook up a number of signals that will be emitted by the :ref:`XRInterface `. +We'll provide more detail about these signals as we implement them. + +We also quit our application if we couldn't successfully initialise OpenXR. +Now this can be a choice. +If you are making a mixed mode game you setup the VR mode of your game on success, +and setup the non-VR mode of your game on failure. +However, when running a VR only application on a standalone headset, +it is nicer to exit on failure than to hang the system. + +.. tabs:: + .. code-tab:: gdscript GDScript + + ... + + # Called when the node enters the scene tree for the first time. + func _ready(): + xr_interface = XRServer.find_interface("OpenXR") + if xr_interface and xr_interface.is_initialized(): + print("OpenXR instantiated successfully.") + var vp : Viewport = get_viewport() + + # Enable XR on our viewport + vp.use_xr = true + + # Make sure v-sync is off, v-sync is handled by OpenXR + DisplayServer.window_set_vsync_mode(DisplayServer.VSYNC_DISABLED) + + # 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) + xr_interface.connect("session_stopping", _on_openxr_stopping) + xr_interface.connect("pose_recentered", _on_openxr_pose_recentered) + else: + # We couldn't start OpenXR. + print("OpenXR not instantiated!") + get_tree().quit() + + ... + + .. code-tab:: csharp + + ... + + /// + /// Called when the node enters the scene tree for the first time. + /// + public override void _Ready() + { + _xrInterface = (OpenXRInterface)XRServer.FindInterface("OpenXR"); + if (_xrInterface != null && _xrInterface.IsInitialized()) + { + GD.Print("OpenXR instantiated successfully."); + var vp = GetViewport(); + + // Enable XR on our viewport + vp.UseXR = true; + + // Make sure v-sync is off, v-sync is handled by OpenXR + DisplayServer.WindowSetVsyncMode(DisplayServer.VSyncMode.Disabled); + + // Connect the OpenXR events + _xrInterface.SessionBegun += OnOpenXRSessionBegun; + _xrInterface.SessionVisible += OnOpenXRVisibleState; + _xrInterface.SessionFocussed += OnOpenXRFocusedState; + _xrInterface.SessionStopping += OnOpenXRStopping; + _xrInterface.PoseRecentered += OnOpenXRPoseRecentered; + } + else + { + // We couldn't start OpenXR. + GD.Print("OpenXR not instantiated!"); + GetTree().Quit(); + } + } + + ... + + +On session begun +---------------- + +This signal is emitted by OpenXR when our session is setup. +This means the headset has run through setting everything up and is ready to begin receiving content from us. +Only at this time various information is properly available. + +The main thing we do here is to check our headsets refresh rate. +We also check the available refresh rates reported by the XR runtime to determine if we want to set our headset to a higher refresh rate. + +Finally we match our physics update rate to our headset update rate. +Godot runs at a physics update rate of 60 updates per second by default while headsets run at a minimum of 72, +and for modern headsets often up to 144 frames per second. +Not matching the physics update rate will cause stuttering as frames are rendered without objects moving. + +.. tabs:: + .. code-tab:: gdscript GDScript + + ... + + # Handle OpenXR session ready + func _on_openxr_session_begun() -> void: + # Get the reported refresh rate + var 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") + + # See if we have a better refresh rate available + var new_rate = 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: + # Only one available, so use it + new_rate = available_rates[0] + else: + for rate in available_rates: + if rate > new_rate and rate <= maximum_refresh_rate: + new_rate = rate + + # Did we find a better rate? + if current_refresh_rate != new_rate: + print("OpenXR: Setting refresh rate to ", str(new_rate)) + xr_interface.set_display_refresh_rate(new_rate) + current_refresh_rate = new_rate + + # Now match our physics rate + Engine.physics_ticks_per_second = current_refresh_rate + + ... + + .. code-tab:: csharp + + ... + + /// + /// Handle OpenXR session ready + /// + private void OnOpenXRSessionBegun() + { + // Get the reported refresh rate + var currentRefreshRate = _xrInterface.DisplayRefreshRate; + GD.Print(currentRefreshRate > 0.0F + ? $"OpenXR: Refresh rate reported as {currentRefreshRate}" + : "OpenXR: No refresh rate given by XR runtime"); + + // See if we have a better refresh rate available + var newRate = currentRefreshRate; + var availableRates = _xrInterface.GetAvailableDisplayRefreshRates(); + if (availableRates.Count == 0) + { + GD.Print("OpenXR: Target does not support refresh rate extension"); + } + else if (availableRates.Count == 1) + { + // Only one available, so use it + newRate = (float)availableRates[0]; + } + else + { + GD.Print("OpenXR: Available refresh rates: ", availableRates); + foreach (float rate in availableRates) + if (rate > newRate && rate <= MaximumRefreshRate) + newRate = rate; + } + + // Did we find a better rate? + if (currentRefreshRate != newRate) + { + GD.Print($"OpenXR: Setting refresh rate to {newRate}"); + _xrInterface.DisplayRefreshRate = newRate; + currentRefreshRate = newRate; + } + + // Now match our physics rate + Engine.PhysicsTicksPerSecond = (int)currentRefreshRate; + } + + ... + +On visible state +---------------- + +This signal is emitted by OpenXR when our game becomes visible but is not focussed. +This is a bit of a weird description in OpenXR but it basically means that our game has just started +and we're about to switch to the focussed state next, +that the user has opened a system menu or the users has just took their headset off. + +On receiving this signal we'll update our focussed state, +we'll change the process mode of our node to disabled which will pause processing on this node and it's children, +and emit our ``focus_lost`` signal. + +If you've added this script to your root node, +this means your game will automatically pause when required. +If you haven't, you can connect a method to the signal that performs additional changes. + +.. note:: + + While your game is in visible state because the user has opened a system menu, + Godot will keep rendering frames and head tracking will remain active so your game will remain visible in the background. + However controller and hand tracking will be disabled until the user exits the system menu. + +.. tabs:: + .. code-tab:: gdscript GDScript + + ... + + # Handle OpenXR visible state + func _on_openxr_visible_state() -> void: + # We always pass this state at startup, + # but the second time we get this it means our player took off their headset + if xr_is_focussed: + print("OpenXR lost focus") + + xr_is_focussed = false + + # pause our game + process_mode = Node.PROCESS_MODE_DISABLED + + emit_signal("focus_lost") + + ... + + .. code-tab:: csharp + + ... + + /// + /// Handle OpenXR visible state + /// + private void OnOpenXRVisibleState() + { + // We always pass this state at startup, + // but the second time we get this it means our player took off their headset + if (_xrIsFocused) + { + GD.Print("OpenXR lost focus"); + + _xrIsFocused = false; + + // Pause our game + ProcessMode = ProcessModeEnum.Disabled; + + EmitSignal(SignalName.FocusLost); + } + } + + ... + +On focussed state +----------------- + +This signal is emitted by OpenXR when our game gets focus. +This is done at the completion of our startup, +but it can also be emitted when the user exits a system menu, or put their headset back on. + +Note also that when your game starts while the user is not wearing their headset, +the game stays in 'visible' state until the user puts their headset on. + +.. warning:: + + It is thus important to keep your game paused while in visible mode. + If you don't the game will keep on running while your user isn't interacting with your game. + Also when the game returns to focussed mode, + suddenly all controller and hand tracking is re-enabled and could have game breaking consequences + if you do not react to this accordingly. + Be sure to test this behaviour in your game! + +While handling our signal we will update the focusses state, unpause our node and emit our ``focus_gained`` signal. + +.. tabs:: + .. code-tab:: gdscript GDScript + + ... + + # Handle OpenXR focused state + func _on_openxr_focused_state() -> void: + print("OpenXR gained focus") + xr_is_focussed = true + + # unpause our game + process_mode = Node.PROCESS_MODE_INHERIT + + emit_signal("focus_gained") + + ... + + .. code-tab:: csharp + + ... + + /// + /// Handle OpenXR focused state + /// + private void OnOpenXRFocusedState() + { + GD.Print("OpenXR gained focus"); + _xrIsFocused = true; + + // Un-pause our game + ProcessMode = ProcessModeEnum.Inherit; + + EmitSignal(SignalName.FocusGained); + } + + ... + +On stopping state +----------------- + +This signal is emitted by OpenXR when we enter our stop state. +There are some differences between platforms when this happens. +On some platforms this is only emitted when the game is being closed. +But on other platforms this will also be emitted every time the player takes off their headset. + +For now this method is only a place holder. + +.. tabs:: + .. code-tab:: gdscript GDScript + + ... + + # Handle OpenXR stopping state + func _on_openxr_stopping() -> void: + # Our session is being stopped. + print("OpenXR is stopping") + + ... + + .. code-tab:: csharp + + ... + + /// + /// Handle OpenXR stopping state + /// + private void OnOpenXRStopping() + { + // Our session is being stopped. + GD.Print("OpenXR is stopping"); + } + + ... + + +On pose recentered +------------------ + +This signal is emitted by OpenXR when the user requests their view to be recentered. +Basically this communicates to your game that the user is now facing forward +and you should re-orient the player so they are facing forward in the virtual world. + +As doing so is dependent on your game, your game needs to react accordingly. + +All we do here is emit the ``pose_recentered`` signal. +You can connect to this signal and implement the actual recenter code. +Often it is enough to call :ref:`center_on_hmd() `. + +.. tabs:: + .. code-tab:: gdscript GDScript + + ... + + # Handle OpenXR pose recentered signal + func _on_openxr_pose_recentered() -> void: + # User recentered view, we have to react to this by recentering the view. + # This is game implementation dependent. + emit_signal("pose_recentered") + + .. code-tab:: csharp + + ... + + /// + /// Handle OpenXR pose recentered signal + /// + private void OnOpenXRPoseRecentered() + { + // User recentered view, we have to react to this by recentering the view. + // This is game implementation dependent. + EmitSignal(SignalName.PoseRecentered); + } + } + +And that finished our script. It was written so that it can be re-used over multiple projects. +Just add it as the script on your main node (and extend it if needed) +or add it on a child node specific for this script. diff --git a/tutorials/xr/index.rst b/tutorials/xr/index.rst index a48450019df..2268eee88a9 100644 --- a/tutorials/xr/index.rst +++ b/tutorials/xr/index.rst @@ -10,6 +10,7 @@ Basic Tutorial setting_up_xr deploying_to_android + a_better_xr_start_script introducing_xr_tools basic_xr_locomotion