Attention: Here be dragons
This is the latest
(unstable) version of this documentation, which may document features
not available in or compatible with released stable versions of Godot.
Checking the stable version of the documentation...
更好的 XR 启动脚本
在 设置 XR 中,我们介绍了一个用于初始化配置的启动脚本,并将其作为主节点脚本使用,以执行任何接口部署所需的最小步骤。
使用 OpenXR 时,这个脚本最好进行一些改进。为此,我们重新编写了一个更为详尽的启动脚本。你可以在演示项目中找到它。
除此以外,如果你使用 XR 工具(见 XR 工具简介),它也包含了另一个版本的启动脚本,那个版本在源代码基础上添加了一些与 XR 工具相关联的功能。
下面将详细介绍演示中使用的脚本,并解释添加的部分。
脚本的信号
我们在脚本中引入了 3 个信号以方便在游戏中添加更多逻辑:
focus_lost
作为检测玩家摘下头戴设备或进入头戴设备的菜单系统时的触发器。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 player's position to be reset.
我们的游戏将根据这些信号作出相应的反应。
extends Node3D
signal focus_lost
signal focus_gained
signal pose_recentered
...
using Godot;
public partial class MyNode3D : Node3D
{
[Signal]
public delegate void FocusLostEventHandler();
[Signal]
public delegate void FocusGainedEventHandler();
[Signal]
public delegate void PoseRecenteredEventHandler();
...
脚本的变量
我们还向脚本引入了几个新变量:
maximum_refresh_rate
将控制头显设备的刷新率——如果头显设备支持控制的话。xr_interface
保存了对我们的 XR 接口的引用,这个变量其实已经存在,但现在我们将其类型化,以便更好地访问 XRInterface API。xr_is_focussed
将在我们的游戏获得焦点时设置为 true。
...
@export var maximum_refresh_rate : int = 90
var xr_interface : OpenXRInterface
var xr_is_focussed = false
...
...
[Export]
public int MaximumRefreshRate { get; set; } = 90;
private OpenXRInterface _xrInterface;
private bool _xrIsFocused;
...
更新后的 _ready 函数
我们在 _ready
函数中新加了一些东西。
If we're using the mobile or forward+ renderer we set the viewport's 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 OpenXR Settings for further details.
这些信号将由 XRInterface 触发。随着实现的深入,后续将提供更多关于这些信号的详细信息。
如果我们无法顺利启动 OpenXR ,我们也会选择退出应用。对于混合现实游戏的开发来说,你可以在成功初始化后进入 VR 模式,若失败再切换至非 VR 模式。不过,在一个独立的 VR 设备上运行仅支持 VR 的应用,启动失败时直接退出程序会比让系统挂着更合适。
...
# 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()
...
...
/// <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();
}
}
...
会话开始
该信号由 OpenXR 在我们设置会话时发出。意味着头戴设备已经完成了所有设置,并准备好开始接收程序内容。只有此时,各种信息才能正确地获取到。
The main thing we do here is to check our headset's 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.
最后,我们将物理更新速率与头戴设备的更新速率相匹配。Godot 默认物理帧刷新率为每秒 60 帧,而头戴设备通常至少以每秒 72 帧运行,较为先进的型号能高达 144 帧 / 秒。如果不将物理帧刷新率相匹配,将导致设备在对象尚未移动前过早开始渲染,导致画面出现卡顿。
...
# 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
...
...
/// <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;
}
...
进入可见状态
This signal is emitted by OpenXR when our game becomes visible but is not focused. 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 focused state next, that the user has opened a system menu or the user has just took their headset off.
On receiving this signal we'll update our focused state,
we'll change the process mode of our node to disabled which will pause processing on this node and its children,
and emit our focus_lost
signal.
如果你将此脚本添加到根节点,这意味着你的游戏将在需要时自动暂停。如果没有,你可以将方法连接到该信号,以执行额外的更改。
备注
如果游戏是因当用户打开系统菜单而处于可见状态,Godot 会继续渲染帧并保持头部跟踪活跃,因此游戏会在后台保持可见。然而,控制器和手部跟踪将被禁用,直到用户退出系统菜单为止。
...
# 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")
...
...
/// <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);
}
}
...
进入聚焦状态
OpenXR 会在游戏获得聚焦时发出这个信号。这会在启动完成时触发,但也可能在用户退出系统菜单或重新戴上头戴设备时触发。
同时注意,当游戏在用户未佩戴头戴设备时启动,游戏会保持在可见状态,直到用户戴上头戴设备。
警告
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 the focused 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 behavior in your game!
While handling our signal we will update the focuses state, unpause our node and emit our focus_gained
signal.
...
# 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")
...
...
/// <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);
}
...
进入停止状态
OpenXR 会在进入停止状态时发出这个信号。不同平台在该情况下的表现会有所不同。一部分平台只会在游戏关闭时发出此信号,另一部分在玩家摘下头戴设备时也会发出。
目前为止,该方法只充当一个占位符。
...
# Handle OpenXR stopping state
func _on_openxr_stopping() -> void:
# Our session is being stopped.
print("OpenXR is stopping")
...
...
/// <summary>
/// Handle OpenXR stopping state
/// </summary>
private void OnOpenXRStopping()
{
// Our session is being stopped.
GD.Print("OpenXR is stopping");
}
...
姿势重新居中
当用户请求重新定位视角时,OpenXR 会发出此信号。该信号主要用于告诉你的游戏:用户现在面朝前方,你应该重新定位玩家,使其在虚拟世界中面朝前方。
由于重新定位视角依赖于游戏设计,因此你的游戏需要被设计能正确地做出反应。
下面这段代码里,我们只是发出 pose_recentered
信号,并未提供用户重新定位的代码实现。你可以连接到这个信号并自行实现它。通常调用 center_on_hmd() 就足够了。
...
# 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")
...
/// <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);
}
}
这样就完成了我们的脚本。它被设计为能够重复利用。只需将它添加为主节点的脚本(如有需要还可以进行扩展),或者添加到专门用于此脚本的子节点上。