HoloLens(第一代)输入 211:手势
重要
混合现实学院教程在制作时考虑到了 HoloLens(第一代)、Unity 2017 和混合现实沉浸式头戴显示设备。 因此,对于仍在寻求这些设备的开发指导的开发人员而言,我们觉得很有必要保留这些教程。 我们不会在这些教程中更新 HoloLens 2 所用的最新工具集或交互相关的内容,因此这些教程可能与较新版本的 Unity 不相符。 我们将维护这些教程,使之持续适用于支持的设备。 已经为 HoloLens 2 发布了一系列新教程。
手势将用户意图转化成动作。 用户可以使用手势来与全息影像交互。 本课程介绍如何跟踪用户的手部,对用户输入做出响应,并根据手部状态和位置向用户提供反馈。
在 MR 基础知识 101 中,我们使用了简单的隔空敲击手势来与全息影像交互。 现在,我们将在隔空敲击手势的基础上更进一步,探索以下新的概念:
- 检测何时正在跟踪用户的手部并向用户提供反馈。
- 使用导航手势来旋转全息影像。
- 当用户的手部即将离开视场时提供反馈。
- 使用操控事件来允许用户用手移动全息影像。
在本课程中,我们将再次用到在 MR 输入 210 中生成的 Unity 项目“模型资源管理器”。 我们的宇航员朋友将回来帮忙探索这些新的手势概念。
重要
下面每一章中嵌入的视频是使用旧版 Unity 和混合现实工具包录制的。 虽然分步说明比较准确且是最新的,但你在相应视频中可能会看到已过时的脚本和视觉效果。 保留这些视频是为了供后来的读者参考,并且涉及的概念现在仍然适用。
设备支持
课程 | HoloLens | 沉浸式头戴显示设备 |
---|---|---|
MR 输入 211:手势 | ✔ | ✔ |
开始之前
先决条件
- 一台安装了正确工具的 Windows 10 电脑。
- 基础的 C# 编程能力。
- 应已完成 MR 基础知识 101。
- 应已完成 MR 输入 210。
- 一台配置用于开发的 HoloLens 设备。
项目文件
- 下载项目所需的文件。 需要 Unity 2017.2 或更高版本。
- 将文件解压缩到桌面或其他易于访问的位置。
注意
如果要在下载源代码之前查看它,可以在 GitHub 上查看。
勘误表和备注
- 需要在 Visual Studio 中的“工具”->“选项”->“调试”下禁用(取消选中)“启用仅我的代码”,以便在代码中命中断点。
第 0 章 - Unity 设置
说明
- 启动 “Unity”。
- 选择打开。
- 导航到前面解压缩的“Gesture”文件夹。
- 找到并选择“Starting”/“Model Explorer”文件夹。
- 单击“选择文件夹”按钮。
- 在“项目”面板中,展开“Scenes”文件夹。
- 双击“ModelExplorer”场景以在 Unity 中加载它。
生成
- 在 Unity 中,选择“文件”>“生成设置”。
- 如果“Scenes/ModelExplorer”未列在“生成中的场景”中,请单击“添加开放场景”以添加该场景。
- 如果专门针对 HoloLens 进行开发,请将“目标设备”设置为“HoloLens”。 否则,请将此选项保留为“任何设备”。
- 确保将“生成类型”设置为“D3D”,将“SDK”设置为“最新安装版本”(应该是 SDK 16299 或更高版本)。
- 单击“生成”。
- 创建名为“App”的新文件夹。
- 单击“App”文件夹。
- 按“选择文件夹”,然后 Unity 将开始生成适用于 Visual Studio 的项目。
完成 Unity 设置后,将出现一个文件资源管理器窗口。
- 打开“App”文件夹。
- 打开“ModelExplorer Visual Studio 解决方案”。
如果是部署到 HoloLens:
- 使用 Visual Studio 中的顶部工具栏,将目标从“调试”更改为“发布”,并从“ARM”更改为“x86”。
- 单击“本地计算机”按钮旁边的下拉箭头,然后选择“远程计算机”。
- 输入 HoloLens 设备 IP 地址,将“身份验证模式”设置为“通用(未加密协议)”。 单击“选择”。 如果你不知道自己的设备 IP 地址,可以在“设置”>“网络和 Internet”>“高级选项”中找到。
- 在顶部菜单栏中,单击“调试”->“开始执行(不调试)”或按 Ctrl + F5。 如果这是你第一次部署到设备,需要将设备与 Visual Studio 配对。
- 部署应用后,使用“选择手势”关闭“工具箱”。
如果部署到沉浸式头戴显示设备:
- 使用 Visual Studio 中的顶部工具栏,将目标从“调试”更改为“发布”,并从“ARM”更改为“x64”。
- 确保部署目标设置为“本地计算机”。
- 在顶部菜单栏中,单击“调试”->“开始执行(不调试)”或按 Ctrl + F5。
- 部署应用后,通过拉动运动控制器上的触发器来关闭“工具箱”。
注意
你可能会注意到 Visual Studio“错误”面板中以红色字体显示了一些错误。 可以放心地忽略这些错误。 切换到“输出”面板以查看实际生成进度。 需要修复“输出”面板中的错误(它们往往是脚本中的错误导致的)。
第 1 章 -“检测到手部”反馈
目标
- 订阅手部追踪事件。
- 使用光标反馈向用户告知正在跟踪手部。
注意
在 HoloLens 2 上,只要手部可见(而不仅仅是手指朝上时),就会触发“检测到手部”。
说明
- 在“层次结构”面板中,展开“InputManager”对象。
- 找到并选择“GesturesInput”对象。
InteractionInputSource.cs 脚本执行以下步骤:
- 订阅 InteractionSourceDetected 和 InteractionSourceLost 事件。
- 设置 HandDetected 状态。
- 取消订阅 InteractionSourceDetected 和 InteractionSourceLost 事件。
接下来,我们将 MR 输入 210 中的光标升级为根据用户操作显示反馈的光标。
- 在“层次结构”面板中,选择“Cursor”对象并将其删除。
- 在“项目”面板中,搜索“CursorWithFeedback”并将其拖放到“层次结构”面板中。
- 在“层次结构”面板中单击“InputManager”,然后将“CursorWithFeedback”对象从“层次结构”拖放到 InputManager 的“SimpleSinglePointerSelector”的“Cursor”字段中(在“检查器”的底部)。
- 在“层次结构”中单击“CursorWithFeedback”。
- 在“检查器”面板中,展开“对象光标”脚本中的“光标状态数据”。
“光标状态数据”的工作方式如下:
- 任何“观察”状态表示没有检测到手部,用户只是在环顾四周。
- 任何“交互”状态表示已检测到手部或控制器。
- 任何“悬停”状态表示用户正在注视全息影像。
生成和部署
- 在 Unity 中,使用“文件”>“生成设置”来重新生成应用程序。
- 打开“App”文件夹。
- 如果尚未打开 ModelExplorer Visual Studio 解决方案,请将其打开。
- (如果在设置期间已在 Visual Studio 中生成/部署了该项目,则可以打开该 VS 实例并在出现提示时单击“全部重新加载”)。
- 在 Visual Studio 中,单击“调试”->“开始执行(不调试)”或按 Ctrl + F5。
- 在应用程序部署到 HoloLens 后,使用隔空敲击手势关闭工具箱。
- 将手移入视场,将食指指向天空以开始手部跟踪。
- 向左、向右、向上和向下移动手部。
- 观察当检测到手部,然后手部从视场中消失时光标如何变化。
- 如果使用沉浸式头戴显示设备,则必须连接再断开连接控制器。 这种反馈在沉浸式设备上不那么有趣,因为连接的控制器始终“可用”。
第 2 章 - 导航
目标
- 使用导航手势事件来旋转宇航员。
说明
为了在应用中使用导航手势,我们将编辑“GestureAction.cs”以便在发生导航手势时旋转对象。 此外,我们将为光标添加在导航可用时显示的反馈。
- 在“层次结构”面板中,展开“CursorWithFeedback”。
- 在“Holograms”文件夹中,找到“ScrollFeedback”资产。
- 将“ScrollFeedback”预制件拖放到“层次结构”中的“CursorWithFeedback”GameObject。
- 单击“CursorWithFeedback”。
- 在“检查器”面板中,单击“添加组件”按钮。
- 在菜单中的搜索框内键入“CursorFeedback”。 选择搜索结果。
- 将“层次结构”中的“ScrollFeedback”对象拖放到“检查器”的“光标反馈”组件中的“滚动检测到的游戏对象”属性。
- 在“层次结构”面板中,选择“AstroMan”对象。
- 在“检查器”面板中,单击“添加组件”按钮。
- 在菜单中的搜索框内键入“手势操作”。 选择搜索结果。
接下来,在 Visual Studio 中打开“GestureAction.cs”。 在编程练习 2.c 中,编辑脚本以执行以下操作:
- 每当执行导航手势时旋转 AstroMan 对象。
- 计算 rotationFactor 以控制应用于对象的旋转量。
- 当用户向左或向右移动手部时围绕 y 轴旋转对象。
完成脚本中的编程练习 2.c,或将代码替换为下面已完成的解决方案:
using HoloToolkit.Unity.InputModule;
using UnityEngine;
/// <summary>
/// GestureAction performs custom actions based on
/// which gesture is being performed.
/// </summary>
public class GestureAction : MonoBehaviour, INavigationHandler, IManipulationHandler, ISpeechHandler
{
[Tooltip("Rotation max speed controls amount of rotation.")]
[SerializeField]
private float RotationSensitivity = 10.0f;
private bool isNavigationEnabled = true;
public bool IsNavigationEnabled
{
get { return isNavigationEnabled; }
set { isNavigationEnabled = value; }
}
private Vector3 manipulationOriginalPosition = Vector3.zero;
void INavigationHandler.OnNavigationStarted(NavigationEventData eventData)
{
InputManager.Instance.PushModalInputHandler(gameObject);
}
void INavigationHandler.OnNavigationUpdated(NavigationEventData eventData)
{
if (isNavigationEnabled)
{
/* TODO: DEVELOPER CODING EXERCISE 2.c */
// 2.c: Calculate a float rotationFactor based on eventData's NormalizedOffset.x multiplied by RotationSensitivity.
// This will help control the amount of rotation.
float rotationFactor = eventData.NormalizedOffset.x * RotationSensitivity;
// 2.c: transform.Rotate around the Y axis using rotationFactor.
transform.Rotate(new Vector3(0, -1 * rotationFactor, 0));
}
}
void INavigationHandler.OnNavigationCompleted(NavigationEventData eventData)
{
InputManager.Instance.PopModalInputHandler();
}
void INavigationHandler.OnNavigationCanceled(NavigationEventData eventData)
{
InputManager.Instance.PopModalInputHandler();
}
void IManipulationHandler.OnManipulationStarted(ManipulationEventData eventData)
{
if (!isNavigationEnabled)
{
InputManager.Instance.PushModalInputHandler(gameObject);
manipulationOriginalPosition = transform.position;
}
}
void IManipulationHandler.OnManipulationUpdated(ManipulationEventData eventData)
{
if (!isNavigationEnabled)
{
/* TODO: DEVELOPER CODING EXERCISE 4.a */
// 4.a: Make this transform's position be the manipulationOriginalPosition + eventData.CumulativeDelta
}
}
void IManipulationHandler.OnManipulationCompleted(ManipulationEventData eventData)
{
InputManager.Instance.PopModalInputHandler();
}
void IManipulationHandler.OnManipulationCanceled(ManipulationEventData eventData)
{
InputManager.Instance.PopModalInputHandler();
}
void ISpeechHandler.OnSpeechKeywordRecognized(SpeechEventData eventData)
{
if (eventData.RecognizedText.Equals("Move Astronaut"))
{
isNavigationEnabled = false;
}
else if (eventData.RecognizedText.Equals("Rotate Astronaut"))
{
isNavigationEnabled = true;
}
else
{
return;
}
eventData.Use();
}
}
你会注意到,其他导航事件中已填充了一些信息。 将 GameObject 推送到工具包的 InputSystem 的模态堆栈上,因此一旦开始旋转,用户就不必聚焦在宇航员上。 相应地,一旦手势完成,我们就会将 GameObject 从堆栈中弹出。
生成和部署
- 在 Unity 中重新生成应用程序,然后在 Visual Studio 中生成并部署,以便在 HoloLens 中运行该应用程序。
- 凝视宇航员,应会有两个箭头出现在光标的每一侧。 此新视觉效果表示可以旋转宇航员。
- 将手放在准备好的位置(食指指向天空),使 HoloLens 开始跟踪你的手部。
- 若要旋转宇航员,请勾回食指使其呈捏合状,然后向左或向右移动手部以触发 NavigationX 手势。
第 3 章 - 手部引导
目标
- 使用手部引导评分来帮助预测何时失去手部跟踪。
- 提供光标反馈,以便在用户的手部靠近相机视场边缘时显示。
说明
- 在“层次结构”面板中,选择“CursorWithFeedback”对象。
- 在“检查器”面板中,单击“添加组件”按钮。
- 在菜单中的搜索框内键入“手部引导”。 选择搜索结果。
- 在“项目”面板上的“Holograms”文件夹中,找到“HandGuidanceFeedback”资产。
- 将“HandGuidanceFeedback”资产拖放到“检查器”面板中的“手部引导指示器”属性上。
生成和部署
- 在 Unity 中重新生成应用程序,然后在 Visual Studio 中生成并部署,以便在 HoloLens 上体验应用。
- 将手放在视场中并举起食指进行跟踪。
- 使用导航手势开始旋转宇航员(将食指和拇指捏合在一起)。
- 在远处向左、向右、向上和向下移动手部。
- 当手靠近手势框的边缘时,光标旁边应会出现一个箭头,警告将会失去手部跟踪。 箭头指示手的移动方向,以防止失去跟踪。
第 4 章 - 操控
目标
- 使用操控事件用手移动宇航员。
- 提供光标反馈,让用户知道何时可以使用操控手势。
说明
通过 GestureManager.cs 和 AstronautManager.cs 可以执行以下操作:
- 使用语音关键字“移动宇航员”启用操控手势,使用语音关键字“旋转宇航员”禁用操控手势。
- 切换为响应“操控手势识别器”。
现在就开始吧。
- 在“层次结构”面板中,新建一个空的 GameObject。 将其命名为“AstronautManager”。
- 在“检查器”面板中,单击“添加组件”按钮。
- 在菜单中的搜索框内键入“宇航员管理器”。 选择搜索结果。
- 在“检查器”面板中,单击“添加组件”按钮。
- 在菜单中的搜索框内键入“语音输入源”。 选择搜索结果。
现在,我们将添加控制宇航员交互状态所需的语音命令。
- 在“检查器”中展开“关键字”部分。
- 单击右侧的 + 以添加新关键字。
- 键入“移动宇航员”作为关键字。 如果需要,请随意添加快捷键。
- 单击右侧的 + 以添加新关键字。
- 键入“旋转宇航员”作为关键字。 如果需要,请随意添加快捷键。
- 可以在“GestureAction.cs”中的“ISpeechHandler.OnSpeechKeywordRecognized”处理程序中找到相应的处理程序代码。
接下来,在光标上设置操控反馈。
- 在“项目”面板上的“Holograms”文件夹中,找到“PathingFeedback”资产。
- 将“PathingFeedback”预制件拖放到“层次结构”中的“CursorWithFeedback”对象。
- 在“层次结构”面板中,单击“CursorWithFeedback”。
- 将“层次结构”中的“PathingFeedback”对象拖放到“检查器”的“光标反馈”组件中的“为检测到的游戏对象设置路径”属性。
现在我们需要将代码添加到 GestureAction.cs 以启用以下功能:
- 将代码添加到 IManipulationHandler.OnManipulationUpdated 函数,以便在检测到操控手势时移动宇航员。
- 计算运动向量,以根据手部位置确定宇航员应移到的位置。
- 将宇航员移到新位置。
完成 GestureAction.cs 中的编程练习 4.a,或使用下面已完成的解决方案:
using HoloToolkit.Unity.InputModule;
using UnityEngine;
/// <summary>
/// GestureAction performs custom actions based on
/// which gesture is being performed.
/// </summary>
public class GestureAction : MonoBehaviour, INavigationHandler, IManipulationHandler, ISpeechHandler
{
[Tooltip("Rotation max speed controls amount of rotation.")]
[SerializeField]
private float RotationSensitivity = 10.0f;
private bool isNavigationEnabled = true;
public bool IsNavigationEnabled
{
get { return isNavigationEnabled; }
set { isNavigationEnabled = value; }
}
private Vector3 manipulationOriginalPosition = Vector3.zero;
void INavigationHandler.OnNavigationStarted(NavigationEventData eventData)
{
InputManager.Instance.PushModalInputHandler(gameObject);
}
void INavigationHandler.OnNavigationUpdated(NavigationEventData eventData)
{
if (isNavigationEnabled)
{
/* TODO: DEVELOPER CODING EXERCISE 2.c */
// 2.c: Calculate a float rotationFactor based on eventData's NormalizedOffset.x multiplied by RotationSensitivity.
// This will help control the amount of rotation.
float rotationFactor = eventData.NormalizedOffset.x * RotationSensitivity;
// 2.c: transform.Rotate around the Y axis using rotationFactor.
transform.Rotate(new Vector3(0, -1 * rotationFactor, 0));
}
}
void INavigationHandler.OnNavigationCompleted(NavigationEventData eventData)
{
InputManager.Instance.PopModalInputHandler();
}
void INavigationHandler.OnNavigationCanceled(NavigationEventData eventData)
{
InputManager.Instance.PopModalInputHandler();
}
void IManipulationHandler.OnManipulationStarted(ManipulationEventData eventData)
{
if (!isNavigationEnabled)
{
InputManager.Instance.PushModalInputHandler(gameObject);
manipulationOriginalPosition = transform.position;
}
}
void IManipulationHandler.OnManipulationUpdated(ManipulationEventData eventData)
{
if (!isNavigationEnabled)
{
/* TODO: DEVELOPER CODING EXERCISE 4.a */
// 4.a: Make this transform's position be the manipulationOriginalPosition + eventData.CumulativeDelta
transform.position = manipulationOriginalPosition + eventData.CumulativeDelta;
}
}
void IManipulationHandler.OnManipulationCompleted(ManipulationEventData eventData)
{
InputManager.Instance.PopModalInputHandler();
}
void IManipulationHandler.OnManipulationCanceled(ManipulationEventData eventData)
{
InputManager.Instance.PopModalInputHandler();
}
void ISpeechHandler.OnSpeechKeywordRecognized(SpeechEventData eventData)
{
if (eventData.RecognizedText.Equals("Move Astronaut"))
{
isNavigationEnabled = false;
}
else if (eventData.RecognizedText.Equals("Rotate Astronaut"))
{
isNavigationEnabled = true;
}
else
{
return;
}
eventData.Use();
}
}
生成和部署
- 在 Unity 中重新生成,然后在 Visual Studio 中生成并部署,以便在 HoloLens 中运行应用。
- 将手移到 HoloLens 前面并举起食指,以便对其进行跟踪。
- 将光标聚焦在宇航员上。
- 说出“移动宇航员”,以使用操控手势移动宇航员。
- 光标周围应会出现四个箭头,指示程序现在将响应操控事件。
- 将食指勾回到拇指处,并使两根手指保持捏合状态。
- 移动手部时,宇航员也会随之移动(这就是操控)。
- 举起食指会停止操控宇航员。
- 注意:如果在移动手部之前不说出“移动宇航员”,则会改用导航手势。
- 说出“旋转宇航员”会返回到可旋转状态。
第 5 章 - 模型扩展
目标
- 将宇航员模型扩展为用户可以与之交互的多个较小部分。
- 使用导航和操控手势单独移动每个部分。
说明
在本部分,我们将完成以下任务:
- 添加新关键字“扩展模型”以扩展宇航员模型。
- 添加新关键字“重置模型”以将模型恢复为其原始形式。
我们将通过向前一章中的“语音输入源”添加另外两个关键字来完成这些任务。 我们还将演示另一种处理识别事件的方式。
- 返回到“检查器”中的“AstronautManager”,然后展开“检查器”中的“关键字”部分。
- 单击右侧的 + 以添加新关键字。
- 键入“扩展模型”作为关键字。 如果需要,请随意添加快捷键。
- 单击右侧的 + 以添加新关键字。
- 键入“重置模型”作为关键字。 如果需要,请随意添加快捷键。
- 在“检查器”面板中,单击“添加组件”按钮。
- 在菜单中的搜索框内键入“语音输入处理程序”。 选择搜索结果。
- 选中“是全局收听器”,因为我们希望无论聚焦于哪个 GameObject,这些命令都可正常运行。
- 单击 + 按钮并从关键字下拉列表中选择“扩展模型”。
- 单击“响应”下的 +,然后将“层次结构”中的“AstronautManager”拖放到“无(对象)”字段中。
- 现在单击“无函数”下拉列表,然后依次选择“AstronautManager”、“ExpandModelCommand”。
- 单击“语音输入处理程序”的 + 按钮,并从关键字下拉列表中选择“重置模型”。
- 单击“响应”下的 +,然后将“层次结构”中的“AstronautManager”拖放到“无(对象)”字段中。
- 现在单击“无函数”下拉列表,然后依次选择“AstronautManager”、“ResetModelCommand”。
生成和部署
- 试试看! 生成应用并将其部署到 HoloLens。
- 说出“展开模型”,以查看扩展的宇航员模型。
- 使用导航手势来旋转宇航员套装的各个部分。
- 说出“移动宇航员”,然后使用操控手势来移动宇航员套装的各个部分。
- 说出“旋转宇航员”以再次旋转各个部分。
- 说出“重置模型”,将宇航员恢复为原始状态。
结束
祝贺你! 现已完成“MR 输入 211:手势”。
- 你已了解如何检测和响应手部跟踪、导航和操控事件。
- 你已了解导航和操控手势之间的区别。
- 你已了解如何更改光标,以在检测到手部、即将失去手部跟踪以及对象支持不同的交互(导航与操控)时提供视觉反馈。