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.

OpenXR 手部跟踪

介绍

备注

此页面着重于 OpenXR 提供的特性。这里提到的部分功能同样适用于 WebXR,也可以由其他 XR 接口提供。

在讨论手跟踪前应当先意识到,各个不同的系统对各个区域的理解不同。因此,不同的 OpenXR 运行时实现的表现也会不同。在开发过程中,你可能遇到某硬件不支持某一操作的问题,或者出现与其他平台表现的差别大到需要额外处理的情况。

尽管如此,近期对 OpenXR 规范的改进缩小了这种差距,而随着各个平台开始实现新版规范,我们也在逐步迈向一个各个平台之间可以完美移植的美好未来,或者至少能拥有一个可以检测平台功能的明确方法。

早期的大型 VR 平台主要采用的是基于跟踪手柄的输入。这套系统追踪一个物理设备,该设备上还装有一系列按键,用于进一步的输入。从跟踪数据中可以推断出玩家手部位置,但无法进一步获取更多信息。通常情况下,由游戏负责实现一套利用进一步输入来显示玩家的手部并创建手指动画的机制,其中的输入可能来自于按键或接近传感器。大部分时候手指的位置还会参考游戏上下文进行判断,例如用户手持的物品,或者正在进行的动作。

近来,光学手部跟踪逐渐开始流行,这套系统使用摄像头来追踪用户手部,可以生成完整的手部和手指位置数据。大部分供应商将其视为完全不同于手柄跟踪的一套体系,并发布了独立的 API 来访问手部和手指的位置与旋转数据。在处理输入时,由游戏开发人员负责实现手势检测系统。

这种两边分割的情况同样存在于 OpenXR 中,其中手柄跟踪主要由动作映射系统处理,而光学手部跟踪则主要由手部跟踪 API 扩展处理。

然而,这世界并非非黑即白,有这么几种情况难以简单地归类:

  • 同时符合两种类别的设备,例如带跟踪的手套或手柄,例如同时拥有手指追踪功能的 Index controller。

  • 通过实现用手柄数据进行推断手部追踪来解决多个手柄的手指放置问题的 XR 运行时。

  • 意图在手柄和手部追踪之间流畅切换,对两种方法提供相同的用户体验的 XR 应用软件。

OpenXR 正通过引入更多的扩展功能来响应这一需求,让我们能够查询 XR 运行时/硬件的具体性能,或者在各类设备之间增加更多通用功能。但目前依然存在的一个问题是,这些扩展功能的普及还存在空白,导致部分平台无法完全发挥出它们的全部性能。因此,你可能需要针对特定硬件上实际可用的功能进行测试,并相应地调整你的开发方案。

示例项目

本页介绍的内容被用来制作了一个演示项目,你可以在 here 找到它。

手部跟踪 API

正如我们在开篇提到的,手部追踪 API 主要应用于光学手部追踪,而且在许多平台上,只有当用户没有握持手柄时它才会生效。不过,有些平台支持‘通过手柄推断的手部追踪’,这意味着即使用户正握着手柄,你依然能获取到手部追踪数据。目前支持该功能的平台包括 SteamVR 和 Meta Quest(目前仅限 Quest 原生模式,但 Meta Link 串流模式的支持预计很快也会到来),希望不久之后也会有更多平台加入进来。

Godot 中的手部追踪实现已经围绕‘Godot 人形骨骼(Humanoid Skeleton)’完成了标准化,并且同时支持 OpenXR 和 WebXR。因此,下面的操作说明在这两种环境下都是通用的。

要想使用 OpenXR 的手部追踪 API,你首先需要先启用它。这可以在项目设置中完成:

../../_images/xr_enable_handtracking.webp

对于某些一体式 XR 设备,你还需要在导出设置里配置手部追踪扩展插件,比如 Meta Quest 就是如此:

../../_images/openxr_enable_hand_tracking_meta.webp

现在,你需要为每只手在你的场景里添加 3 个组件:

  • 一个用于定位手部位置的被追踪节点。

  • 个带有骨骼且蒙皮正确的手部网格模型。

  • 种将手指追踪数据应用到骨骼上的骨骼修改器。

../../_images/openxr_hand_tracking_nodes.webp

手部跟踪节点

手部追踪系统会使用独立的手部追踪器,来追踪玩家双手在我们追踪空间内的位置。

针对以下几种使用场景,这些信息已被单独拆分出来了:

  • 追踪是在 XROrigin3D 节点的本地空间中进行的。因此,该节点必须是 XROrigin3D 的子节点,这样才能被正确地放置在 VR 空间里。

  • 当使用带有手臂的完整上半身模型,而不是单独的手部模型时,这个节点可以被用作 IK(反向动力学)目标。

  • 手部的实际放置位置可能与追踪结果存在松散绑定关系。这种情况常见于角色创建界面、虚拟镜子等类似场景,在这些情况下,手部网格和手指追踪可能会被定位到其他位置。

我们只考虑第一个用例。

为此,你需要在 XROrigin3D 节点中添加一个 XRNode3D 节点。

  • 该节点上的 tracker 应设置为 /user/hand_tracker/left/user/hand_tracker/right,分别对应左右手。

  • pose 应保持设置为 default,其他选项均无法使用。

  • 勾选 Show When Tracked (追踪时显示)这个复选框后,如果没有可用的追踪数据,该节点会自动隐藏;而一旦有了追踪数据,该节点就会自动显示出来。

带骨骼的手部模型

为了显示我们的手,我们需要一个已经正确绑定了骨骼和蒙皮的手部网格模型(hand mesh)。为此,Godot 使用的是 Godot Humanoid 中定义的手部骨骼结构,并且还支持在每根手指上额外添加一个指尖骨骼(tip bone)。

OpenXR hand tracking demo 里包含了已经做好了正确骨骼绑定的手部 glTF 模型文件示例。

我们将在这里用到它们,并将它们添加为 XRNode3D 节点的子节点。同时,我们还需要开启‘可编辑子节点’(Editable Children)功能,这样才能访问到内部的 Skeleton3D 节点。

手部骨架修改器

最后,我们需要在 Skeleton3D 节点下添加一个 XRHandModifier3D 子节点。这个节点的作用是从 OpenXR 获取手指追踪数据,并将其应用到我们的手部模型上。

你需要将 Hand Tracker (手部追踪器)属性设置为 /user/hand_tracker/left/user/hand_tracker/right 。具体选哪个,取决于你当前是要应用左手还是右手的追踪数据。

你也可以在这个节点上设置 Bone Update (骨骼更新)模式。

  • Full (完全模式)会完全应用手部追踪的数据。这意味着骨骼的定位会真实地反映出用户实际手掌的大小。但如果模型(mesh)的蒙皮权重没有做好相应的适配,这可能会导致手部出现奇怪的“挤压”或“皱缩”效果。所以一定要记得,当使用光学手部追踪时,找不同手型大小的玩家来测试你的游戏!

  • Rotation Only (仅旋转)模式只会改变手部骨骼的旋转角度,并保持骨骼的原始长度不变。在这种模式下,手部模型(mesh)的整体大小是不会发生变化的。

加上这个之后,当我们运行项目时,只要(设备或环境)支持手部追踪,我们应该就能正确看到手部模型被显示出来了。

手部跟踪数据源

这是一个 OpenXR 扩展,用来提供手部追踪数据具体来源的相关信息。目前只有少数几个 XR 运行时(runtimes)实现了它,但如果系统中有这个扩展,Godot 引擎就会自动启用它。

如果不支持这个扩展,并且因此返回了 unknown(未知),你可以做以下推断:

  • 如果你使用的是 SteamVR(包括 Steam link),目前仅支持基于手柄的手部追踪。

  • 对于任何其他运行时(runtime),如果支持手部追踪,则仅支持光学手部追踪(注:Meta Link 目前也属于这一类)。

  • 除了上述情况外,其他所有情形下都无法使用手部追踪功能。

你可以通过代码来获取这些信息:

var hand_tracker : XRHandTracker = XRServer.get_tracker('/user/hand_tracker/left')
if hand_tracker:
    if hand_tracker.has_tracking_data:
        if hand_tracker.hand_tracking_source == XRHandTracker.HAND_TRACKING_SOURCE_UNKNOWN:
            print("Hand tracking source unknown")
        elif hand_tracker.hand_tracking_source == XRHandTracker.HAND_TRACKING_SOURCE_UNOBSTRUCTED:
            print("Hand tracking source is optical hand tracking")
        elif hand_tracker.hand_tracking_source == XRHandTracker.HAND_TRACKING_SOURCE_CONTROLLER:
            print("Hand tracking data is inferred from controller data")
        else:
            print("Unknown hand tracking source ", hand_tracker.hand_tracking_source)
    else:
        print("Hand is currently not being tracked")
else:
    print("No hand tracker registered")

这个示例会打印(或记录)左手的状态信息。

在这个示例中,如果 get_tracker 没有返回任何手部追踪器(hand tracker),这就意味着当前的 XR 运行时(XR runtime)压根就不支持手部追踪 API。

如果存在追踪器(tracker),但 has_tracking_data 的值为 false(假),则说明用户的手部当前并未被成功追踪。这很可能是由以下原因之一导致的:

  • 头显上的追踪摄像头看不到玩家的手

  • 玩家当前正在使用控制器,而头显仅支持光学手部追踪

  • 手柄已关闭,当前仅支持手柄手部追踪(controller hand tracking)。

处理用户输入

在使用手柄(controllers)的情况下,系统对用户操作的响应是通过 XR 动作映射 来处理的。在动作映射中,你可以将手柄上的各种输入(比如扳机键或摇杆)绑定到一个具体的动作上。这样一来,这些动作就能直接驱动你游戏里的各种逻辑了。

在使用裸手追踪时,我们最初并没有提供这类标准的输入信号。所有的输入都依赖于用户做出的具体手势,比如握紧拳头来抓取物体,或者将拇指和食指捏在一起来选择某个东西。而在当时,如何识别和实现这些手势,全靠游戏开发者自己去搞定。

鉴于越来越多的应用需要能够在手柄追踪和裸手追踪之间无缝切换,同时也需要某种形式的基础输入能力,因此在规范中新增了一系列扩展。这些扩展提供了一些基础的手势识别功能,并且可以直接配合动作映射(action map)来使用。

手部交互式配置

hand interaction profile extension 是一项全新的核心扩展,它支持捏合(pinch)、抓握(grasp)和戳(poke)这三种手势以及与之相关的姿势。目前,对该扩展的支持还比较有限,但在不久的将来,预计会有更多的运行时(runtimes)开始提供对它的支持。

../../_images/openxr_hand_interaction_profile.webp

捏合(pinch)手势是通过将拇指和食指捏在一起触发的。这个手势通常被用作菜单系统中的‘选择’操作,这就好比你在用手柄时,先用准星对准一个物体,然后扣动扳机键(trigger)来确认选择。因此,在映射交互逻辑时,通常也会按照这个思路来设定。

  • pinch pose (捏合姿势)是一个定位在拇指指尖和食指指尖正中间的姿势,其朝向经过专门设定,以便可以使用射线投射(ray cast)来识别目标。

  • pinch (捏合)这个浮点型输入的取值范围在 0.0(拇指和食指的指尖分开)到 1.0(拇指和食指的指尖接触)之间。

  • 当(两个)指尖(几乎)快要触碰到一起时, pinch ready (捏合就绪)输入即为真(true)。

抓握(grasp)手势是通过握紧拳头来触发的,通常用于拾取物品,这与按下手柄上的侧键(squeeze input)非常相似。

  • grasp (抓握)这个浮点型输入的取值范围在 0.0(手掌张开)到 1.0(握成拳头)之间。

  • 当用户握紧拳头时, grasp ready (抓握就绪)输入即为真(true)。

戳(poke)手势是通过伸直食指来触发的。这个手势算是一个小小的特例,因为通常我们会直接利用食指指尖的姿势,去‘戳’那些可以交互的物体。所谓的 戳姿势( poke pose ),其实就是定位在你食指指尖上的一个姿势。

最后, aim activate (ready) (瞄准激活/就绪)输入被定义为:当食指伸直并指向一个可以被激活的目标时,该输入值为 1.0(或 true)。不过,各个运行时(runtimes)具体会如何解读这个动作,目前还不是很明确。

通过这种设置,系统会使用常规的 left_handright_hand 追踪器。因此,你可以实现手柄操作与手部追踪输入之间的无缝切换。

备注

你需要在 OpenXR 的项目设置中,启用手部交互配置扩展。

Microsoft 手部交互配置文件

Microsoft hand interaction profile extension 是由微软推出的,其设计思路大致模仿了简易控制器配置(simple controller profile)。Meta 也已经在其原生的 OpenXR 客户端上增加了对该扩展的支持,但目前通过 Meta Link(连接电脑使用)时暂时无法使用该功能。

../../_images/openxr_msft_hand_interaction_profile.webp

捏合(Pinch)支持是通过 select 输入来提供的。当拇指和食指的指尖分开时,该值为 0.0;当它们捏合在一起时,该值为 1.0。

需要注意的是,在这个配置中, aim pose 被重新定义为位于拇指和食指之间的一种姿势,其朝向经过专门设计,以便通过发射射线来精准识别目标。

抓握(Grasp)支持是通过 squeeze 输入来实现的。当手完全张开时,该值为 0.0;当手紧握成拳时,该值为 1.0。

通过这种设置,系统会使用常规的 left_handright_hand 追踪器。因此,你可以实现手柄操作与手部追踪输入之间的无缝切换。

HTC 手部交互配置

HTC hand interaction profile extension 是由 HTC 推出的,其定义方式与微软的扩展非常相似。目前该扩展仅受 HTC 的 Focus 3 和 Elite XR 头显支持。

../../_images/openxr_htc_hand_interaction_profile.webp

关于手势支持的相关信息,请查阅 Microsoft 手部交互配置(Microsoft hand interaction profile)。

最根本的区别在于,这个扩展引入了两个全新的追踪器: /user/hand_htc/left/user/hand_htc/right 。这意味着,当用户放下或拿起手柄时,系统需要额外实现一套逻辑,以便在默认追踪器和 HTC 专属追踪器之间进行切换。

简易控制器配置

简单控制器配置文件(simple controller profile)是一个标准的通用配置文件。当用户使用的控制器找不到任何对应的专属配置文件时,它就会作为后备方案(fallback)自动启用。

目前有不少 OpenXR 运行时(XR runtime)在手部追踪模式下,会通过‘简易控制器配置’来模拟出控制器的信号。

遗憾的是,目前并没有一种可靠的方法,能准确判断出用户到底是在使用一个未知的控制器,还是手部追踪功能正在通过这个配置(Profile)来模拟控制器。

../../_images/openxr_simple_controller_hand.webp

XR 运行时可以自由地定义简易控制器配置(Simple Controller Profile)的运作方式,因此,这套配置具体是如何映射到各种手势上的,也并没有一个绝对确定的标准。

最常见的映射规则是:当用户的拇指和食指指尖相互触碰,并且手掌朝向远离身体的方向(即手心朝外)时, select click 就会被判定为触发(true)。而当拇指和食指指尖触碰,同时手掌朝向自己(即手心朝内)时, menu click 就会被判定为触发(true)。

通过这种设置,系统会使用常规的 left_handright_hand 追踪器。因此,你可以实现手柄操作与手部追踪输入之间的无缝切换。

备注

由于这些交互配置之间存在一定的重叠,所以你需要知道的是:你可以把所有配置都添加到你的动作映射(Action Map)中,而 XR 运行时会帮你自动挑选出最合适的那一个。

举个例子,Meta Quest 同时支持‘微软手部交互配置’和‘简易控制器配置’。如果两者都被指定了,系统会优先采用‘微软手部交互配置’。

按照预期,一旦 Meta 支持了‘核心手部交互配置扩展’,那么该配置将会优先于微软(Microsoft)和简易控制器(Simple Controller)的交互配置。

手势交互输入

如果某个平台在使用手部追踪时不支持任何交互配置文件(interaction profiles),或者你正在开发的应用需要更复杂的手势支持,那么你就得自己搭建一套手势识别系统了。

你可以通过每只手的 XRHandTracker 资源来获取完整的手部追踪数据。只需要调用 XRServer.get_tracker 方法,并将 /user/hand_tracker/left/user/hand_tracker/left 作为参数传入,就能获取到对应的手部追踪器。通过这个资源,你就可以访问该只手上所有关节的具体信息。

详细讲解一整套手势识别算法已经超出了本手册的范畴,不过你可以参考以下几个社区项目: