更好的 XR 启动脚本

设置 XR 中,我们介绍了一个用于初始化配置的启动脚本,并将其作为主节点脚本使用,以执行任何接口部署所需的最小步骤。

使用 OpenXR 时,这个脚本最好进行一些改进。为此,我们重新编写了一个更为详尽的启动脚本。你可以在演示项目中找到它。

除此以外,如果你使用 XR 工具(见 XR 工具简介),它也包含了另一个版本的启动脚本,那个版本在源代码基础上添加了一些与 XR 工具相关联的功能。

下面将详细介绍演示中使用的脚本,并解释添加的部分。

脚本的信号

我们在脚本中引入了 3 个信号以方便在游戏中添加更多逻辑:

  • focus_lost 作为检测玩家摘下头戴设备或进入头戴设备的菜单系统时的触发器。

  • focus_gained 信号则相反,在玩家重新戴上头戴设备或退出菜单系统并返回游戏时触发。

  • pose_recentered 信号在头戴设备请求重置玩家位置时触发。

我们的游戏将根据这些信号作出相应的反应。

GDScriptC#

  1. extends Node3D
  2. signal focus_lost
  3. signal focus_gained
  4. signal pose_recentered
  5. ...
  1. using Godot;
  2. public partial class MyNode3D : Node3D
  3. {
  4. [Signal]
  5. public delegate void FocusLostEventHandler();
  6. [Signal]
  7. public delegate void FocusGainedEventHandler();
  8. [Signal]
  9. public delegate void PoseRecenteredEventHandler();
  10. ...

脚本的变量

我们还向脚本引入了几个新变量:

  • maximum_refresh_rate 将控制头显设备的刷新率——如果头显设备支持控制的话。

  • xr_interface 保存了对我们的 XR 接口的引用,这个变量其实已经存在,但现在我们将其类型化,以便更好地访问 XRInterface API。

  • xr_is_focussed 将在我们的游戏获得焦点时设置为 true。

GDScriptC#

  1. ...
  2. @export var maximum_refresh_rate : int = 90
  3. var xr_interface : OpenXRInterface
  4. var xr_is_focussed = false
  5. ...
  1. ...
  2. [Export]
  3. public int MaximumRefreshRate { get; set; } = 90;
  4. private OpenXRInterface _xrInterface;
  5. private bool _xrIsFocused;
  6. ...

更新后的 _ready 函数

我们在 _ready 函数中新加了一些东西。

如果我们使用移动或 Forward+ 渲染器,我们可以将 viewports 的 vrs_mode 设置为 VRS_XR 。在支持此功能的平台上,这样设置将启用锥形渲染。

使用兼容性渲染器时,Godot 会检查是否配置了 OpenXR 的锥形渲染设置,如果没有进行配置,将弹出警告。详请参阅 OpenXR Settings

这些信号将由 XRInterface 触发。随着实现的深入,后续将提供更多关于这些信号的详细信息。

如果我们无法顺利启动 OpenXR ,我们也会选择退出应用。对于混合现实游戏的开发来说,你可以在成功初始化后进入 VR 模式,若失败再切换至非 VR 模式。不过,在一个独立的 VR 设备上运行仅支持 VR 的应用,启动失败时直接退出程序会比让系统挂着更合适。

GDScriptC#

  1. ...
  2. # Called when the node enters the scene tree for the first time.
  3. func _ready():
  4. xr_interface = XRServer.find_interface("OpenXR")
  5. if xr_interface and xr_interface.is_initialized():
  6. print("OpenXR instantiated successfully.")
  7. var vp : Viewport = get_viewport()
  8. # Enable XR on our viewport
  9. vp.use_xr = true
  10. # Make sure v-sync is off, v-sync is handled by OpenXR
  11. DisplayServer.window_set_vsync_mode(DisplayServer.VSYNC_DISABLED)
  12. # Enable VRS
  13. if RenderingServer.get_rendering_device():
  14. vp.vrs_mode = Viewport.VRS_XR
  15. elif int(ProjectSettings.get_setting("xr/openxr/foveation_level")) == 0:
  16. push_warning("OpenXR: Recommend setting Foveation level to High in Project Settings")
  17. # Connect the OpenXR events
  18. xr_interface.session_begun.connect(_on_openxr_session_begun)
  19. xr_interface.session_visible.connect(_on_openxr_visible_state)
  20. xr_interface.session_focussed.connect(_on_openxr_focused_state)
  21. xr_interface.session_stopping.connect(_on_openxr_stopping)
  22. xr_interface.pose_recentered.connect(_on_openxr_pose_recentered)
  23. else:
  24. # We couldn't start OpenXR.
  25. print("OpenXR not instantiated!")
  26. get_tree().quit()
  27. ...
  1. ...
  2. /// <summary>
  3. /// Called when the node enters the scene tree for the first time.
  4. /// </summary>
  5. public override void _Ready()
  6. {
  7. _xrInterface = (OpenXRInterface)XRServer.FindInterface("OpenXR");
  8. if (_xrInterface != null && _xrInterface.IsInitialized())
  9. {
  10. GD.Print("OpenXR instantiated successfully.");
  11. var vp = GetViewport();
  12. // Enable XR on our viewport
  13. vp.UseXR = true;
  14. // Make sure v-sync is off, v-sync is handled by OpenXR
  15. DisplayServer.WindowSetVsyncMode(DisplayServer.VSyncMode.Disabled);
  16. // Enable VRS
  17. if (RenderingServer.GetRenderingDevice() != null)
  18. vp.VrsMode = Viewport.VrsModeEnum.XR;
  19. else if ((int)ProjectSettings.GetSetting("xr/openxr/foveation_level") == 0)
  20. GD.PushWarning("OpenXR: Recommend setting Foveation level to High in Project Settings");
  21. // Connect the OpenXR events
  22. _xrInterface.SessionBegun += OnOpenXRSessionBegun;
  23. _xrInterface.SessionVisible += OnOpenXRVisibleState;
  24. _xrInterface.SessionFocussed += OnOpenXRFocusedState;
  25. _xrInterface.SessionStopping += OnOpenXRStopping;
  26. _xrInterface.PoseRecentered += OnOpenXRPoseRecentered;
  27. }
  28. else
  29. {
  30. // We couldn't start OpenXR.
  31. GD.Print("OpenXR not instantiated!");
  32. GetTree().Quit();
  33. }
  34. }
  35. ...

会话开始

该信号由 OpenXR 在我们设置会话时发出。意味着头戴设备已经完成了所有设置,并准备好开始接收程序内容。只有此时,各种信息才能正确地获取到。

在这里,我们主要做的事情是检查头戴设备的刷新率。除此以外还检查 XR 运行时报告的可用刷新率,以确定是否要将头戴设备设置为更高的刷新率。

最后,我们将物理更新速率与头戴设备的更新速率相匹配。Godot 默认物理帧刷新率为每秒 60 帧,而 HMD 通常至少以每秒 72 帧运行,当下先进的头戴设备甚至高达 144 帧 / 秒。如果不将物理帧刷新率相匹配,将导致设备在对象尚未移动前过早开始渲染,导致画面出现卡顿。

GDScriptC#

  1. ...
  2. # Handle OpenXR session ready
  3. func _on_openxr_session_begun() -> void:
  4. # Get the reported refresh rate
  5. var current_refresh_rate = xr_interface.get_display_refresh_rate()
  6. if current_refresh_rate > 0:
  7. print("OpenXR: Refresh rate reported as ", str(current_refresh_rate))
  8. else:
  9. print("OpenXR: No refresh rate given by XR runtime")
  10. # See if we have a better refresh rate available
  11. var new_rate = current_refresh_rate
  12. var available_rates : Array = xr_interface.get_available_display_refresh_rates()
  13. if available_rates.size() == 0:
  14. print("OpenXR: Target does not support refresh rate extension")
  15. elif available_rates.size() == 1:
  16. # Only one available, so use it
  17. new_rate = available_rates[0]
  18. else:
  19. for rate in available_rates:
  20. if rate > new_rate and rate <= maximum_refresh_rate:
  21. new_rate = rate
  22. # Did we find a better rate?
  23. if current_refresh_rate != new_rate:
  24. print("OpenXR: Setting refresh rate to ", str(new_rate))
  25. xr_interface.set_display_refresh_rate(new_rate)
  26. current_refresh_rate = new_rate
  27. # Now match our physics rate
  28. Engine.physics_ticks_per_second = current_refresh_rate
  29. ...
  1. ...
  2. /// <summary>
  3. /// Handle OpenXR session ready
  4. /// </summary>
  5. private void OnOpenXRSessionBegun()
  6. {
  7. // Get the reported refresh rate
  8. var currentRefreshRate = _xrInterface.DisplayRefreshRate;
  9. GD.Print(currentRefreshRate > 0.0F
  10. ? $"OpenXR: Refresh rate reported as {currentRefreshRate}"
  11. : "OpenXR: No refresh rate given by XR runtime");
  12. // See if we have a better refresh rate available
  13. var newRate = currentRefreshRate;
  14. var availableRates = _xrInterface.GetAvailableDisplayRefreshRates();
  15. if (availableRates.Count == 0)
  16. {
  17. GD.Print("OpenXR: Target does not support refresh rate extension");
  18. }
  19. else if (availableRates.Count == 1)
  20. {
  21. // Only one available, so use it
  22. newRate = (float)availableRates[0];
  23. }
  24. else
  25. {
  26. GD.Print("OpenXR: Available refresh rates: ", availableRates);
  27. foreach (float rate in availableRates)
  28. if (rate > newRate && rate <= MaximumRefreshRate)
  29. newRate = rate;
  30. }
  31. // Did we find a better rate?
  32. if (currentRefreshRate != newRate)
  33. {
  34. GD.Print($"OpenXR: Setting refresh rate to {newRate}");
  35. _xrInterface.DisplayRefreshRate = newRate;
  36. currentRefreshRate = newRate;
  37. }
  38. // Now match our physics rate
  39. Engine.PhysicsTicksPerSecond = (int)currentRefreshRate;
  40. }
  41. ...

进入可见状态

当游戏变得可见但未检测到聚焦时,OpenXR 会发出这个信号。这一状态在 OpenXR 文档中的描述有些迷惑,不过基本上来说,它通常指游戏刚启动,用户打开了系统菜单或用户刚摘下头戴设备,即将切换到聚焦状态时。

收到此信号时,Godot 将更新聚焦状态,将并节点的处理模式更改为禁用,从而暂停该节点及其子节点的处理,然后发出 focus_lost 信号。

如果你将此脚本添加到根节点,这意味着你的游戏将在需要时自动暂停。如果没有,你可以将方法连接到该信号,以执行额外的更改。

备注

如果游戏是因当用户打开系统菜单而处于可见状态,Godot 会继续渲染帧并保持头部跟踪活跃,因此游戏会在后台保持可见。然而,控制器和手部跟踪将被禁用,直到用户退出系统菜单为止。

GDScriptC#

  1. ...
  2. # Handle OpenXR visible state
  3. func _on_openxr_visible_state() -> void:
  4. # We always pass this state at startup,
  5. # but the second time we get this it means our player took off their headset
  6. if xr_is_focussed:
  7. print("OpenXR lost focus")
  8. xr_is_focussed = false
  9. # pause our game
  10. process_mode = Node.PROCESS_MODE_DISABLED
  11. emit_signal("focus_lost")
  12. ...
  1. ...
  2. /// <summary>
  3. /// Handle OpenXR visible state
  4. /// </summary>
  5. private void OnOpenXRVisibleState()
  6. {
  7. // We always pass this state at startup,
  8. // but the second time we get this it means our player took off their headset
  9. if (_xrIsFocused)
  10. {
  11. GD.Print("OpenXR lost focus");
  12. _xrIsFocused = false;
  13. // Pause our game
  14. ProcessMode = ProcessModeEnum.Disabled;
  15. EmitSignal(SignalName.FocusLost);
  16. }
  17. }
  18. ...

进入聚焦状态

OpenXR 会在游戏获得聚焦时发出这个信号。这会在启动完成时触发,但也可能在用户退出系统菜单或重新戴上头戴设备时触发。

同时注意,当游戏在用户未佩戴头戴设备时启动,游戏会保持在可见状态,直到用户戴上头戴设备。

警告

因此,在可见模式下保持游戏暂停非常重要。如果不暂停,游戏会在用户未与游戏互动时继续运行。此外,当游戏返回到聚焦模式时,所有控制器和手部跟踪会突然重新启用,如果你没有对此作出相应反应,可能会导致游戏出现严重问题。一定要在游戏中测试这种行为!

在处理该信号时,Godot 将更新聚焦状态,解除节点的暂停,并发出 focus_gained 信号。

GDScriptC#

  1. ...
  2. # Handle OpenXR focused state
  3. func _on_openxr_focused_state() -> void:
  4. print("OpenXR gained focus")
  5. xr_is_focussed = true
  6. # unpause our game
  7. process_mode = Node.PROCESS_MODE_INHERIT
  8. emit_signal("focus_gained")
  9. ...
  1. ...
  2. /// <summary>
  3. /// Handle OpenXR focused state
  4. /// </summary>
  5. private void OnOpenXRFocusedState()
  6. {
  7. GD.Print("OpenXR gained focus");
  8. _xrIsFocused = true;
  9. // Un-pause our game
  10. ProcessMode = ProcessModeEnum.Inherit;
  11. EmitSignal(SignalName.FocusGained);
  12. }
  13. ...

进入停止状态

OpenXR 会在进入停止状态时发出这个信号。不同平台在该情况下的表现会有所不同。一部分平台只会在游戏关闭时发出此信号,另一部分在玩家摘下头戴设备时也会发出。

目前为止,该方法只充当一个占位符。

GDScriptC#

  1. ...
  2. # Handle OpenXR stopping state
  3. func _on_openxr_stopping() -> void:
  4. # Our session is being stopped.
  5. print("OpenXR is stopping")
  6. ...
  1. ...
  2. /// <summary>
  3. /// Handle OpenXR stopping state
  4. /// </summary>
  5. private void OnOpenXRStopping()
  6. {
  7. // Our session is being stopped.
  8. GD.Print("OpenXR is stopping");
  9. }
  10. ...

姿势重新居中

当用户请求重新定位视角时,OpenXR 会发出此信号。该信号主要用于告诉你的游戏:用户现在面朝前方,你应该重新定位玩家,使其在虚拟世界中面朝前方。

由于重新定位视角依赖于游戏设计,因此你的游戏需要被设计能正确地做出反应。

下面这段代码里,我们只是发出 pose_recentered 信号,并未提供用户重新定位的代码实现。你可以连接到这个信号并自行实现它。通常调用 center_on_hmd() 就足够了。

GDScriptC#

  1. ...
  2. # Handle OpenXR pose recentered signal
  3. func _on_openxr_pose_recentered() -> void:
  4. # User recentered view, we have to react to this by recentering the view.
  5. # This is game implementation dependent.
  6. emit_signal("pose_recentered")
  1. ...
  2. /// <summary>
  3. /// Handle OpenXR pose recentered signal
  4. /// </summary>
  5. private void OnOpenXRPoseRecentered()
  6. {
  7. // User recentered view, we have to react to this by recentering the view.
  8. // This is game implementation dependent.
  9. EmitSignal(SignalName.PoseRecentered);
  10. }
  11. }

这样就完成了我们的脚本。它被设计为能够重复利用。只需将它添加为主节点的脚本(如有需要还可以进行扩展),或者添加到专门用于此脚本的子节点上。