手部菜单 - MRTK3

Hand Menu

通过手部菜单,用户可以快速调出常用功能的手部连接 UI。 这些通常是提供快速操作的小型按钮组。 但有时会以手部菜单的形式向用户提供用于显示信息或用于设置的更复杂布局,通常可以选择从手部“拆离”该菜单并将其锁定到世界中。

为防止在与其他对象交互时错误激活,手部菜单提供了“要求手平伸”和“使用凝视激活”选项。 建议使用这些选项以防止意外激活。

示例场景和预制件

如果使用的是模板项目,HandMenuExamples.unity 演示了手部菜单的多个常见配置,所有这些配置都使用 HandConstraintPalmUp 脚本。

Hand Menu Example Scene

HandMenuLarge

此预制件演示需要延长交互时间的大型或复杂 UI 的示例。 对于这种类型的 UI,建议在手放下时对菜单进行世界锁定,以提高实用性并避免手臂疲劳。 此示例还支持通过“抓取和拉取”来对菜单进行世界锁定。

在此示例中,通过在 OnFirstHandDetected() 事件上激活 MenuContent 对象,可将菜单变得可见/不可见。 通过 OnLastHandLost() 事件,将激活“关闭”按钮,并触发“放置动画”。 此动画是一个简单的缩放波动。 由于我们没有在 OnLastHandLost() 事件中隐藏 MenuContent,因此当手不可见时,菜单将自动进行世界锁定。 “掌心向上”部分中的值已经过优化,使菜单可以进行世界锁定,而不会在手放下时向下拖动太多

Hand Menu Example Large 1

Palm Up configuration

此示例在菜单底部区域提供了可抓取栏和自动世界锁定行为。 用户可以显式将菜单从手部分离出来,并通过抓住菜单将其放置在世界中。 为了实现这一点,我们将在 ObjectManipulator 中的 ManipulationStarted() 事件上禁用 SolverHandler.UpdateSolvers。 否则,菜单将无法分离,因为 HandConstraint 解算器会尝试将菜单定位在手部位置附近。 我们还将使用 HandConstraintPalmUp.StartWorldLockReattachCheckCoroutine,使用户抬手便可将菜单从手部拆离

Hand Menu Example Large 2

最后,“关闭”按钮需要重新激活 SolverHandler.UpdateSolvers 才能恢复 HandConstraint 求解器的功能

Hand Menu Example Large 3

脚本

HandConstraint 行为提供了一个求解器,该求解器将跟踪对象约束在确保可显示手部约束内容(如手部 UI、菜单等)的区域内。安全区域是指不会与手部相交的区域。 还包含了一个名为 HandConstraintPalmUpHandConstraint 派生类,用于演示手掌朝向用户时激活求解器的常见行为。

有关其他文档,请参阅每个 HandConstraint 属性的可用工具提示。 下面更详细地定义了一些属性。

  • 安全区域:安全区域指定了手掌上限制内容的区域。 建议内容放在尺侧,避免与手重叠,提高交互质量。 安全区域的计算方法是将手部方向投影到与相机视野正交的平面中,并针对手部周围的边界框进行光线投射。 安全区域被定义为可以使用 XRNode。 建议探索每个安全区域在不同控制器类型上代表的意义。

  • 跟随手直到面对相机:激活此选项后,求解器将跟随手旋转,直到菜单与用户凝视目光充分对齐,此时手朝向相机。 要实现此目的,请随着 GazeAlignment 以及解算器的变化将 HandConstraintSolver 中的 SolverRotationBehaviorLookAtTrackedObject 更改为 LookAtMainCamera

Hand Menu Example Safe Zone

  • 激活事件:目前 HandConstraint 触发了四个激活事件。 这些事件可以以多种不同的组合使用来创建独特的 HandConstraint 行为。

    • OnHandActivate:当手形满足 IsHandActive 方法时触发
    • OnHandDeactivate:当不再满足 IsHandActive 时触发
    • OnFirstHandDetected:当手部跟踪状态从视野中没有手形变为视野中出现第一个手形时触发。
    • OnLastHandLost:当手部跟踪状态从视野中至少出现一个手形变为视野中没有手形时触发。
  • 求解器激活/停用逻辑:目前,激活和停用 HandConstraintPalmUp 逻辑是通过使用 SolverHandlerUpdateSolver 值来实现的,而不是通过禁用/启用对象来实现的。 在示例场景中,通过在附加菜单的 ManipulationHandler“OnManipulationStarted/Ended”事件后触发的基于编辑器的挂钩即可看到上述效果。

    • 停止手部约束逻辑:尝试将手部约束对象设置为停止(且不运行激活/停用逻辑)时,请将 UpdateSolver 设置为 False,而不是禁用 HandConstraintPalmUp。
      • 如果想启用基于凝视(甚至基于非凝视)的重新附加逻辑,则随后调用 HandConstraintPalmUp.StartWorldLockReattachCheckCoroutine() 函数。 这将触发一个协同例程,然后继续检查“IsValidController”条件是否满足,一旦满足(或对象被禁用),就会将 UpdateSolver 设置为 True。
    • 启动手部约束逻辑:尝试将手部约束对象设置为再次开始跟随你的手部时(根据是否满足激活条件),将 SolverHandler 的 UpdateSolver 设置为 true。
  • 重新附加逻辑:目前,HandConstraintPalmUp 能够自动将目标对象重新附加到跟踪点,无论 SolverHandlerUpdateSolver 是否为 True。 这是在目标对象经世界锁定后通过调用 HandConstraintPalmUpStartWorldLockReattachCheckCoroutine() 函数来完成的(在这种情况下,实际上是将 SolverHandler 的 UpdateSolver 设置为 False)。