MRTK3 中的手部跟踪

概述

作为唯一尚未由 Unity 输入系统本地处理的输入数据片段之一,铰接式手部关节数据由子系统处理。

注意

如果你不熟悉 MRTK3 子系统及其与 MRTK 2.x 服务的区别,请参阅 MRTK3 子系统体系结构文档,深入了解我们的理念和设计。

我们的子系统引入来自多个源的手部关节数据,并将它们聚合到一个可跨设备和模拟环境工作的中央 API 中。 以下子系统是 HandsSubsystem 的实现:

  • OpenXRHandsSubsystem 直接从 OpenXR 插件接收手部数据
  • XRSDKHandsSubsystem 从 Unity 的 XR SDK 抽象层接收手部数据(反过来,它可能从 OpenXR 或一些其他来源获取数据)
  • SyntheticHandsSubsystem 根据来自系统的输入动作(例如 devicePositiondeviceRotation 等)合成假的手部关节。 该子系统提供在编辑器中使用输入模拟时看到的关节。

这些 HandsSubsystemsHandsAggregatorSubsystem 查询,后者将所有手部数据源组合并到中央 API 中。

重要

每当直接查询手部关节数据时,始终首选从聚合器查询,而不是从任何单个 HandsSubsystem 查询。 这样,代码将适用于任何手部数据源,包括模拟数据。

聚合器和手部子系统徐缓地评估传入的手部数据请求。 在“客户端”脚本请求之前,不会查询手部数据。 如果应用仅请求单个关节,则手部子系统将徐缓地评估并且仅从基础 API 查询单个关节。 此外,如果“客户端”请求完整的关节数据,同一帧内的后续调用将重用相同的数据,从而减少在同一帧内查询多个关节的成本。 在每个新帧上,缓存将被弄脏并刷新,后续调用将开始重新填充缓存。

因此,在分析应用程序时,你可能会发现帧中的第一个关节查询比后续查询花费更多时间。 这是由于与第一个查询关联的摊销成本,以及后续“缓存命中”的相对性能。

捏合特征

聚合器根据从每个特定手子部系统查询的关节数据计算有关捏合手势的多个测量值。 这些测量值在聚合器子系统配置中进行配置。

Hands aggregator subsystem configuration

“捏合开放阈值”和“捏合封闭阈值”控制拇指和食指之间的绝对世界距离,用于规范化捏合进度。 当距离等于封闭阈值时,捏合进度为 1.0,当距离等于开放阈值时,为 0.0。 (这些阈值目前采用世界单位,但很快就会规范化为用户的手部大小。)

“举手相机 FOV”控制手必须离用户视野中心多近才能被视为有效捏合。 “手背向容差”控制测量用户手部旋转的容差,以确定用户的手何时背向。

示例

// Get a reference to the aggregator.
var aggregator = XRSubsystemHelpers.GetFirstRunningSubsystem<HandsAggregatorSubsystem>();
// Wait until an aggregator is available.
IEnumerator EnableWhenSubsystemAvailable()
{
    yield return new WaitUntil(() => XRSubsystemHelpers.GetFirstRunningSubsystem<HandsAggregatorSubsystem>() != null);
    GoAhead();
}
// Get a single joint (Index tip, on left hand, for example)
bool jointIsValid = aggregator.TryGetJoint(TrackedHandJoint.IndexTip, XRNode.LeftHand, out HandJointPose jointPose);
// Get an entire hand's worth of joints from the left hand.
bool allJointsAreValid = aggregator.TryGetEntireHand(XRNode.LeftHand, out IReadOnlyList<HandJointPose> joints)
// Check whether the user's left hand is facing away (commonly used to check "aim" intent)
// This is adjustable with the HandFacingAwayTolerance option in the aggregator configuration.
// handIsValid represents whether there was valid hand data in the first place!
bool handIsValid = aggregator.TryGetPalmFacingAway(XRNode.LeftHand, out bool isLeftPalmFacingAway)
// Query pinch characteristics from the left hand.
// pinchAmount is [0,1], normalized to the open/closed thresholds specified in the aggregator configuration.
// isReadyToPinch is adjusted with the HandRaiseCameraFOV and HandFacingAwayTolerance settings in the configuration.
bool handIsValid = aggregator.TryGetPinchProgress(XRNode.LeftHand, out bool isReadyToPinch, out bool isPinching, out float pinchAmount)