a_better_xr_start_script.rst 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512
  1. .. _doc_a_better_xr_start_script:
  2. A better XR start script
  3. ========================
  4. 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.
  5. This script performs the minimum steps required for any given interface.
  6. When using OpenXR there are a number of improvements we should do here.
  7. For this we've created a more elaborate starting script.
  8. You will find these used in our demo projects.
  9. 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.
  10. Below we will detail out the script used in our demos and explain the parts that are added.
  11. Signals for our script
  12. ----------------------
  13. We are introducing 3 signals to our script so that our game can add further logic:
  14. - ``focus_lost`` is emitted when the player takes off their headset or when the player enters the menu system of the headset.
  15. - ``focus_gained`` is emitted when the player puts their headset back on or exits the menu system and returns to the game.
  16. - ``pose_recentered`` is emitted when the headset requests the player's position to be reset.
  17. Our game should react accordingly to these signals.
  18. .. tabs::
  19. .. code-tab:: gdscript GDScript
  20. extends Node3D
  21. signal focus_lost
  22. signal focus_gained
  23. signal pose_recentered
  24. ...
  25. .. code-tab:: csharp
  26. using Godot;
  27. public partial class MyNode3D : Node3D
  28. {
  29. [Signal]
  30. public delegate void FocusLostEventHandler();
  31. [Signal]
  32. public delegate void FocusGainedEventHandler();
  33. [Signal]
  34. public delegate void PoseRecenteredEventHandler();
  35. ...
  36. Variables for our script
  37. ------------------------
  38. We introduce a few new variables to our script as well:
  39. - ``maximum_refresh_rate`` will control the headsets refresh rate if this is supported by the headset.
  40. - ``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.
  41. - ``xr_is_focussed`` will be set to true whenever our game has focus.
  42. .. tabs::
  43. .. code-tab:: gdscript GDScript
  44. ...
  45. @export var maximum_refresh_rate : int = 90
  46. var xr_interface : OpenXRInterface
  47. var xr_is_focussed = false
  48. ...
  49. .. code-tab:: csharp
  50. ...
  51. [Export]
  52. public int MaximumRefreshRate { get; set; } = 90;
  53. private OpenXRInterface _xrInterface;
  54. private bool _xrIsFocused;
  55. ...
  56. Our updated ready function
  57. --------------------------
  58. We add a few things to the ready function.
  59. If we're using the mobile or forward+ renderer we set the viewport's ``vrs_mode`` to ``VRS_XR``.
  60. On platforms that support this, this will enable foveated rendering.
  61. If we're using the compatibility renderer, we check if the OpenXR foveated rendering settings
  62. are configured and if not, we output a warning.
  63. See :ref:`OpenXR Settings <doc_openxr_settings>` for further details.
  64. We hook up a number of signals that will be emitted by the :ref:`XRInterface <class_xrinterface>`.
  65. We'll provide more detail about these signals as we implement them.
  66. We also quit our application if we couldn't successfully initialise OpenXR.
  67. Now this can be a choice.
  68. If you are making a mixed mode game you setup the VR mode of your game on success,
  69. and setup the non-VR mode of your game on failure.
  70. However, when running a VR only application on a standalone headset,
  71. it is nicer to exit on failure than to hang the system.
  72. .. tabs::
  73. .. code-tab:: gdscript GDScript
  74. ...
  75. # Called when the node enters the scene tree for the first time.
  76. func _ready():
  77. xr_interface = XRServer.find_interface("OpenXR")
  78. if xr_interface and xr_interface.is_initialized():
  79. print("OpenXR instantiated successfully.")
  80. var vp : Viewport = get_viewport()
  81. # Enable XR on our viewport
  82. vp.use_xr = true
  83. # Make sure v-sync is off, v-sync is handled by OpenXR
  84. DisplayServer.window_set_vsync_mode(DisplayServer.VSYNC_DISABLED)
  85. # Enable VRS
  86. if RenderingServer.get_rendering_device():
  87. vp.vrs_mode = Viewport.VRS_XR
  88. elif int(ProjectSettings.get_setting("xr/openxr/foveation_level")) == 0:
  89. push_warning("OpenXR: Recommend setting Foveation level to High in Project Settings")
  90. # Connect the OpenXR events
  91. xr_interface.session_begun.connect(_on_openxr_session_begun)
  92. xr_interface.session_visible.connect(_on_openxr_visible_state)
  93. xr_interface.session_focussed.connect(_on_openxr_focused_state)
  94. xr_interface.session_stopping.connect(_on_openxr_stopping)
  95. xr_interface.pose_recentered.connect(_on_openxr_pose_recentered)
  96. else:
  97. # We couldn't start OpenXR.
  98. print("OpenXR not instantiated!")
  99. get_tree().quit()
  100. ...
  101. .. code-tab:: csharp
  102. ...
  103. /// <summary>
  104. /// Called when the node enters the scene tree for the first time.
  105. /// </summary>
  106. public override void _Ready()
  107. {
  108. _xrInterface = (OpenXRInterface)XRServer.FindInterface("OpenXR");
  109. if (_xrInterface != null && _xrInterface.IsInitialized())
  110. {
  111. GD.Print("OpenXR instantiated successfully.");
  112. var vp = GetViewport();
  113. // Enable XR on our viewport
  114. vp.UseXR = true;
  115. // Make sure v-sync is off, v-sync is handled by OpenXR
  116. DisplayServer.WindowSetVsyncMode(DisplayServer.VSyncMode.Disabled);
  117. // Enable VRS
  118. if (RenderingServer.GetRenderingDevice() != null)
  119. {
  120. vp.VrsMode = Viewport.VrsModeEnum.XR;
  121. }
  122. else if ((int)ProjectSettings.GetSetting("xr/openxr/foveation_level") == 0)
  123. {
  124. GD.PushWarning("OpenXR: Recommend setting Foveation level to High in Project Settings");
  125. }
  126. // Connect the OpenXR events
  127. _xrInterface.SessionBegun += OnOpenXRSessionBegun;
  128. _xrInterface.SessionVisible += OnOpenXRVisibleState;
  129. _xrInterface.SessionFocussed += OnOpenXRFocusedState;
  130. _xrInterface.SessionStopping += OnOpenXRStopping;
  131. _xrInterface.PoseRecentered += OnOpenXRPoseRecentered;
  132. }
  133. else
  134. {
  135. // We couldn't start OpenXR.
  136. GD.Print("OpenXR not instantiated!");
  137. GetTree().Quit();
  138. }
  139. }
  140. ...
  141. On session begun
  142. ----------------
  143. This signal is emitted by OpenXR when our session is setup.
  144. This means the headset has run through setting everything up and is ready to begin receiving content from us.
  145. Only at this time various information is properly available.
  146. The main thing we do here is to check our headset's refresh rate.
  147. 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.
  148. Finally we match our physics update rate to our headset update rate.
  149. Godot runs at a physics update rate of 60 updates per second by default while headsets run at a minimum of 72,
  150. and for modern headsets often up to 144 frames per second.
  151. Not matching the physics update rate will cause stuttering as frames are rendered without objects moving.
  152. .. tabs::
  153. .. code-tab:: gdscript GDScript
  154. ...
  155. # Handle OpenXR session ready
  156. func _on_openxr_session_begun() -> void:
  157. # Get the reported refresh rate
  158. var current_refresh_rate = xr_interface.get_display_refresh_rate()
  159. if current_refresh_rate > 0:
  160. print("OpenXR: Refresh rate reported as ", str(current_refresh_rate))
  161. else:
  162. print("OpenXR: No refresh rate given by XR runtime")
  163. # See if we have a better refresh rate available
  164. var new_rate = current_refresh_rate
  165. var available_rates : Array = xr_interface.get_available_display_refresh_rates()
  166. if available_rates.size() == 0:
  167. print("OpenXR: Target does not support refresh rate extension")
  168. elif available_rates.size() == 1:
  169. # Only one available, so use it
  170. new_rate = available_rates[0]
  171. else:
  172. for rate in available_rates:
  173. if rate > new_rate and rate <= maximum_refresh_rate:
  174. new_rate = rate
  175. # Did we find a better rate?
  176. if current_refresh_rate != new_rate:
  177. print("OpenXR: Setting refresh rate to ", str(new_rate))
  178. xr_interface.set_display_refresh_rate(new_rate)
  179. current_refresh_rate = new_rate
  180. # Now match our physics rate
  181. Engine.physics_ticks_per_second = current_refresh_rate
  182. ...
  183. .. code-tab:: csharp
  184. ...
  185. /// <summary>
  186. /// Handle OpenXR session ready
  187. /// </summary>
  188. private void OnOpenXRSessionBegun()
  189. {
  190. // Get the reported refresh rate
  191. var currentRefreshRate = _xrInterface.DisplayRefreshRate;
  192. GD.Print(currentRefreshRate > 0.0F
  193. ? $"OpenXR: Refresh rate reported as {currentRefreshRate}"
  194. : "OpenXR: No refresh rate given by XR runtime");
  195. // See if we have a better refresh rate available
  196. var newRate = currentRefreshRate;
  197. var availableRates = _xrInterface.GetAvailableDisplayRefreshRates();
  198. if (availableRates.Count == 0)
  199. {
  200. GD.Print("OpenXR: Target does not support refresh rate extension");
  201. }
  202. else if (availableRates.Count == 1)
  203. {
  204. // Only one available, so use it
  205. newRate = (float)availableRates[0];
  206. }
  207. else
  208. {
  209. GD.Print("OpenXR: Available refresh rates: ", availableRates);
  210. foreach (float rate in availableRates)
  211. {
  212. if (rate > newRate && rate <= MaximumRefreshRate)
  213. {
  214. newRate = rate;
  215. }
  216. }
  217. }
  218. // Did we find a better rate?
  219. if (currentRefreshRate != newRate)
  220. {
  221. GD.Print($"OpenXR: Setting refresh rate to {newRate}");
  222. _xrInterface.DisplayRefreshRate = newRate;
  223. currentRefreshRate = newRate;
  224. }
  225. // Now match our physics rate
  226. Engine.PhysicsTicksPerSecond = (int)currentRefreshRate;
  227. }
  228. ...
  229. On visible state
  230. ----------------
  231. This signal is emitted by OpenXR when our game becomes visible but is not focused.
  232. This is a bit of a weird description in OpenXR but it basically means that our game has just started
  233. and we're about to switch to the focused state next,
  234. that the user has opened a system menu or the user has just took their headset off.
  235. On receiving this signal we'll update our focused state,
  236. we'll change the process mode of our node to disabled which will pause processing on this node and its children,
  237. and emit our ``focus_lost`` signal.
  238. If you've added this script to your root node,
  239. this means your game will automatically pause when required.
  240. If you haven't, you can connect a method to the signal that performs additional changes.
  241. .. note::
  242. While your game is in visible state because the user has opened a system menu,
  243. Godot will keep rendering frames and head tracking will remain active so your game will remain visible in the background.
  244. However controller and hand tracking will be disabled until the user exits the system menu.
  245. .. tabs::
  246. .. code-tab:: gdscript GDScript
  247. ...
  248. # Handle OpenXR visible state
  249. func _on_openxr_visible_state() -> void:
  250. # We always pass this state at startup,
  251. # but the second time we get this it means our player took off their headset
  252. if xr_is_focussed:
  253. print("OpenXR lost focus")
  254. xr_is_focussed = false
  255. # pause our game
  256. get_tree().paused = true
  257. emit_signal("focus_lost")
  258. ...
  259. .. code-tab:: csharp
  260. ...
  261. /// <summary>
  262. /// Handle OpenXR visible state
  263. /// </summary>
  264. private void OnOpenXRVisibleState()
  265. {
  266. // We always pass this state at startup,
  267. // but the second time we get this it means our player took off their headset
  268. if (_xrIsFocused)
  269. {
  270. GD.Print("OpenXR lost focus");
  271. _xrIsFocused = false;
  272. // Pause our game
  273. GetTree().Paused = true;
  274. EmitSignal(SignalName.FocusLost);
  275. }
  276. }
  277. ...
  278. On focussed state
  279. -----------------
  280. This signal is emitted by OpenXR when our game gets focus.
  281. This is done at the completion of our startup,
  282. but it can also be emitted when the user exits a system menu, or put their headset back on.
  283. Note also that when your game starts while the user is not wearing their headset,
  284. the game stays in 'visible' state until the user puts their headset on.
  285. .. warning::
  286. It is thus important to keep your game paused while in visible mode.
  287. If you don't the game will keep on running while your user isn't interacting with your game.
  288. Also when the game returns to the focused mode,
  289. suddenly all controller and hand tracking is re-enabled and could have game breaking consequences
  290. if you do not react to this accordingly.
  291. Be sure to test this behavior in your game!
  292. While handling our signal we will update the focuses state, unpause our node and emit our ``focus_gained`` signal.
  293. .. tabs::
  294. .. code-tab:: gdscript GDScript
  295. ...
  296. # Handle OpenXR focused state
  297. func _on_openxr_focused_state() -> void:
  298. print("OpenXR gained focus")
  299. xr_is_focussed = true
  300. # unpause our game
  301. get_tree().paused = false
  302. emit_signal("focus_gained")
  303. ...
  304. .. code-tab:: csharp
  305. ...
  306. /// <summary>
  307. /// Handle OpenXR focused state
  308. /// </summary>
  309. private void OnOpenXRFocusedState()
  310. {
  311. GD.Print("OpenXR gained focus");
  312. _xrIsFocused = true;
  313. // Un-pause our game
  314. GetTree().Paused = false;
  315. EmitSignal(SignalName.FocusGained);
  316. }
  317. ...
  318. On stopping state
  319. -----------------
  320. This signal is emitted by OpenXR when we enter our stop state.
  321. There are some differences between platforms when this happens.
  322. On some platforms this is only emitted when the game is being closed.
  323. But on other platforms this will also be emitted every time the player takes off their headset.
  324. For now this method is only a place holder.
  325. .. tabs::
  326. .. code-tab:: gdscript GDScript
  327. ...
  328. # Handle OpenXR stopping state
  329. func _on_openxr_stopping() -> void:
  330. # Our session is being stopped.
  331. print("OpenXR is stopping")
  332. ...
  333. .. code-tab:: csharp
  334. ...
  335. /// <summary>
  336. /// Handle OpenXR stopping state
  337. /// </summary>
  338. private void OnOpenXRStopping()
  339. {
  340. // Our session is being stopped.
  341. GD.Print("OpenXR is stopping");
  342. }
  343. ...
  344. On pose recentered
  345. ------------------
  346. This signal is emitted by OpenXR when the user requests their view to be recentered.
  347. Basically this communicates to your game that the user is now facing forward
  348. and you should re-orient the player so they are facing forward in the virtual world.
  349. As doing so is dependent on your game, your game needs to react accordingly.
  350. All we do here is emit the ``pose_recentered`` signal.
  351. You can connect to this signal and implement the actual recenter code.
  352. Often it is enough to call :ref:`center_on_hmd() <class_XRServer_method_center_on_hmd>`.
  353. .. tabs::
  354. .. code-tab:: gdscript GDScript
  355. ...
  356. # Handle OpenXR pose recentered signal
  357. func _on_openxr_pose_recentered() -> void:
  358. # User recentered view, we have to react to this by recentering the view.
  359. # This is game implementation dependent.
  360. emit_signal("pose_recentered")
  361. .. code-tab:: csharp
  362. ...
  363. /// <summary>
  364. /// Handle OpenXR pose recentered signal
  365. /// </summary>
  366. private void OnOpenXRPoseRecentered()
  367. {
  368. // User recentered view, we have to react to this by recentering the view.
  369. // This is game implementation dependent.
  370. EmitSignal(SignalName.PoseRecentered);
  371. }
  372. }
  373. And that finished our script. It was written so that it can be re-used over multiple projects.
  374. Just add it as the script on your main node (and extend it if needed)
  375. or add it on a child node specific for this script.