DirectX 中的手和运动控制器
注意
本文与旧版 WinRT 原生 API 相关。 对于新的本机应用项目,建议使用 OpenXR API。
在 Windows Mixed Reality 中,手和运动控制器输入都是通过空间输入 API(可在 Windows.UI.Input.Spatial 命名空间中找到)来处理的。 这使你能够轻松处理常见操作,例如在手和运动控制器中“选择”以相同方式按下。
使用入门
若要访问 Windows Mixed Reality 中的空间输入,请从 SpatialInteractionManager 接口开始。 可以通过调用 SpatialInteractionManager::GetForCurrentView 访问此接口,通常在应用启动期间进行。
using namespace winrt::Windows::UI::Input::Spatial;
SpatialInteractionManager interactionManager = SpatialInteractionManager::GetForCurrentView();
SpatialInteractionManager 的作业是提供对 SpatialInteractionSources 的访问,它表示一个输入源。 系统中提供了三种 SpatialInteractionSources。
- 手表示用户检测到的手。 手源根据设备提供不同的功能,从 HoloLens 的基本手势到 HoloLens 2 的全关节手部跟踪。
- 控制器表示配对的运动控制器。 运动控制器可以提供不同功能,例如,“选择”触发器、“菜单”按钮、“抓取”按钮、触摸板和控制杆。
- 语音表示用户语音系统检测到的关键字。 例如,每当用户发出“选择”口令时,此源都会注入“选择”按下和释放。
源的每帧数据由 SpatialInteractionSourceState 接口表示。 可以通过两种不同的方式来访问此数据,这取决于你是要在应用程序中使用基于事件驱动的模型还是基于轮询的模型。
事件驱动输入
SpatialInteractionManager 提供了多个应用可以侦听的事件。 一些示例包括 SourcePressed、[SourceReleased 和 SourceUpdated。
例如,下面的代码将名为 MyApp::OnSourcePressed 的事件处理程序挂接到 SourcePressed 事件。 这允许应用检测任何类型交互源的按压操作。
using namespace winrt::Windows::UI::Input::Spatial;
auto interactionManager = SpatialInteractionManager::GetForCurrentView();
interactionManager.SourcePressed({ this, &MyApp::OnSourcePressed });
此按压事件将在按下时与相应的 SpatialInteractionSourceState 一起以异步方式发送到你的应用。 应用或游戏引擎可能希望立即开始处理,或在输入处理例程中对事件数据进行排队。 下面是 SourcePressed 事件的事件处理程序函数,用于检查是否已按下“选择”按钮。
using namespace winrt::Windows::UI::Input::Spatial;
void MyApp::OnSourcePressed(SpatialInteractionManager const& sender, SpatialInteractionSourceEventArgs const& args)
{
if (args.PressKind() == SpatialInteractionPressKind::Select)
{
// Select button was pressed, update app state
}
}
上述代码仅检查“选择”是否按下,它对应于设备上的主要操作。 示例包括在 HoloLens 执行隔空敲击或在运动控制器上拉取触发器。 “选择”按下表示用户激活其目标全息映像的意图。 将为多个不同的按钮和手势触发 SourcePressed 事件,你可以检查 SpatialInteractionSource 上的其他属性,以测试这些情况。
基于轮询的输入
还可以使用 SpatialInteractionManager 轮询每个帧的当前输入状态。 为此,请在每个帧上调用 GetDetectedSourcesAtTimestamp。 对于每个活动的 SpatialInteractionSource,该函数返回一个包含 SpatialInteractionSourceState 的数组。 这意味着每个活动运动控制器、每个跟踪手部和语音(如果发出“选择”口令)都有一个数组。 然后,你可以检查每个 SpatialInteractionSourceState 上的属性以将输入驱动到你的应用程序中。
下面的示例演示如何使用轮询方法检查“选择”操作。 prediction 变量表示 HolographicFramePrediction 对象,可从 HolographicFrame 中获取。
using namespace winrt::Windows::UI::Input::Spatial;
auto interactionManager = SpatialInteractionManager::GetForCurrentView();
auto sourceStates = m_spatialInteractionManager.GetDetectedSourcesAtTimestamp(prediction.Timestamp());
for (auto& sourceState : sourceStates)
{
if (sourceState.IsSelectPressed())
{
// Select button is down, update app state
}
}
每个 SpatialInteractionSource 都有一个 ID,可用于标识新源并将现有源从帧关联到帧。 手每次离开并进入 FOV 时,都会获得新 ID,但在会话期间,控制器 ID 将保持不变。 你可以使用 SpatialInteractionManager 上的事件(如 SourceDetected 和 SourceLost),在手进入或离开设备视野,或者当运动控制器开启/关闭或配对/为配对时做出反应。
预测手势与历史手势
GetDetectedSourcesAtTimestamp 具有 timestamp 参数。 这使你可以请求预测的或历史记录的状态和手势数据,从而使你可以将空间交互与其他输入源关联起来。 例如,在呈现当前帧中的手位置时,可以传入由 HolographicFrame 提供的预测时间戳。 这样一来,系统就可以向前预测手位置,使其与呈现的帧输出保持一致,从而最大程度减少感觉滞后时间。
但是,此类预测手势并不会生成瞄准交互源的理想指向射线。 例如,按下运动控制器按钮时,该事件可能需要 20 毫秒的时间才能通过蓝牙弹出到操作系统。 同样,在用户执行手势后,在系统检测到手势之前可能需要一定的时间,然后应用才会轮询该手势。 当应用轮询状态变化时,用于瞄准此交互的头部姿势和手势实际上在过去发生。 如果你通过将当前 HolographicFrame 的时间戳传递给 GetDetectedSourcesAtTimestamp 来锁定目标,则在显示帧时,姿势将被向前预测到目标射线,这可能在未来超过 20 毫秒。 这种未来姿势适用于呈现交互源,但是对于锁定交互目标而言,会加剧我们的时间问题,因为用户目标发生在过去。
幸运的是,SourcePressed、[SourceReleased 和 SourceUpdated 事件提供与每个输入事件关联的历史状态。 这直接包括通过 TryGetPointerPose 提供的历史头部姿势和手势,以及可传递给其他 API 以与此事件关联的历史时间戳。
这会在每个帧使用手和控制器呈现和定位时提供以下最佳做法:
- 对于每个帧的“手/控制器呈现”,应用应在当前帧的光子时间轮询每个交互源的向前预测姿势。 可以通过调用每个帧的 GetDetectedSourcesAtTimestamp 来轮询所有交互源,并传入 HolographicFrame::CurrentPrediction 提供的预测时间戳。
- 对于在按下或释放时的手/控制器目标,应用应处理按下/释放事件,根据该事件的历史头部姿势和手势进行光线投射。 可以通过以下方式获取此目标射线:处理 SourcePressed 或 SourceReleased 事件,从事件参数获取 State属性,然后调用其 TryGetPointerPose 方法。
跨设备输入属性
SpatialInteractionSource API 支持具有各种功能的控制器和手部跟踪系统。 许多这些功能在设备类型之间是通用的。 例如,手部跟踪和运动控制器都提供“选择”操作和 3D 位置。 只要有可能,API 就会将这些常见功能映射到 SpatialInteractionSource 上的相同属性。 这使应用程序能够更轻松地支持各种输入类型。 下表介绍了支持的属性,以及它们如何跨输入类型进行比较。
properties | 说明 | HoloLens(第一代)手势 | 运动控制器 | 关节手 |
---|---|---|---|---|
SpatialInteractionSource::Handedness | 右手或左手/控制器。 | 不支持 | 支持 | 支持 |
SpatialInteractionSourceState::IsSelectPressed | 主要按钮的当前状态。 | 隔空敲击 | 触发器 | 随意隔空敲击(纵向收缩) |
SpatialInteractionSourceState::IsGrasped | “抓取”按钮的当前状态。 | 不支持 | “抓取”按钮 | 收缩或握拳 |
SpatialInteractionSourceState::IsMenuPressed | “菜单”按钮的当前状态。 | 不支持 | “菜单”按钮 | 不支持 |
SpatialInteractionSourceLocation::Position | 控制器上手或抓握位置的 XYZ 位置。 | 手掌位置 | 抓握手势位置 | 手掌位置 |
SpatialInteractionSourceLocation::Orientation | 表示手或抓握姿势在控制器上的方向的四元数。 | 不支持 | 抓握姿势方向 | 手掌方向 |
SpatialPointerInteractionSourcePose::Position | 指向射线的原点。 | 不支持 | 支持 | 支持 |
SpatialPointerInteractionSourcePose::ForwardDirection | 指向射线的方向。 | 不支持 | 支持 | 支持 |
上述一些属性在所有设备上都不可用,API 提供了一种方法来测试这一点。 例如,你可以检查 SpatialInteractionSource::IsGraspSupported 属性来确定源是否提供抓取操作。
抓握姿势与指向姿势
Windows Mixed Reality 支持不同外形规格的运动控制器。 它还支持关节手部跟踪系统。 所有这些系统在手部位置和应用用来指向或呈现用户手中持有对象的自然“向前”方向之间都有不同的关系。 为支持所有此类情况,可为手部跟踪和运动控制器提供两种类型的 3D 姿势。 第一种是抓握姿势,表示用户的手位置。 第二个是指向姿势,它表示源自用户手或控制器的指向射线。 因此,如果你想要呈现用户手部或用户手中持有的对象(例如剑或枪),请使用抓握姿势。 如果你想要从控制器或手进行光线投射,例如当用户指向 UI 时,则使用指向姿势。
可以通过 SpatialInteractionSourceState::Properties::TryGetLocation(...) 访问抓握姿势。定义如下:
- 抓握位置:自然持有控制器时的手掌质心,向左或向右调整以使位置在抓握范围内。
- 抓握方向的右轴:当你完全打开手部以形成平展的 5 指姿势时,垂直于手掌的光线(指向左手掌前方,指向右手掌后方)
- 抓握方向的前轴:当你部分握紧手时(就如同持有控制器一样),通过手指关节形成的管道指向“前方”的射线。
- 抓握方向的上轴:由向右和向前定义默示的上轴。
可以通过 SpatialInteractionSourceState::Properties::TryGetLocation(...)::SourcePointerPose 或 SpatialInteractionSourceState::TryGetPointerPose(...)::TryGetInteractionSourcePose 访问指针姿势。
控制器特定的输入属性
对于控制器,SpatialInteractionSource 具有包含其他功能的 Controller 属性。
- HasThumbstick:如果为 true,则控制器具有控制杆。 检查 SpatialInteractionSourceState 的 ControllerProperties 属性,以获取控制杆 x 和 y 值(ThumbstickX 和 ThumbstickY)及其按下状态 (IsThumbstickPressed)。
- HasTouchpad:如果为 true,则控制器具有触摸板。 检查 SpatialInteractionSourceState 的 ControllerProperties 属性,以获取触摸板的 x 和 y 值(TouchpadX 和 TouchpadY),并了解用户是否触摸板 (IsTouchpadTouched),以及是否按下了触摸板 (IsTouchpadPressed)。
- SimpleHapticsController:控制器的 SimpleHapticsController API 可用于检查控制器的触觉功能,还可用于控制触觉反馈。
对于两个轴,触摸板和控制杆的范围为 -1 到 1(从下到上,以及从左到右)。 使用 SpatialInteractionSourceState::SelectPressedValue 属性访问的模拟触发器的范围为 0 到 1。 1 值与等于 true 的 IsSelectPressed 关联;其他任何值与等于 false 的 IsSelectPressed 关联。
关节手部跟踪
Windows Mixed Reality API 为关节手部跟踪提供完全支持,例如在 HoloLens 2 上。 可在应用程序中使用关节手部跟踪来实现直接操作和指向并提交输入模型。 它还可用于创作完全自定义交互。
手部骨骼
关节手部跟踪提供了 25 个关节骨骼,能够实现许多不同类型的交互。 骨骼包含食指/中指/无名指/小指五个关节,拇指四个关节,还有一个腕关节。 腕关节是层次结构的基础。 下图说明了骨骼布局。
在大多数情况下,每个关节都根据它所表示的骨头来命名。 因为每个关节处都有两块骨头,所以我们根据该位置的子骨来命名每个关节。 子骨的定义是离手腕较远的骨头。 例如,“食指近端”关节包含食指近端骨的起始位置和该骨头的方向。 它不包含骨头的结束位置。 如果需要,你可以从层次结构的下一个关节处获得结束位置,即“食指中间”关节。
除了 25 个分层关节外,该系统还提供了手掌关节。 手掌通常不被视为骨骼结构的一部分。 它只是作为一种方便的方法来获得手的整体位置和方向。
对于每个关节,提供了以下信息:
名称 | 描述 |
---|---|
Position | 关节的 3D 位置,可在任何请求的坐标系统中获得。 |
方向 | 骨头的 3D 方向,可在任何请求的坐标系统中获得。 |
半径 | 到关节位置皮肤表面的距离。 适用于调整依赖于手指宽度的直接交互或可视化效果。 |
准确性 | 提示系统对此关节信息的置信度。 |
可以通过 SpatialInteractionSourceState 上的函数访问手部骨骼数据。 该函数名为 TryGetHandPose,并返回名为 HandPose 的对象。 如果源不支持关节手,则此函数将返回 null。 有了 HandPose 后,就可以通过调用 TryGetJoint 来获取当前关节数据,并使用你感兴趣的关节名称。 数据返回为 JointPose 结构。 下面的代码获取食指指尖的位置。 变量 currentState 表示 SpatialInteractionSourceState 实例。
using namespace winrt::Windows::Perception::People;
using namespace winrt::Windows::Foundation::Numerics;
auto handPose = currentState.TryGetHandPose();
if (handPose)
{
JointPose joint;
if (handPose.TryGetJoint(desiredCoordinateSystem, HandJointKind::IndexTip, joint))
{
float3 indexTipPosition = joint.Position;
// Do something with the index tip position
}
}
手部网格
关节手部跟踪 API 允许使用完全可变形的三角形手部网格。 此网格可以随手部骨骼实时变形,对可视化效果和高级物理技术非常有用。 若要访问手部网格,需要先通过调用 SpatialInteractionSource 上的 TryCreateHandMeshObserverAsync 来创建 HandMeshObserver 对象。 每个源仅需执行一次此操作,通常是在第一次看到它时。 这意味着,只要手进入 FOV,就要调用此函数以创建 HandMeshObserver 对象。 这是一个异步函数,因此你必须在此处理一些并发操作。 一旦可用,你可以通过调用 GetTriangleIndices 来向 HandMeshObserver 对象请求三角形索引缓冲区。 索引不会在帧之间更改,因此你可以一次获取这些索引,并在源的生存期内缓存它们。 索引按顺时针缠绕顺序提供。
下面的代码将向上旋转分离的 std::thread 来创建网格观察器并在网格观察器可用后提取索引缓冲区。 它从名为 currentState 的变量开始,该变量是表示跟踪手的 SpatialInteractionSourceState 实例。
using namespace Windows::Perception::People;
std::thread createObserverThread([this, currentState]()
{
HandMeshObserver newHandMeshObserver = currentState.Source().TryCreateHandMeshObserverAsync().get();
if (newHandMeshObserver)
{
unsigned indexCount = newHandMeshObserver.TriangleIndexCount();
vector<unsigned short> indices(indexCount);
newHandMeshObserver.GetTriangleIndices(indices);
// Save the indices and handMeshObserver for later use - and use a mutex to synchronize access if needed!
}
});
createObserverThread.detach();
启动分离线程只是处理异步调用的一种选择。 你也可以使用 C++/WinRT 支持的新 co_await 功能。
获得 HandMeshObserver 对象后,应在其对应的 SpatialInteractionSource 处于活动状态的持续时间内保留该对象。 然后对于每个帧,你可以通过调用 GetVertexStateForPose,并传递一个 HandPose 实例(它代表你所需顶点的姿势),要求它提供最新的顶点缓冲区。 缓冲区中的每个顶点都有一个位置和一根法线。 下面的示例演示如何获手部网格的当前顶点集。 如前所述,currentState 变量表示 SpatialInteractionSourceState 实例。
using namespace winrt::Windows::Perception::People;
auto handPose = currentState.TryGetHandPose();
if (handPose)
{
std::vector<HandMeshVertex> vertices(handMeshObserver.VertexCount());
auto vertexState = handMeshObserver.GetVertexStateForPose(handPose);
vertexState.GetVertices(vertices);
auto meshTransform = vertexState.CoordinateSystem().TryGetTransformTo(desiredCoordinateSystem);
if (meshTransform != nullptr)
{
// Do something with the vertices and mesh transform, along with the indices that you saved earlier
}
}
与骨骼关节不同,手部网格 API 不允许为顶点指定坐标系统。 相反,HandMeshVertexState 指定提供顶点的坐标系统。 然后,你可以通过调用 TryGetTransformTo 并指定你所需的坐标系统,获取网格转换。 每当使用顶点时,都需要使用此网格转换。 此方法可减少 CPU 开销,尤其是在仅将网格用于呈现目的时。
凝视和提交复合手势
对于使用凝视和提交输入模型的应用程序,尤其是在 HoloLens(第一代)上,空间输入 API 提供了可选的 SpatialGestureRecognizer,可用于启用基于“select”事件构建的复合手势。 通过将交互从 SpatialInteractionManager 路由到全息影像的 SpatialGestureRecognizer,应用可以跨受、语义和空间输入设备统一检测点击、按住、操作和导航事件,而无需手动处理按下和释放。
SpatialGestureRecognizer 仅对请求的一组手势进行最小区分。 例如,如果只请求点击,那么只要用户愿意,他们可按住手指,然后仍然会出现点击。 如果同时请求点击并按住,在按住手指大约一秒后,手势将提升为按住,并且不再发生点击。
若要使用 SpatialGestureRecognizer,请处理 SpatialInteractionManager 的 InteractionDetected 事件,并抓取其中显示的 SpatialPointerPose。 使用此姿势中的用户头部凝视射线与用户周围的全息影像和表面网格相交,以确定用户打算与哪些对象交互。 然后,使用 CaptureInteraction 方法将事件参数中的 SpatialInteraction 路由到目标全息影像的 SpatialGestureRecognizer。 这将开始根据创建时该识别器上设置的 SpatialGestureSettings 或 TrySetGestureSettings 解释该交互。
在 HoloLens(第一代)上,交互和手势应从用户的头部凝视派生其目标,而不是在手的位置呈现或交互。 启动交互后,手的相对运动可用于控制手势,就像操作或导航手势一样。