123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504 |
- .. _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 exits 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 <class_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
- --------------------------
- We add a few things to the ready function.
- If we're using the mobile or forward+ renderer we set the viewports ``vrs_mode`` to ``VRS_XR``.
- On platforms that support this, this will enable foveated rendering.
- If we're using the compatibility renderer, we check if the OpenXR foveated rendering settings
- are configured and if not, we output a warning.
- See :ref:`OpenXR Settings <doc_openxr_settings>` for further details.
- We hook up a number of signals that will be emitted by the :ref:`XRInterface <class_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)
- # Enable VRS
- if RenderingServer.get_rendering_device():
- vp.vrs_mode = Viewport.VRS_XR
- elif int(ProjectSettings.get_setting("xr/openxr/foveation_level")) == 0:
- push_warning("OpenXR: Recommend setting Foveation level to High in Project Settings")
- # Connect the OpenXR events
- xr_interface.session_begun.connect(_on_openxr_session_begun)
- xr_interface.session_visible.connect(_on_openxr_visible_state)
- xr_interface.session_focussed.connect(_on_openxr_focused_state)
- xr_interface.session_stopping.connect(_on_openxr_stopping)
- xr_interface.pose_recentered.connect(_on_openxr_pose_recentered)
- else:
- # We couldn't start OpenXR.
- print("OpenXR not instantiated!")
- get_tree().quit()
- ...
- .. code-tab:: csharp
- ...
- /// <summary>
- /// Called when the node enters the scene tree for the first time.
- /// </summary>
- 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);
- // Enable VRS
- if (RenderingServer.GetRenderingDevice() != null)
- vp.VrsMode = Viewport.VrsModeEnum.XR;
- else if ((int)ProjectSettings.GetSetting("xr/openxr/foveation_level") == 0)
- GD.PushWarning("OpenXR: Recommend setting Foveation level to High in Project Settings");
- // 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
- ...
- /// <summary>
- /// Handle OpenXR session ready
- /// </summary>
- 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
- get_tree().paused = true
- emit_signal("focus_lost")
-
- ...
- .. code-tab:: csharp
- ...
- /// <summary>
- /// Handle OpenXR visible state
- /// </summary>
- 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
- GetTree().Paused = true;
- 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
- get_tree().paused = false
- emit_signal("focus_gained")
- ...
- .. code-tab:: csharp
- ...
- /// <summary>
- /// Handle OpenXR focused state
- /// </summary>
- private void OnOpenXRFocusedState()
- {
- GD.Print("OpenXR gained focus");
- _xrIsFocused = true;
- // Un-pause our game
- GetTree().Paused = false;
- 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
- ...
- /// <summary>
- /// Handle OpenXR stopping state
- /// </summary>
- 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() <class_XRServer_method_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
- ...
- /// <summary>
- /// Handle OpenXR pose recentered signal
- /// </summary>
- 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.
|