2025-01-01 NO2. XRHands 介绍
文章目录
- 软件配置
- 1 XR Hands 简介
- 2 XRHand
- 2.1 Pose
- 2.2 Handedness
- 3 XRHandJoint
- 3.1 XRHandJointID
- 3.2 XRHandJointTrackingState
- 4 XRHandSubsystem
- 4.1 数据属性
- 4.1.1 UpdateSuccessFlags
- 4.1.2 UpdateType
- 4.2 处理器管理:注册和注销
- 4.3 更新手部数据:`TryUpdateHands` 方法
- 4.3.1 检查运行,交由第三方处理
- 4.3.2 手部状态追踪与更新
- 4.3.3 数据预处理和处理器
- 4.3.4 动态更新和手势支持
- 4.3.5 回调处理
软件配置
- Unity 版本:Unity6000.0.26
- XRHands 版本:1.5.0
1 XR Hands 简介
XR Hands 包定义了允许从支持手部跟踪的设备访问手部跟踪数据的 API。要访问手部跟踪数据,还必须启用实现 XR 手部跟踪子系统的提供程序插件。

XR Hand 套件提供:
-
XR Hand Subsystem :定义用于手部跟踪数据的 XR 子系统接口。
-
OpenXR HandTracking feature:此功能为 OpenXR 提供程序插件实现
XRHandSubsystem
。在项目中同时安装了 OpenXR 和 XR Hand 软件包,就可以访问手部数据。其他提供程序插件必须更新以实现XRHandSubsystem
,然后才能在使用它们时访问手部数据。 -
Open XR Meta Aim Hand:此功能将来自 XR_FB_hand_tracking_aim 扩展的数据提供给 OpenXR 规范。此扩展提供基本的手势识别。
-
XR Hand:单个跟踪手的数据。
-
XR Hand Joint:手部单个关节或其他跟踪点的数据。
-
Meta Aim Hand:来自 Meta Aim 手 OpenXR 功能的捏合和瞄准手势数据。
注意:
XR Hands 包定义了用于手部跟踪的 API,但本身并未实现该功能。要在目标平台上使用手部跟踪,您还需要该平台的单独提供程序插件包,该插件包已更新以向 XR Hand Subsystem(该包定义的子系统)提供手部跟踪数据。
2 XRHand
代表来自 XRHandSubsystem
的手数据对象,用于管理和表示手的状态和关节信息。它包含了关于手的信息和操作,例如获取手部关节、手部位姿和手部跟踪状态。
该类不被直接创建,而是通过 XRHandSubsystem.leftHand
和 XRHandSubsystem.rightHand
属性来访问。
public struct XRHand : IEquatable<XRHand>
{// 获取手的根位姿,位于手腕部位。public Pose rootPose { get; }// 该手是左手还是右手public Handedness handedness { get; }// 当前手的根位姿和关节是否被追踪public bool isTracked { get; }// 返回指定 ID 的关节数据// 由于关节数据存储在本地的 NativeArray 中,调用此方法时,会获取到最新的手数据。public XRHandJoint GetJoint(XRHandJointID id) { ... }...
}
2.1 Pose
Pose
是 Unity 中用于表示物体在三维空间中的位置和旋转,封装了 Vector3
和 Quaternion
的数据结构,分别用于存储位置和旋转,适合用于表示游戏对象、虚拟角色、手部模型等在空间中的姿势。
public struct Pose : IEquatable<Pose>
{public Vector3 position; // 位置public Quaternion rotation; // 旋转// 将当前 Pose 应用到一个给定的 Pose/Transform 上,通常用于将 Pose 从一个坐标系转换到另一个坐标系public Pose GetTransformedBy(Pose lhs) { ... }public Pose GetTransformedBy(Transform lhs) { ... }// 公共属性public Vector3 forward { get; }public Vector3 right { get; }public Vector3 up { get; }public Vector3 forward { get; }
}
2.2 Handedness
用于表示左右手:
// Decompiled with JetBrains decompiler
// Type: UnityEngine.XR.Hands.Handedness
// Assembly: Unity.XR.Hands, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null
// MVID: A6E3BCB6-F905-44B3-8327-8AFFABC5770A
// Assembly location: P:\Unity Project\Learning\XRHands-Learning\Library\ScriptAssemblies\Unity.XR.Hands.dll#nullable disable
namespace UnityEngine.XR.Hands
{/// <summary>/// Denotes which hand this joint is on./// </summary>public enum Handedness{Invalid,Left,Right,}
}
3 XRHandJoint
表示 XR 中手部关节的结构体,封装了与手部关节相关的多种信息,例如关节的 位置、旋转、半径、线速度、角速度 等。
[StructLayout(LayoutKind.Sequential)]
public struct XRHandJoint : IEquatable<XRHandJoint>
{public XRHandJointID id { get; } // 关节唯一标识符public Handedness handedness { get; } // 属于哪只手public XRHandJointTrackingState trackingState { get; } // 关节追踪状态// 尝试获取该关节的半径(如果有)。// 返回 true 表示成功获取,false 表示未能获取到有效数据。// 成功时,radius 参数将被赋值为关节的半径。public bool TryGetRadius(out float radius) { ... }// 尝试获取该关节的 姿势(位置和旋转)。// 如果姿势数据有效,返回 true,并将姿势信息赋值给 pose 参数。// 如果无效,则返回 false 并将 pose 设置为 Pose.identity。public bool TryGetPose(out Pose pose) { ... }// 尝试获取该关节的 线速度。// 如果线速度数据有效,返回 true,并将线速度赋值给 linearVelocity 参数。// 如果无效,则返回 false,并将 linearVelocity 设置为 Vector3.zero。public bool TryGetLinearVelocity(out Vector3 linearVelocity) { ... }// 尝试获取该关节的 角速度。// 如果角速度数据有效,返回 true,并将角速度赋值给 angularVelocity 参数。// 如果无效,则返回 false,并将 angularVelocity 设置为 Vector3.zero。public bool TryGetAngularVelocity(out Vector3 angularVelocity) { ... }...
}
3.1 XRHandJointID
XR Hand 将手部关节分为 26 种,包括指关节、指尖、手腕和手掌。

XR Hand 使用 XRHandJointID
枚举类表示该 26 个 Joint:
// Decompiled with JetBrains decompiler
// Type: UnityEngine.XR.Hands.XRHandJointID
// Assembly: Unity.XR.Hands, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null
// MVID: A6E3BCB6-F905-44B3-8327-8AFFABC5770A
// Assembly location: P:\Unity Project\Learning\XRHands-Learning\Library\ScriptAssemblies\Unity.XR.Hands.dll#nullable disable
namespace UnityEngine.XR.Hands
{/// <summary>/// The ID of this joint./// </summary>public enum XRHandJointID{Invalid = 0,BeginMarker = 1,Wrist = 1,Palm = 2,ThumbMetacarpal = 3,ThumbProximal = 4,ThumbDistal = 5,ThumbTip = 6,IndexMetacarpal = 7,IndexProximal = 8,IndexIntermediate = 9,IndexDistal = 10, // 0x0000000AIndexTip = 11, // 0x0000000BMiddleMetacarpal = 12, // 0x0000000CMiddleProximal = 13, // 0x0000000DMiddleIntermediate = 14, // 0x0000000EMiddleDistal = 15, // 0x0000000FMiddleTip = 16, // 0x00000010RingMetacarpal = 17, // 0x00000011RingProximal = 18, // 0x00000012RingIntermediate = 19, // 0x00000013RingDistal = 20, // 0x00000014RingTip = 21, // 0x00000015LittleMetacarpal = 22, // 0x00000016LittleProximal = 23, // 0x00000017LittleIntermediate = 24, // 0x00000018LittleDistal = 25, // 0x00000019LittleTip = 26, // 0x0000001AEndMarker = 27, // 0x0000001B}
}
例如,在 HandVisualizer 示例场景中,Left Hand Tracking 物体上关联了手的所有 XRHandJointID
。

3.2 XRHandJointTrackingState
表示正在跟踪的特定关节的值,分别有:
// Decompiled with JetBrains decompiler
// Type: UnityEngine.XR.Hands.XRHandJointTrackingState
// Assembly: Unity.XR.Hands, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null
// MVID: A6E3BCB6-F905-44B3-8327-8AFFABC5770A
// Assembly location: P:\Unity Project\Learning\XRHands-Learning\Library\ScriptAssemblies\Unity.XR.Hands.dllusing System;#nullable disable
namespace UnityEngine.XR.Hands
{[Flags]public enum XRHandJointTrackingState{None = 0, // 没有跟踪到数据Radius = 1, // 关节半径Pose = 2, // 关节姿势LinearVelocity = 4, // 关节线速度AngularVelocity = 8, // 关节角速度WillNeverBeValid = 16, // 永远失效,通常因为该关节不存在HighFidelityPose = 32, // 运行时精确追踪,而非基于推测或推算。通常来源于准确的传感器数据}
}
需要注意,该枚举类被标识为 [Flags],因此可以多选。
4 XRHandSubsystem
XRHandSubsystem
是一个抽象类或基类,通常由特定平台或设备的实现类继承和扩展,专门处理与手部追踪相关的数据,并提供框架来处理手部的状态、姿势、关节和手势等信息。
4.1 数据属性
public partial class XRHandSubsystem : SubsystemWithProvider<XRHandSubsystem, XRHandSubsystemDescriptor, XRHandSubsystemProvider>
{// 左、右手信息public XRHand leftHand { get; }public XRHand rightHand { get; } // 存储手部关节的布局信息(每个关节是否存在)public NativeArray<bool> jointsInLayout { get; }// 在最近一次手部数据更新过程中,左右手的哪些数据被成功更新。// 该属性每次手部数据更新时都会被更新。public UpdateSuccessFlags updateSuccessFlags { get; } // 与左、右手相关的常见手势的识别数据public XRCommonHandGestures leftHandCommonGestures { get; }public XRCommonHandGestures rightHandCommonGestures { get; }// 手部跟踪成功的回调事件public Action<XRHand> trackingAcquired;// 用于在手部数据的关节信息处理之前进行预处理。public Action<XRHandSubsystem, UpdateSuccessFlags, UpdateType> preprocessJoints;// 该回调会在手部数据更新时被调用。public Action<XRHandSubsystem, UpdateSuccessFlags, UpdateType> updatedHands;// 手部跟踪丢失的回调事件public Action<XRHand> trackingLost;...
}
回调的执行顺序为:
trackingAcquired ->
preprocessJoints ->
updatedHands ->
trackingLost。
注意:
updatedHands 事件每帧执行两次,最大化手部数据的更新频率:
- 第一次:在 MonoBehaviour.Update 事件附近执行。
- 第二次:在 Application.onBeforeRender 事件附近执行。
4.1.1 UpdateSuccessFlags
包括以下:
/// <summary>
/// 在每次手部数据更新中,哪些手部数据被成功更新。
/// </summary>
[Flags]
public enum UpdateSuccessFlags
{// 没有任何数据被成功更新None = 0,// 左手根姿势数据已成功更新LeftHandRootPose = 1 << 0,// 左手关节数据已成功更新LeftHandJoints = 1 << 1,// 右手根姿势数据已成功更新RightHandRootPose = 1 << 2,// 右手关节数据已成功更新RightHandJoints = 1 << 3,// 所有有效的数据已成功更新All = LeftHandRootPose | LeftHandJoints | RightHandRootPose | RightHandJoints
}
4.1.2 UpdateType
/// <summary>
/// The timing of a hand update during a frame.
/// </summary>
public enum UpdateType
{// 类似 MonoBehaviour.Update 时机更新Dynamic,// 类似 Application.onBeforeRender 时机更新BeforeRender
}
4.2 处理器管理:注册和注销
XRHandSubsystem
支持在手部数据处理流程中插入自定义的处理器,通过 RegisterProcessor
和 UnregisterProcessor
方法来进行管理。
public partial class XRHandSubsystem : SubsystemWithProvider<XRHandSubsystem, XRHandSubsystemDescriptor, XRHandSubsystemProvider>
{...List<IXRHandProcessor> m_Processors = new List<IXRHandProcessor>();public void RegisterProcessor<TProcessor>(TProcessor processor)where TProcessor : class, IXRHandProcessor{if (processor == null)throw new ArgumentException("Processor cannot be null.", nameof(processor));m_Processors.Add(processor);m_Processors.Sort(CompareProcessors);}public void UnregisterProcessor<TProcessor>(TProcessor processor)where TProcessor : class, IXRHandProcessor{m_Processors.Remove(processor);}...
}
处理器需要实现 IXRHandProcessor
接口,负责处理和修改手部关节数据。每个处理器可以根据自己的优先级顺序进行数据处理,这个顺序由 callbackOrder
来决定。
例如,在 HandVisualizer 场景中,Hand Visualizer 物体上挂载的 HandProcessor.cs 脚本就是一个处理器,依据 ProcessorExampleMode 来对 Hands 进行平滑或翻转处理。

4.3 更新手部数据:TryUpdateHands
方法
在 XRHandProviderUtility.cs 脚本中,m_Subsystem.TryUpdateHands() 方法被 Update() 方法调用。因此,TryUpdateHands() 方法用于每帧更新 Hands 数据。
在 XRHandProviderUtility.cs 中的 OnUpdate() 和 OnBeforeRender() 方法分别调用了 Update() 方法,因此 m_Subsystem.TryUpdateHands() 方法每帧被更新 2 次,即 4.1 节中的注意内容。

4.3.1 检查运行,交由第三方处理
如果未运行,则直接返回。
同时,向手部数据提供者发送请求,更新左右手的数据。
public virtual unsafe UpdateSuccessFlags TryUpdateHands(UpdateType updateType)
{if (!running)return UpdateSuccessFlags.None;updateSuccessFlags = provider.TryUpdateHands(updateType,ref m_LeftHand.m_RootPose,m_LeftHand.m_Joints,ref m_RightHand.m_RootPose,m_RightHand.m_Joints);...
}
4.3.2 手部状态追踪与更新
public virtual unsafe UpdateSuccessFlags TryUpdateHands(UpdateType updateType)
{...// 清除手指的状态缓存,确保每次计算时使用的是最新的数据。XRFingerShapeMath.ClearFingerStateCache(Handedness.Left);XRFingerShapeMath.ClearFingerStateCache(Handedness.Right);// 检测左右手追踪状态,判断是否触发 trackingAcquired 或 trackingLostvar wasLeftHandTracked = m_LeftHand.isTracked;var success = UpdateSuccessFlags.LeftHandRootPose | UpdateSuccessFlags.LeftHandJoints;m_LeftHand.isTracked = (updateSuccessFlags & success) == success;if (!wasLeftHandTracked && m_LeftHand.isTracked)trackingAcquired?.Invoke(m_LeftHand);else if (wasLeftHandTracked && !m_LeftHand.isTracked)trackingLost?.Invoke(m_LeftHand);var wasRightHandTracked = m_RightHand.isTracked;success = UpdateSuccessFlags.RightHandRootPose | UpdateSuccessFlags.RightHandJoints;m_RightHand.isTracked = (updateSuccessFlags & success) == success;if (!wasRightHandTracked && m_RightHand.isTracked)trackingAcquired?.Invoke(m_RightHand);else if (wasRightHandTracked && !m_RightHand.isTracked)trackingLost?.Invoke(m_RightHand);...
}
对于左手:
-
wasLeftHandTracked
:记录更新前左手的追踪状态。 -
success
:判断左手根姿势和关节数据是否成功更新。 -
m_LeftHand.isTracked
:根据success
判断左手是否成功追踪。如果左手的追踪状态发生了变化(从未追踪到追踪,或从追踪中丧失),则触发
trackingAcquired
或trackingLost
回调。
右手同理。
4.3.3 数据预处理和处理器
public virtual unsafe UpdateSuccessFlags TryUpdateHands(UpdateType updateType)
{...// 对手部关节数据进行预处理preprocessJoints?.Invoke(this, updateSuccessFlags, updateType);// 每个处理器都对关节数据进行处理for (int processorIndex = 0; processorIndex < m_Processors.Count; ++processorIndex)m_Processors[processorIndex].ProcessJoints(this, updateSuccessFlags, updateType);...
}
4.3.4 动态更新和手势支持
如果更新类型是动态(Dynamic
),并且手部数据提供者支持常见的手势数据,系统会更新不同的手势数据,例如:
- AimPose:目标对准姿势。
- GraspPose:抓握姿势。
- PinchPose:捏合姿势。
- GripPose:抓握姿势。
- …
对每种手势数据,XRHandSubsystem
会尝试从 provider
获取数据,并更新到对应的手势识别组件(m_LeftHandCommonGestures
和 m_RightHandCommonGestures
)。如果数据不可用,则调用 Invalidate
方法使手势数据失效。
public virtual unsafe UpdateSuccessFlags TryUpdateHands(UpdateType updateType)
{...if (updateType == UpdateType.Dynamic && provider.canSurfaceCommonPoseData){if (subsystemDescriptor.supportsAimPose){if (provider.TryGetAimPose(Handedness.Left, out var aimPoseLeft))m_LeftHandCommonGestures.UpdateAimPose(aimPoseLeft);elsem_LeftHandCommonGestures.InvalidateAimPose();}// 其它手势处理类似:GraspPose, PinchPose, GripPose等...}...
}
4.3.5 回调处理
在数据更新后,通过 updatedHands
或 handsUpdated
回调通知外部系统手部数据已经更新。
updatedHands
是较新的回调方法,而 handsUpdated
已经被弃用,但仍然保持兼容。
public virtual unsafe UpdateSuccessFlags TryUpdateHands(UpdateType updateType)
{...if (updatedHands != null)updatedHands.Invoke(this, updateSuccessFlags, updateType);#pragma warning disable 618if (handsUpdated != null)handsUpdated.Invoke(updateSuccessFlags, updateType); // 弃用,但保持兼容
#pragma warning restore 618return updateSuccessFlags;
}