Unity中的运动控制器
在 HoloLens 和沉浸式 HMD 中,有两种主要方法可以在Unity、手势和运动控制器中对视线执行作。 可通过 Unity 中的相同 API 访问两个空间输入源的数据。
Unity提供了两种主要方法来访问Windows Mixed Reality的空间输入数据。 常见的 Input.GetButton/Input.GetAxis API 可跨多个Unity XR SDK 工作,而特定于Windows Mixed Reality的 InteractionManager/GestureRecognizer API 公开完整的空间输入数据集。
Unity XR 输入 API
对于新项目,我们建议从一开始就使用新的 XR 输入 API。
可在此处找到有关 XR API 的详细信息。
Unity按钮/轴映射表
Unity的适用于Windows Mixed Reality运动控制器的输入管理器支持通过 Input.GetButton/GetAxis API 列出的按钮和轴 ID。 “Windows MR 特定”列是指 InteractionSourceState 类型中可用的属性。 以下部分详细介绍了其中每个 API。
Windows Mixed Reality的按钮/轴 ID 映射通常与 Oculus 按钮/轴 ID 匹配。
Windows Mixed Reality的按钮/轴 ID 映射与 OpenVR 的映射在两个方面不同:
- 映射使用不同于纵杆的触摸板 ID 来支持具有纵杆和触摸板的控制器。
- 映射可避免重载菜单按钮的 A 和 X 按钮 ID,使它们可用于物理 ABXY 按钮。
Input |
常见Unity API (Input.GetButton/GetAxis) |
特定于 Windows MR 的输入 API (XR。WSA。输入) |
|
---|---|---|---|
左手 | 右手 | ||
选择按下的触发器 | 轴 9 = 1.0 | 轴 10 = 1.0 | selectPressed |
选择触发器模拟值 | 轴 9 | 轴 10 | selectPressedAmount |
选择触发器部分按下 | 按钮 14 (游戏板兼容性) | 按钮 15 (游戏板兼容性) | selectPressedAmount > 0.0 |
已按下“菜单”按钮 | 按钮 6* | 按钮 7* | menuPressed |
已按下手柄按钮 | 轴 11 = 1.0 (无模拟值) 按钮 4 (游戏板兼容性) | 轴 12 = 1.0 (无模拟值) 按钮 5 (游戏板兼容性) | 抓住 |
纵杆 X (左:-1.0,右:1.0) | 轴 1 | 轴 4 | thumbstickPosition.x |
纵杆 Y (顶部: -1.0, 底部: 1.0) | 轴 2 | 轴 5 | thumbstickPosition.y |
按下纵杆 | 按钮 8 | 按钮 9 | thumbstickPressed |
触摸板 X (左:-1.0,右:1.0) | 轴 17* | 轴 19* | touchpadPosition.x |
触摸板 Y (顶部:-1.0,底部:1.0) | 轴 18* | 轴 20* | touchpadPosition.y |
触摸板 | 按钮 18* | 按钮 19* | touchpadTouched |
按下触摸板 | 按钮 16* | 按钮 17* | touchpadPressed |
6DoF 手柄姿势或指针姿势 | 仅抓地力姿势:XR。InputTracking.GetLocalPosition XR。InputTracking.GetLocalRotation | 将 Grip 或 Pointer 作为参数传递:sourceState.sourcePose.TryGetPosition sourceState.sourcePose.TryGetRotation |
|
跟踪状态 | 位置准确性和源丢失风险只能通过特定于 MR 的 API 提供 |
sourceState.sourcePose.positionAccuracy sourceState.properties.sourceLossRisk |
注意
由于游戏板、Oculus Touch 和 OpenVR 使用的映射中存在冲突,这些按钮/轴 ID 不同于Unity用于 OpenVR 的 ID。
OpenXR
若要了解有关Unity中混合现实交互的基础知识,请访问Unity XR 输入Unity手册。 此Unity文档介绍了从特定于控制器的输入到更通用的 InputFeatureUsage的映射、如何识别和分类可用的 XR 输入、如何从这些输入读取数据等。
混合现实 OpenXR 插件提供了其他输入交互配置文件,这些配置文件映射到标准 InputFeatureUsage,详述如下:
InputFeatureUsage | HP Reverb G2 控制器 (OpenXR) | HoloLens Hand (OpenXR) |
---|---|---|
primary2DAxis | 操纵杆 | |
primary2DAxisClick | 游戏杆 - 单击 | |
触发 | Trigger | |
握 | 握 | 空气点击或挤压 |
primaryButton | [X/A] - 按 | Air Tap |
secondaryButton | [Y/B] - 按 | |
gripButton | 抓地力 - 按 | |
triggerButton | 触发器 - 按 | |
menuButton | 菜单 |
抓地力姿势与指向姿势
Windows Mixed Reality支持各种外形规格的运动控制器。 每个控制器的设计在用户手部位置和应用在呈现控制器时应该用于指向的自然“向前”方向之间的关系上有所不同。
为了更好地表示这些控制器,可以针对每个交互源调查两种姿势: 抓地力姿势 和 指针姿势。 抓地力姿势和指针姿势坐标均由所有Unity API 以全局Unity世界坐标表示。
抓地力姿势
抓地力姿势表示用户手掌的位置,由 HoloLens 检测到或握住运动控制器。
在沉浸式头戴显示设备上,抓地力姿势最适合用于呈现 用户的手 或 用户手中握住的对象。 在可视化运动控制器时,还使用抓地力姿势。 Windows 为运动控制器提供的 可渲染模型 使用手柄姿势作为其原点和旋转中心。
抓地力姿势具体定义如下:
- 抓地力位置:自然握住控制器时的手掌质心,向左或向右调整以将手柄内的位置居中。 在Windows Mixed Reality运动控制器上,此位置通常与“抓取”按钮对齐。
- 抓地力方向的右轴:当你完全张开手形成一个扁平的五指姿势时,手掌正常光线 (左手掌向前,从右手掌向后)
- 抓地力方向的向前轴:当你将手部分 (时,好像拿着控制器) 时,光线会“向前”穿过由非拇指形成的管子。
- 抓地力方向的向上轴:右向和向前定义所隐含的向上轴。
可以通过Unity的跨供应商输入 API (XR 访问抓地力姿势。InputTracking。GetLocalPosition/Rotation) 或通过特定于 Windows MR 的 API (sourceState.sourcePose.TryGetPosition/Rotation,请求“抓地力”节点的姿势数据) 。
指针姿势
指针姿势表示控制器向前指尖。
在 呈现控制器模型本身时,系统提供的指针姿势最适合用于光线投射。 如果要呈现其他一些虚拟对象来代替控制器,例如虚拟枪,则应指向该虚拟对象最自然的光线,例如沿应用定义枪模型的枪管移动的光线。 由于用户可以看到虚拟对象,而不是物理控制器,因此,对于使用应用的用户来说,指向虚拟对象可能会更自然。
目前,指针姿势在 Unity中只能通过特定于 Windows MR 的 API sourceState.sourcePose.TryGetPosition/Rotation 提供,并将 InteractionSourceNode.Pointer 作为参数传递。
OpenXR
可以通过 OpenXR 输入交互访问两组姿势:
- 手部呈现对象的抓地力姿势
- 目标提出指向世界。
有关此设计以及两个姿势之间的差异的详细信息,请参阅 OpenXR 规范 - 输入子路径。
由 InputFeatureUsages DevicePosition、 DeviceRotation、 DeviceVelocity 和 DeviceAngularVelocity 提供的姿势都表示 OpenXR 手柄 姿势。 与抓地力姿势相关的 InputFeatureUsages 在 Unity 的 CommonUsages 中定义。
由 InputFeatureUsages PointerPosition、 PointerRotation、 PointerVelocity 和 PointerAngularVelocity 提供的姿势都表示 OpenXR 目标 姿势。 这些 InputFeatureUsage 未在任何包含的 C# 文件中定义,因此需要定义自己的 InputFeatureUsage,如下所示:
public static readonly InputFeatureUsage<Vector3> PointerPosition = new InputFeatureUsage<Vector3>("PointerPosition");
触觉
有关在Unity的 XR 输入系统中使用触觉的信息,请参阅Unity XR 输入 - 触觉Unity手册中的文档。
控制器跟踪状态
与头戴显示设备一样,Windows Mixed Reality运动控制器无需设置外部跟踪传感器。 相反,控制器由头戴显示设备本身中的传感器跟踪。
如果用户将控制器移出头戴显示设备的视野,在大多数情况下,Windows 会继续推断控制器位置。 当控制器失去视觉跟踪的时间足够长时,控制器的位置将下降到近似的准确性位置。
此时,系统将控制器主体锁定给用户,跟踪用户移动时的位置,同时仍然使用控制器的内部方向传感器公开控制器的真实方向。 许多使用控制器指向和激活 UI 元素的应用都可以正常运行,同时大致准确,而用户不会注意到。
显式跟踪状态的推理
希望根据跟踪状态以不同方式处理位置的应用可能会进一步检查控制器状态的属性,例如 SourceLossRisk 和 PositionAccuracy:
跟踪状态 | SourceLossRisk | PositionAccuracy | TryGetPosition |
---|---|---|---|
高准确度 | < 1.0 | 高 | true |
具有丢失) 风险的高可用性 ( | == 1.0 | 高 | true |
近似准确度 | == 1.0 | 近似 | true |
无位置 | == 1.0 | 近似 | false |
这些运动控制器跟踪状态的定义如下:
- 高准确度: 虽然运动控制器位于头戴显示设备的视野内,但它通常会基于视觉跟踪提供高准确度的位置。 暂时离开视场或暂时从头戴显示设备传感器 (遮挡的移动控制器,例如用户的另一只手) 将基于控制器本身的惯性跟踪,在短时间内继续返回高准确度姿势。
- 具有丢失) 风险的高可用性 (: 当用户将运动控制器移动到头戴显示设备视野边缘时,头戴显示设备将很快无法直观地跟踪控制器的位置。 应用通过看到 SourceLossRisk 达到 1.0,知道控制器何时达到此 FOV 边界。 此时,应用可以选择暂停需要稳定高质量姿势流的控制器手势。
- 近似准确度: 当控制器失去视觉跟踪的时间足够长时,控制器的位置将下降到近似的准确性位置。 此时,系统将控制器主体锁定给用户,跟踪用户移动时的位置,同时仍然使用控制器的内部方向传感器公开控制器的真实方向。 许多使用控制器指向和激活 UI 元素的应用都可以正常运行,同时大致准确,而用户不会注意到。 输入要求较重的应用可能会选择通过检查 PositionAccuracy 属性来感知从高准确度到近似准确度的下降,例如,在此期间,在屏幕外目标上为用户提供更慷慨的点击框。
- 无位置: 虽然控制器可以长时间以大致的准确性运行,但有时系统知道,即使身体锁定位置目前也无意义。 例如,打开的控制器可能从未被直观地观察到,或者用户可能会放下控制器,然后由其他人选取。 在这些时候,系统不会向应用提供任何位置, 并且 TryGetPosition 将返回 false。
(Input.GetButton/GetAxis) 的常见Unity API
Namespace:UnityEngine、 UnityEngine.XR
类型: 输入、 XR。InputTracking
Unity当前使用其通用 Input.GetButton/Input.GetAxis API 来公开 Oculus SDK、OpenVR SDK 和Windows Mixed Reality(包括手部和运动控制器)的输入。 如果你的应用使用这些 API 进行输入,则可以轻松地跨多个 XR SDK(包括Windows Mixed Reality)支持运动控制器。
获取逻辑按钮的按下状态
若要使用常规Unity输入 API,通常首先将按钮和轴连接到Unity输入管理器中的逻辑名称,将按钮或轴 ID 绑定到每个名称。 然后,可以编写引用该逻辑按钮/轴名称的代码。
例如,若要将左侧运动控制器的触发器按钮映射到“提交”作,请转到Unity中的“编辑>项目设置输入”,然后展开“轴”下的“提交”>部分的属性。 将 “正向按钮” 或 “替换正按钮” 属性更改为阅读 游戏杆按钮 14,如下所示:
Unity InputManager
然后,脚本可以使用 Input.GetButton 为“提交”作检查:
if (Input.GetButton("Submit"))
{
// ...
}
可以通过更改 Axes 下的 Size 属性来添加更多逻辑按钮。
直接获取物理按钮的按下状态
还可以使用 Input.GetKey 按按钮的完全限定名称手动访问按钮:
if (Input.GetKey("joystick button 8"))
{
// ...
}
获取手部或运动控制器的姿势
可以使用 XR 访问控制器的位置和旋转 。InputTracking:
Vector3 leftPosition = InputTracking.GetLocalPosition(XRNode.LeftHand);
Quaternion leftRotation = InputTracking.GetLocalRotation(XRNode.LeftHand);
注意
上述代码表示控制器的抓地姿势 (用户持有控制器) ,这对于在用户手中呈现剑或枪或控制器本身的模型非常有用。
此抓地力姿势与指针姿势之间的关系 (控制器的尖端指向) 可能因控制器而异。 目前,只能通过特定于 MR 的输入 API 访问控制器的指针姿势,如下部分所述。
特定于 Windows 的 API (XR。WSA。输入)
警告
如果项目正在使用任何 XR。WSA API,这些 API 将逐步淘汰,以在未来Unity版本中使用 XR SDK。 对于新项目,我们建议从一开始就使用 XR SDK。 可在此处找到有关 XR 输入系统和 API 的详细信息。
Namespace:UnityEngine.xr.WSA.Input
类型: InteractionManager、 InteractionSourceState、 InteractionSource、 InteractionSourceProperties、 InteractionSourceKind、 InteractionSourceLocation
若要获取有关 HoloLens) 和运动控制器Windows Mixed Reality手动输入 (的更多详细信息,可以选择使用 UnityEngine.XR.WSA.Input 命名空间下的特定于 Windows 的空间输入 API。 这使你可以访问其他信息,例如位置准确性或源类型,使你可以区分手和控制器。
轮询手部和运动控制器的状态
可以使用 GetCurrentReading 方法轮询每个交互源 (手部或运动控制器) 此帧的状态。
var interactionSourceStates = InteractionManager.GetCurrentReading();
foreach (var interactionSourceState in interactionSourceStates) {
// ...
}
返回的每个 InteractionSourceState 都表示当前时刻的交互源。 InteractionSourceState 公开如下信息:
( Select/Menu/Grasp/Touchpad/Thumbstick)
if (interactionSourceState.selectPressed) { // ... }
特定于运动控制器的其他数据,例如触摸板和/或纵杆的 XY 坐标和触摸状态
if (interactionSourceState.touchpadTouched && interactionSourceState.touchpadPosition.x > 0.5) { // ... }
用于知道源是手部还是运动控制器的 InteractionSourceKind
if (interactionSourceState.source.kind == InteractionSourceKind.Hand) { // ... }
轮询前向预测呈现姿势
从手部和控制器轮询交互源数据时,你得到的姿势是向前预测的姿势,此时此帧的光子将到达用户眼睛。 前向预测姿势最适合用于 呈现 控制器或每个帧的保留对象。 如果使用控制器面向给定的新闻或发布,如果使用下面所述的历史事件 API,则最准确。
var sourcePose = interactionSourceState.sourcePose; Vector3 sourceGripPosition; Quaternion sourceGripRotation; if ((sourcePose.TryGetPosition(out sourceGripPosition, InteractionSourceNode.Grip)) && (sourcePose.TryGetRotation(out sourceGripRotation, InteractionSourceNode.Grip))) { // ... }
还可以获取此当前帧的向前预测头部姿势。 与源姿势一样,这对于 呈现 游标很有用,不过,如果使用下面所述的历史事件 API,则以给定的新闻或发布为目标是最准确的。
var headPose = interactionSourceState.headPose; var headRay = new Ray(headPose.position, headPose.forward); RaycastHit raycastHit; if (Physics.Raycast(headPose.position, headPose.forward, out raycastHit, 10)) { var cursorPos = raycastHit.point; // ... }
处理交互源事件
若要使用准确的历史姿势数据处理输入事件,可以处理交互源事件,而不是轮询。
处理交互源事件:
注册 InteractionManager 输入事件。 对于你感兴趣的每种类型的交互事件,需要订阅它。
InteractionManager.InteractionSourcePressed += InteractionManager_InteractionSourcePressed;
处理事件。 订阅交互事件后,将在适当时获得回调。 在 SourcePressed 示例中,这将是在检测到源之后以及释放或丢失之前。
void InteractionManager_InteractionSourceDetected(InteractionSourceDetectedEventArgs args) var interactionSourceState = args.state; // args.state has information about: // targeting head ray at the time when the event was triggered // whether the source is pressed or not // properties like position, velocity, source loss risk // source id (which hand id for example) and source kind like hand, voice, controller or other }
如何停止处理事件
如果不再对事件感兴趣,或者正在销毁已订阅该事件的对象,则需要停止处理事件。 若要停止处理事件,请取消订阅事件。
InteractionManager.InteractionSourcePressed -= InteractionManager_InteractionSourcePressed;
交互源事件列表
可用的交互源事件包括:
- interactionSourceDetected (源变为活动)
- InteractionSourceLost (变为非活动)
- InteractionSourcePressed (点击、按按钮或“选择”)
- InteractionSourceReleased (点击结束、释放按钮或“选择”结束)
- InteractionSourceUpdated (移动或以其他方式更改某些状态)
最准确地匹配新闻或发布的历史定位姿势的事件
前面所述的轮询 API 为应用提供向前预测姿势。 虽然这些预测姿势最适合呈现控制器或虚拟手持物体,但未来的姿势并不是最佳定位,原因有两个关键:
- 当用户按下控制器上的按钮时,在系统收到按钮之前,蓝牙可能会有大约 20 毫秒的无线延迟。
- 然后,如果使用前向预测姿势,则会应用另外 10-20 毫秒的向前预测,以定位当前帧的光子到达用户眼睛的时间。
这意味着,投票会为你提供一个源姿势或头部姿势,该姿势或头部姿势是 30-40 毫秒前移,当用户的头部和手在新闻或释放发生时实际返回的位置。 对于 HoloLens 手动输入,虽然没有无线传输延迟,但也有类似的处理延迟来检测按下。
若要根据用户手部或控制器按下的原始意图准确定位,应使用该 InteractionSourcePressed 或 InteractionSourceReleased 输入事件的历史源姿势或头部姿势。
可以使用来自用户头部或其控制器的历史姿势数据来定位新闻或发布:
发生手势或控制器按下时头部姿势,可用于 确定 用户正在 注视 的内容:
void InteractionManager_InteractionSourcePressed(InteractionSourcePressedEventArgs args) { var interactionSourceState = args.state; var headPose = interactionSourceState.headPose; RaycastHit raycastHit; if (Physics.Raycast(headPose.position, headPose.forward, out raycastHit, 10)) { var targetObject = raycastHit.collider.gameObject; // ... } }
运动控制器按下时的源姿势,可用于 确定 用户指向控制器的目标位置。 这是遇到压力的控制器的状态。 如果要呈现控制器本身,可以请求指针姿势而不是抓地力姿势,以从用户认为该呈现控制器的自然尖端拍摄目标射线:
void InteractionManager_InteractionSourcePressed(InteractionSourcePressedEventArgs args) { var interactionSourceState = args.state; var sourcePose = interactionSourceState.sourcePose; Vector3 sourceGripPosition; Quaternion sourceGripRotation; if ((sourcePose.TryGetPosition(out sourceGripPosition, InteractionSourceNode.Pointer)) && (sourcePose.TryGetRotation(out sourceGripRotation, InteractionSourceNode.Pointer))) { RaycastHit raycastHit; if (Physics.Raycast(sourceGripPosition, sourceGripRotation * Vector3.forward, out raycastHit, 10)) { var targetObject = raycastHit.collider.gameObject; // ... } } }
事件处理程序示例
using UnityEngine.XR.WSA.Input;
void Start()
{
InteractionManager.InteractionSourceDetected += InteractionManager_InteractionSourceDetected;
InteractionManager.InteractionSourceLost += InteractionManager_InteractionSourceLost;
InteractionManager.InteractionSourcePressed += InteractionManager_InteractionSourcePressed;
InteractionManager.InteractionSourceReleased += InteractionManager_InteractionSourceReleased;
InteractionManager.InteractionSourceUpdated += InteractionManager_InteractionSourceUpdated;
}
void OnDestroy()
{
InteractionManager.InteractionSourceDetected -= InteractionManager_InteractionSourceDetected;
InteractionManager.InteractionSourceLost -= InteractionManager_InteractionSourceLost;
InteractionManager.InteractionSourcePressed -= InteractionManager_InteractionSourcePressed;
InteractionManager.InteractionSourceReleased -= InteractionManager_InteractionSourceReleased;
InteractionManager.InteractionSourceUpdated -= InteractionManager_InteractionSourceUpdated;
}
void InteractionManager_InteractionSourceDetected(InteractionSourceDetectedEventArgs args)
{
// Source was detected
// args.state has the current state of the source including id, position, kind, etc.
}
void InteractionManager_InteractionSourceLost(InteractionSourceLostEventArgs state)
{
// Source was lost. This will be after a SourceDetected event and no other events for this
// source id will occur until it is Detected again
// args.state has the current state of the source including id, position, kind, etc.
}
void InteractionManager_InteractionSourcePressed(InteractionSourcePressedEventArgs state)
{
// Source was pressed. This will be after the source was detected and before it is
// released or lost
// args.state has the current state of the source including id, position, kind, etc.
}
void InteractionManager_InteractionSourceReleased(InteractionSourceReleasedEventArgs state)
{
// Source was released. The source would have been detected and pressed before this point.
// This event will not fire if the source is lost
// args.state has the current state of the source including id, position, kind, etc.
}
void InteractionManager_InteractionSourceUpdated(InteractionSourceUpdatedEventArgs state)
{
// Source was updated. The source would have been detected before this point
// args.state has the current state of the source including id, position, kind, etc.
}
MRTK 中的运动控制器
可以从输入管理器访问 手势和运动控制器 。
按照教程进行作
混合现实 Academy 中提供了分步教程以及更详细的自定义示例:
下一个开发检查点
如果你遵循我们布局的Unity开发旅程,则你正在探索 MRTK 核心构建基块。 在此处,可以继续下一个构建基块:
或者跳转到混合现实平台功能和 API:
可以随时返回到Unity开发检查点。