HoloLens(第一代)输入 211:手势

重要

混合现实学院教程在制作时考虑到了 HoloLens(第一代)、Unity 2017 和混合现实沉浸式头戴显示设备。 因此,对于仍在寻求这些设备的开发指导的开发人员而言,我们觉得很有必要保留这些教程。 我们不会在这些教程中更新 HoloLens 2 所用的最新工具集或交互相关的内容,因此这些教程可能与较新版本的 Unity 不相符。 我们将维护这些教程,使之持续适用于支持的设备。 已经为 HoloLens 2 发布了一系列新教程

手势将用户意图转化成动作。 用户可以使用手势来与全息影像交互。 本课程介绍如何跟踪用户的手部,对用户输入做出响应,并根据手部状态和位置向用户提供反馈。

MR 基础知识 101 中,我们使用了简单的隔空敲击手势来与全息影像交互。 现在,我们将在隔空敲击手势的基础上更进一步,探索以下新的概念:

  • 检测何时正在跟踪用户的手部并向用户提供反馈。
  • 使用导航手势来旋转全息影像。
  • 当用户的手部即将离开视场时提供反馈。
  • 使用操控事件来允许用户用手移动全息影像。

在本课程中,我们将再次用到在 MR 输入 210 中生成的 Unity 项目“模型资源管理器”。 我们的宇航员朋友将回来帮忙探索这些新的手势概念。

重要

下面每一章中嵌入的视频是使用旧版 Unity 和混合现实工具包录制的。 虽然分步说明比较准确且是最新的,但你在相应视频中可能会看到已过时的脚本和视觉效果。 保留这些视频是为了供后来的读者参考,并且涉及的概念现在仍然适用。

设备支持

课程 HoloLens 沉浸式头戴显示设备
MR 输入 211:手势

开始之前

先决条件

项目文件

  • 下载项目所需的文件。 需要 Unity 2017.2 或更高版本。
  • 将文件解压缩到桌面或其他易于访问的位置。

注意

如果要在下载源代码之前查看它,可以在 GitHub 上查看

勘误表和备注

  • 需要在 Visual Studio 中的“工具”->“选项”->“调试”下禁用(取消选中)“启用仅我的代码”,以便在代码中命中断点

第 0 章 - Unity 设置

说明

  1. 启动 “Unity”。
  2. 选择打开
  3. 导航到前面解压缩的“Gesture”文件夹
  4. 找到并选择“Starting”/“Model Explorer”文件夹
  5. 单击“选择文件夹”按钮
  6. 在“项目”面板中,展开“Scenes”文件夹
  7. 双击“ModelExplorer”场景以在 Unity 中加载它

生成

  1. 在 Unity 中,选择“文件”>“生成设置”
  2. 如果“Scenes/ModelExplorer”未列在“生成中的场景”中,请单击“添加开放场景”以添加该场景
  3. 如果专门针对 HoloLens 进行开发,请将“目标设备”设置为“HoloLens”。 否则,请将此选项保留为“任何设备”
  4. 确保将“生成类型”设置为“D3D”,将“SDK”设置为“最新安装版本”(应该是 SDK 16299 或更高版本)
  5. 单击“生成”
  6. 创建名为“App”的新文件夹
  7. 单击“App”文件夹
  8. 按“选择文件夹”,然后 Unity 将开始生成适用于 Visual Studio 的项目

完成 Unity 设置后,将出现一个文件资源管理器窗口。

  1. 打开“App”文件夹。
  2. 打开“ModelExplorer Visual Studio 解决方案”

如果是部署到 HoloLens:

  1. 使用 Visual Studio 中的顶部工具栏,将目标从“调试”更改为“发布”,并从“ARM”更改为“x86”
  2. 单击“本地计算机”按钮旁边的下拉箭头,然后选择“远程计算机”
  3. 输入 HoloLens 设备 IP 地址,将“身份验证模式”设置为“通用(未加密协议)”。 单击“选择”。 如果你不知道自己的设备 IP 地址,可以在“设置”>“网络和 Internet”>“高级选项”中找到。
  4. 在顶部菜单栏中,单击“调试”->“开始执行(不调试)”或按 Ctrl + F5。 如果这是你第一次部署到设备,需要将设备与 Visual Studio 配对
  5. 部署应用后,使用“选择手势”关闭“工具箱”

如果部署到沉浸式头戴显示设备:

  1. 使用 Visual Studio 中的顶部工具栏,将目标从“调试”更改为“发布”,并从“ARM”更改为“x64”
  2. 确保部署目标设置为“本地计算机”
  3. 在顶部菜单栏中,单击“调试”->“开始执行(不调试)”或按 Ctrl + F5
  4. 部署应用后,通过拉动运动控制器上的触发器来关闭“工具箱”

注意

你可能会注意到 Visual Studio“错误”面板中以红色字体显示了一些错误。 可以放心地忽略这些错误。 切换到“输出”面板以查看实际生成进度。 需要修复“输出”面板中的错误(它们往往是脚本中的错误导致的)。

第 1 章 -“检测到手部”反馈

目标

  • 订阅手部追踪事件。
  • 使用光标反馈向用户告知正在跟踪手部。

注意

在 HoloLens 2 上,只要手部可见(而不仅仅是手指朝上时),就会触发“检测到手部”。

说明

  • 在“层次结构”面板中,展开“InputManager”对象
  • 找到并选择“GesturesInput”对象

InteractionInputSource.cs 脚本执行以下步骤

  1. 订阅 InteractionSourceDetected 和 InteractionSourceLost 事件。
  2. 设置 HandDetected 状态。
  3. 取消订阅 InteractionSourceDetected 和 InteractionSourceLost 事件。

接下来,我们将 MR 输入 210 中的光标升级为根据用户操作显示反馈的光标。

  1. 在“层次结构”面板中,选择“Cursor”对象并将其删除
  2. 在“项目”面板中,搜索“CursorWithFeedback”并将其拖放到“层次结构”面板中
  3. 在“层次结构”面板中单击“InputManager”,然后将“CursorWithFeedback”对象从“层次结构”拖放到 InputManager 的“SimpleSinglePointerSelector”的“Cursor”字段中(在“检查器”的底部)
  4. 在“层次结构”中单击“CursorWithFeedback”
  5. 在“检查器”面板中,展开“对象光标”脚本中的“光标状态数据”

“光标状态数据”的工作方式如下

  • 任何“观察”状态表示没有检测到手部,用户只是在环顾四周
  • 任何“交互”状态表示已检测到手部或控制器
  • 任何“悬停”状态表示用户正在注视全息影像

生成和部署

  • 在 Unity 中,使用“文件”>“生成设置”来重新生成应用程序
  • 打开“App”文件夹。
  • 如果尚未打开 ModelExplorer Visual Studio 解决方案,请将其打开
    • (如果在设置期间已在 Visual Studio 中生成/部署了该项目,则可以打开该 VS 实例并在出现提示时单击“全部重新加载”)。
  • 在 Visual Studio 中,单击“调试”->“开始执行(不调试)”或按 Ctrl + F5
  • 在应用程序部署到 HoloLens 后,使用隔空敲击手势关闭工具箱。
  • 将手移入视场,将食指指向天空以开始手部跟踪。
  • 向左、向右、向上和向下移动手部。
  • 观察当检测到手部,然后手部从视场中消失时光标如何变化。
  • 如果使用沉浸式头戴显示设备,则必须连接再断开连接控制器。 这种反馈在沉浸式设备上不那么有趣,因为连接的控制器始终“可用”。

第 2 章 - 导航

目标

  • 使用导航手势事件来旋转宇航员。

说明

为了在应用中使用导航手势,我们将编辑“GestureAction.cs”以便在发生导航手势时旋转对象。 此外,我们将为光标添加在导航可用时显示的反馈。

  1. 在“层次结构”面板中,展开“CursorWithFeedback”
  2. 在“Holograms”文件夹中,找到“ScrollFeedback”资产
  3. 将“ScrollFeedback”预制件拖放到“层次结构”中的“CursorWithFeedback”GameObject
  4. 单击“CursorWithFeedback”
  5. 在“检查器”面板中,单击“添加组件”按钮
  6. 在菜单中的搜索框内键入“CursorFeedback”。 选择搜索结果。
  7. 将“层次结构”中的“ScrollFeedback”对象拖放到“检查器”的“光标反馈”组件中的“滚动检测到的游戏对象”属性
  8. 在“层次结构”面板中,选择“AstroMan”对象
  9. 在“检查器”面板中,单击“添加组件”按钮
  10. 在菜单中的搜索框内键入“手势操作”。 选择搜索结果。

接下来,在 Visual Studio 中打开“GestureAction.cs”。 在编程练习 2.c 中,编辑脚本以执行以下操作:

  1. 每当执行导航手势时旋转 AstroMan 对象
  2. 计算 rotationFactor 以控制应用于对象的旋转量
  3. 当用户向左或向右移动手部时围绕 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 从堆栈中弹出。

生成和部署

  1. 在 Unity 中重新生成应用程序,然后在 Visual Studio 中生成并部署,以便在 HoloLens 中运行该应用程序。
  2. 凝视宇航员,应会有两个箭头出现在光标的每一侧。 此新视觉效果表示可以旋转宇航员。
  3. 将手放在准备好的位置(食指指向天空),使 HoloLens 开始跟踪你的手部。
  4. 若要旋转宇航员,请勾回食指使其呈捏合状,然后向左或向右移动手部以触发 NavigationX 手势。

第 3 章 - 手部引导

目标

  • 使用手部引导评分来帮助预测何时失去手部跟踪
  • 提供光标反馈,以便在用户的手部靠近相机视场边缘时显示

说明

  1. 在“层次结构”面板中,选择“CursorWithFeedback”对象
  2. 在“检查器”面板中,单击“添加组件”按钮
  3. 在菜单中的搜索框内键入“手部引导”。 选择搜索结果。
  4. 在“项目”面板上的“Holograms”文件夹中,找到“HandGuidanceFeedback”资产
  5. 将“HandGuidanceFeedback”资产拖放到“检查器”面板中的“手部引导指示器”属性上

生成和部署

  • 在 Unity 中重新生成应用程序,然后在 Visual Studio 中生成并部署,以便在 HoloLens 上体验应用。
  • 将手放在视场中并举起食指进行跟踪。
  • 使用导航手势开始旋转宇航员(将食指和拇指捏合在一起)。
  • 在远处向左、向右、向上和向下移动手部。
  • 当手靠近手势框的边缘时,光标旁边应会出现一个箭头,警告将会失去手部跟踪。 箭头指示手的移动方向,以防止失去跟踪。

第 4 章 - 操控

目标

  • 使用操控事件用手移动宇航员。
  • 提供光标反馈,让用户知道何时可以使用操控手势。

说明

通过 GestureManager.cs 和 AstronautManager.cs 可以执行以下操作:

  1. 使用语音关键字“移动宇航员”启用操控手势,使用语音关键字“旋转宇航员”禁用操控手势
  2. 切换为响应“操控手势识别器”

现在就开始吧。

  1. 在“层次结构”面板中,新建一个空的 GameObject。 将其命名为“AstronautManager”
  2. 在“检查器”面板中,单击“添加组件”按钮
  3. 在菜单中的搜索框内键入“宇航员管理器”。 选择搜索结果。
  4. 在“检查器”面板中,单击“添加组件”按钮
  5. 在菜单中的搜索框内键入“语音输入源”。 选择搜索结果。

现在,我们将添加控制宇航员交互状态所需的语音命令。

  1. 在“检查器”中展开“关键字”部分
  2. 单击右侧的 + 以添加新关键字。
  3. 键入“移动宇航员”作为关键字。 如果需要,请随意添加快捷键。
  4. 单击右侧的 + 以添加新关键字。
  5. 键入“旋转宇航员”作为关键字。 如果需要,请随意添加快捷键。
  6. 可以在“GestureAction.cs”中的“ISpeechHandler.OnSpeechKeywordRecognized”处理程序中找到相应的处理程序代码

How to set-up the Speech Input Source for chapter 4

接下来,在光标上设置操控反馈。

  1. 在“项目”面板上的“Holograms”文件夹中,找到“PathingFeedback”资产
  2. 将“PathingFeedback”预制件拖放到“层次结构”中的“CursorWithFeedback”对象
  3. 在“层次结构”面板中,单击“CursorWithFeedback”
  4. 将“层次结构”中的“PathingFeedback”对象拖放到“检查器”的“光标反馈”组件中的“为检测到的游戏对象设置路径”属性

现在我们需要将代码添加到 GestureAction.cs 以启用以下功能

  1. 将代码添加到 IManipulationHandler.OnManipulationUpdated 函数,以便在检测到操控手势时移动宇航员
  2. 计算运动向量,以根据手部位置确定宇航员应移到的位置
  3. 将宇航员移到新位置

完成 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 章 - 模型扩展

目标

  • 将宇航员模型扩展为用户可以与之交互的多个较小部分。
  • 使用导航和操控手势单独移动每个部分。

说明

在本部分,我们将完成以下任务:

  1. 添加新关键字“扩展模型”以扩展宇航员模型
  2. 添加新关键字“重置模型”以将模型恢复为其原始形式

我们将通过向前一章中的“语音输入源”添加另外两个关键字来完成这些任务。 我们还将演示另一种处理识别事件的方式。

  1. 返回到“检查器”中的“AstronautManager”,然后展开“检查器”中的“关键字”部分
  2. 单击右侧的 + 以添加新关键字。
  3. 键入“扩展模型”作为关键字。 如果需要,请随意添加快捷键。
  4. 单击右侧的 + 以添加新关键字。
  5. 键入“重置模型”作为关键字。 如果需要,请随意添加快捷键。
  6. 在“检查器”面板中,单击“添加组件”按钮
  7. 在菜单中的搜索框内键入“语音输入处理程序”。 选择搜索结果。
  8. 选中“是全局收听器”,因为我们希望无论聚焦于哪个 GameObject,这些命令都可正常运行
  9. 单击 + 按钮并从关键字下拉列表中选择“扩展模型”
  10. 单击“响应”下的 +,然后将“层次结构”中的“AstronautManager”拖放到“无(对象)”字段中
  11. 现在单击“无函数”下拉列表,然后依次选择“AstronautManager”、“ExpandModelCommand”
  12. 单击“语音输入处理程序”的 + 按钮,并从关键字下拉列表中选择“重置模型”
  13. 单击“响应”下的 +,然后将“层次结构”中的“AstronautManager”拖放到“无(对象)”字段中
  14. 现在单击“无函数”下拉列表,然后依次选择“AstronautManager”、“ResetModelCommand”

How to set-up the Speech Input Source and Handler for chapter 5

生成和部署

  • 试试看! 生成应用并将其部署到 HoloLens。
  • 说出“展开模型”,以查看扩展的宇航员模型
  • 使用导航手势来旋转宇航员套装的各个部分
  • 说出“移动宇航员”,然后使用操控手势来移动宇航员套装的各个部分
  • 说出“旋转宇航员”以再次旋转各个部分
  • 说出“重置模型”,将宇航员恢复为原始状态

结束

祝贺你! 现已完成“MR 输入 211:手势”

  • 你已了解如何检测和响应手部跟踪、导航和操控事件。
  • 你已了解导航和操控手势之间的区别。
  • 你已了解如何更改光标,以在检测到手部、即将失去手部跟踪以及对象支持不同的交互(导航与操控)时提供视觉反馈。