DirectX 中的头部跟踪视线和眼睛视线输入
注意
本文与旧版 WinRT 原生 API 相关。 对于新的本机应用项目,建议使用 OpenXR API。
在 Windows Mixed Reality 中,眼睛视线和头部跟踪视线输入用于确定用户正在查看的内容。 可以使用这些数据来驱动主输入模型,如头部跟踪视线和提交,并为不同的交互类型提供上下文。 通过 API 可以提供两种类型的视线向量:头部跟踪视线和眼睛视线。 两者都作为具有原点和方向的三维射线提供。 然后,应用程序可以光线投射到其场景或真实世界,并确定用户的目标。
头部跟踪视线表示用户头部指向的方向。 将头部跟踪视线视为设备本身的位置和前进方向,其位置是两个显示器之间的中心点。 头部跟踪视线适用于所有混合现实设备。
眼睛视线表示用户眼睛所看的方向。 原点位于用户的眼睛之间。 它适用于包含眼动跟踪系统的混合现实设备。
头部跟踪视线和眼睛视线都可通过 SpatialPointerPose API 来访问。 调用 SpatialPointerPose::TryGetAtTimestamp 来接收指定时间戳和坐标系的新 SpatialPointerPose 对象。 此 SpatialPointerPose 包含头部跟踪视线原点和方向。 它还包含眼睛视线原点和方向(如果眼动跟踪可用)。
设备支持
功能 | HoloLens(第一代) | HoloLens 2 | 沉浸式头戴显示设备 |
头部凝视 | ✔ | ✔ | ✔ |
眼部凝视 | ❌ | ✔ | ❌ |
使用头部跟踪视线
若要访问头部跟踪视线,请首先调用 SpatialPointerPose::TryGetAtTimestamp 来接收一个新的 SpatialPointerPose 对象。 传递以下参数。
- SpatialCoordinateSystem,表示头部跟踪视线所需的坐标系。 这由以下代码中的 coordinateSystem 变量表示。 有关详细信息,请访问坐标系开发人员指南。
- Timestamp,表示所请求头部姿势的确切时间。 通常,你将使用与当前帧的显示时间对应的时间戳。 可以从 HolographicFramePrediction 对象中获取此预测的显示时间戳,该对象可通过当前 HolographicFrame 进行访问。 此 HolographicFramePrediction 对象由以下代码中的 prediction 变量表示。
获取有效的 SpatialPointerPose 后,可将头部位置和前进方向作为属性来访问。 以下代码演示了如何访问这些信息。
using namespace winrt::Windows::UI::Input::Spatial;
using namespace winrt::Windows::Foundation::Numerics;
SpatialPointerPose pointerPose = SpatialPointerPose::TryGetAtTimestamp(coordinateSystem, prediction.Timestamp());
if (pointerPose)
{
float3 headPosition = pointerPose.Head().Position();
float3 headForwardDirection = pointerPose.Head().ForwardDirection();
// Do something with the head-gaze
}
使用眼睛视线
若要让用户使用眼睛视线输入,每个用户在首次使用设备时必须完成眼动跟踪用户校准。 眼睛视线 API 与头部跟踪视线类似。 它使用相同的 SpatialPointerPose API,该 API 提供可对场景进行光线投射的射线原点和方向。 唯一的区别是,在使用眼动跟踪之前,需要显式启用它:
- 请求在应用中使用眼动跟踪的用户权限。
- 在程序包清单中启用“视线输入”功能。
请求访问眼睛视线输入
在启动应用时,调用 EyesPose::RequestAccessAsync 以请求访问眼动跟踪。 系统将根据需要提示用户,并在授予访问权限后返回 GazeInputAccessStatus::Allowed。 这是一个异步调用,因此它需要一些额外的管理。 以下示例启动一个分离的 std::thread 来等待结果,它将结果存储在名为 m_isEyeTrackingEnabled 的成员变量中。
using namespace winrt::Windows::Perception::People;
using namespace winrt::Windows::UI::Input;
std::thread requestAccessThread([this]()
{
auto status = EyesPose::RequestAccessAsync().get();
if (status == GazeInputAccessStatus::Allowed)
m_isEyeTrackingEnabled = true;
else
m_isEyeTrackingEnabled = false;
});
requestAccessThread.detach();
启动分离线程只是处理异步调用的一种选择。 你也可以使用 C++/WinRT 支持的新 co_await 功能。 下面是另一个请求用户权限的示例:
- EyesPose::IsSupported() 允许应用程序仅在有眼动追踪仪的情况下才触发权限对话框。
- GazeInputAccessStatus m_gazeInputAccessStatus; // 这是为了防止一次又一次地弹出权限提示。
GazeInputAccessStatus m_gazeInputAccessStatus; // This is to prevent popping up the permission prompt over and over again.
// This will trigger to show the permission prompt to the user.
// Ask for access if there is a corresponding device and registry flag did not disable it.
if (Windows::Perception::People::EyesPose::IsSupported() &&
(m_gazeInputAccessStatus == GazeInputAccessStatus::Unspecified))
{
Concurrency::create_task(Windows::Perception::People::EyesPose::RequestAccessAsync()).then(
[this](GazeInputAccessStatus status)
{
// GazeInputAccessStatus::{Allowed, DeniedBySystem, DeniedByUser, Unspecified}
m_gazeInputAccessStatus = status;
// Let's be sure to not ask again.
if(status == GazeInputAccessStatus::Unspecified)
{
m_gazeInputAccessStatus = GazeInputAccessStatus::DeniedBySystem;
}
});
}
声明“视线输入”功能
在“解决方案资源管理器”中双击“appxmanifest”文件。 然后,导航到“功能”部分并选中“凝视输入”功能。
这将向 appxmanifest 文件的“包”部分中添加以下行:
<Capabilities>
<DeviceCapability Name="gazeInput" />
</Capabilities>
获取眼睛视线
获取对 ET 的访问权限后,可以随意抓取每个帧的眼睛视线。 与头部跟踪视线一样,通过调用 SpatialPointerPose::TryGetAtTimestamp 来获取包含所需的时间戳和坐标系的 SpatialPointerPose。 SpatialPointerPose 通过 Eyes 属性包含一个 EyesPose 对象。 仅当启用了眼动跟踪时,此项才为非 null。 从这里,可以通过调用 EyesPose::IsCalibrationValid 来检查设备中的用户是否已完成眼动跟踪校准。 接下来,使用 Gaze 属性获取包含眼睛视线位置和方向的 SpatialRay。 Gaze 属性有时可为 null,因此请务必检查这一点。 如果已校准的用户暂时闭上眼睛,则可能会发生这种情况。
以下代码演示如何访问眼睛视线。
using namespace winrt::Windows::UI::Input::Spatial;
using namespace winrt::Windows::Foundation::Numerics;
SpatialPointerPose pointerPose = SpatialPointerPose::TryGetAtTimestamp(coordinateSystem, prediction.Timestamp());
if (pointerPose)
{
if (pointerPose.Eyes() && pointerPose.Eyes().IsCalibrationValid())
{
if (pointerPose.Eyes().Gaze())
{
auto spatialRay = pointerPose.Eyes().Gaze().Value();
float3 eyeGazeOrigin = spatialRay.Origin;
float3 eyeGazeDirection = spatialRay.Direction;
// Do something with the eye-gaze
}
}
}
眼动跟踪不可用时的回退
如眼动跟踪设计文档中所述,设计人员和开发人员应了解眼动跟踪数据可能不可用的情况。
数据不可用的原因有多种:
- 用户未校准
- 用户已拒绝应用访问其眼动跟踪数据
- 暂时性干扰,例如 HoloLens 面罩上有污迹或用户眼睛被头发遮挡。
虽然本文档中提到了一些 API,但在下文中,我们提供了一个如何检测眼动跟踪的总结作为快速参考:
检查系统是否完全支持眼动跟踪。 调用以下方法:Windows.Perception.People.EyesPose.IsSupported()
检查用户是否已校准。 调用以下属性:Windows.Perception.People.EyesPose.IsCalibrationValid
检查用户是否已授予应用使用其眼动跟踪数据的权限:检索当前的“GazeInputAccessStatus”。 请求访问视线输入中提供了如何执行此操作的示例。
你可能还想通过在接收的眼动跟踪数据更新之间添加一个超时时间来检查眼动跟踪数据是否未过时,否则回退到如下所述的头部跟踪视线。 有关详细信息,请参阅回退设计注意事项。
将视线与其他输入相关联
有时,你可能发现需要一个与过去事件对应的 SpatialPointerPose。 例如,如果用户执行隔空敲击,应用可能想要了解他们正在查看的内容。 为此,仅将 SpatialPointerPose::TryGetAtTimestamp 与预测的帧时间结合使用是不准确的,因为系统输入处理和显示时间之间存在延迟。 此外,如果使用眼睛视线来确定目标,我们的眼睛往往会在完成提交操作之前继续转动。 对于简单的隔空敲击来说,这不是个问题,但在将长语音命令与快速眼球运动结合使用时,这就变得更加关键。 处理此场景的一种方法是使用与输入事件对应的历史时间戳对 SpatialPointerPose::TryGetAtTimestamp 进行一次额外调用。
但是,对于通过 SpatialInteractionManager 路由的输入,有一种更简单的方法。 SpatialInteractionSourceState 有自身的 TryGetAtTimestamp 函数。 调用该函数会提供完全相关的 SpatialPointerPose,而无需进行猜测。 有关使用 SpatialInteractionSourceStates 的详细信息,请参阅 DirectX 中的手部和运动控制器文档。
校准
要准确运行眼动跟踪,每个用户需要完成眼动跟踪用户校准。 这样,设备就可以调整系统,为用户提供更舒适、更高质量的观看体验,同时确保眼动跟踪的准确性。 开发人员无需自行进行任何操作就可以管理用户校准。 系统将确保在下列情况下提示用户校准设备:
- 用户首次使用设备
- 用户之前选择退出校准过程
- 用户上次使用设备时校准过程不成功
开发人员应确保为可能无法获得其眼动跟踪数据的用户提供足够的支持。 若要详细了解回退解决方案的注意事项,请参阅 HoloLens 2 中的眼动跟踪。