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.

更好的 XR 启动脚本

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

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

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

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

脚本的信号

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

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

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

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

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

extends Node3D

signal focus_lost
signal focus_gained
signal pose_recentered

...

脚本的变量

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

  • 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

...

更新后的 _ready 函数

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

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

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

这些信号将由 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()

...

会话开始

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

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

最后,我们将物理更新速率与头戴设备的更新速率相匹配。Godot 默认物理帧刷新率为每秒 60 帧,而 HMD 通常至少以每秒 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

...

进入可见状态

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

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

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

备注

如果游戏是因当用户打开系统菜单而处于可见状态,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
        process_mode = Node.PROCESS_MODE_DISABLED

        emit_signal("focus_lost")

...

进入聚焦状态

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

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

警告

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

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

...

# 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")

...

进入停止状态

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

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

...

# Handle OpenXR stopping state
func _on_openxr_stopping() -> void:
    # Our session is being stopped.
    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")

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