HoloLens 第一代 () Input 211:手势

重要

混合现实学院教程的设计目的是 HoloLens (一代) 、Unity 2017 和混合现实沉浸式耳机。 因此,对于仍在寻求这些设备的开发指导的开发人员而言,我们觉得很有必要保留这些教程。 这些教程 会使用最新工具集或用于 HoloLens 2 的交互进行更新,可能与新版本的 Unity 不兼容。 我们将维护这些教程,使之持续适用于支持的设备。 已经为 HoloLens 2 发布了一系列新教程

手势 会使用户意向变为操作。 用户可以使用手势来与全息影像交互。 在本课程中,我们将了解如何跟踪用户的动手,响应用户输入,并根据手头状态和位置向用户提供反馈。

先生/女士 101中,我们使用了一种简单的点击手势来与全息影像交互。 现在,我们将转到轻攻攻攻,并探索新的概念:

  • 检测何时跟踪用户的手并向用户提供反馈。
  • 使用导航手势旋转全息影像。
  • 请在用户即将退出查看时提供反馈。
  • 使用操作事件可允许用户使用其手移动全息影像。

在本课程中,我们将重新访问 Unity 项目 模型资源管理器,该资源管理器 是在 MR Input 210中生成的。 我们 astronaut 的朋友会回来,帮助我们探索这些新的手势概念。

重要

以下各章中嵌入的视频是使用旧版本的 Unity 记录的,混合现实 Toolkit。 虽然分步说明准确且最新,但你可能会看到处于过期状态的相应视频中的脚本和视觉对象。 视频仍包含在 posterity 中,因为涵盖的概念仍适用。

设备支持

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

准备工作

必备条件

项目文件

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

备注

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

勘误表和说明

  • 若要在代码中命中断点,请在 "工具->选项->调试" 下的 Visual Studio 中禁用 "启用仅我的代码" (取消选中) 。

第0章-Unity 设置

说明

  1. 启动 Unity。
  2. 选择“打开”。
  3. 导航到之前未存档的 手势 文件夹。
  4. 查找并选择 "启动 / 模型资源管理器" 文件夹。
  5. 单击 " 选择文件夹 " 按钮。
  6. Project 面板中,展开 "场景" 文件夹。
  7. 双击 " ModelExplorer 场景" 将其加载到 Unity。

生成

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

当 Unity 完成后,将显示文件资源管理器窗口。

  1. 打开 应用程序 文件夹。
  2. 打开 ModelExplorer Visual Studio 解决方案

如果部署到 HoloLens:

  1. 使用 Visual Studio 中的顶部工具栏将目标从 "调试" 更改为 "发布",将 "从 ARM" 更改为 " x86"。
  2. 单击 "本地计算机" 按钮旁的下拉箭头,然后选择 " 远程计算机"。
  3. 输入 HoloLens 设备 IP 地址,并将身份验证模式设置为 通用 (未加密的协议)。 单击“选择” 。 如果你不知道设备 IP 地址,请查看 设置 > 网络 & Internet > 高级选项
  4. 在顶部菜单栏中,单击 " 调试-> 启动而不调试 " 或按 Ctrl + F5。 如果这是首次部署到设备,则需要将其与 Visual Studio 配对
  5. 当应用程序已部署后,使用 选择手势 关闭 Fitbox

如果要部署到沉浸式耳机:

  1. 使用 Visual Studio 中的顶部工具栏,将目标从 "调试" 更改为 "发布",并将 "从 ARM 更改为 x64"。
  2. 确保将部署目标设置为 " 本地计算机"。
  3. 在顶部菜单栏中,单击 " 调试-> 启动而不调试 " 或按 Ctrl + F5
  4. 当应用程序已部署后,通过将触发器拖到运动控制器上来关闭 Fitbox

备注

你可能会注意到 Visual Studio 错误面板中出现一些红色错误。 可以放心地忽略它们。 切换到 "输出" 面板以查看实际的生成进度。 "输出" 面板中的错误将要求您进行修补 (最常见的原因是脚本) 出错。

第1章-手动检测的反馈

目标

  • 订阅手动跟踪事件。
  • 使用光标反馈向用户显示要跟踪的用户。

备注

在 HoloLens 2 上,无论指针是否) ,只要指针可见 (,就会触发指针。

说明

  • 在 " 层次结构 " 面板中,展开 " InputManager " 对象。
  • 查找并选择 " GesturesInput " 对象。

InteractionInputSource 脚本将执行以下步骤:

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

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

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

游标 状态数据 的工作方式如下所示:

  • " 任何 观察"状态表示未检测到手部,并且用户只是四处查看。
  • " 任何 交互"状态表示检测到手部或控制器。
  • 任何 停状态都表示用户正在查看全息影像。

生成和部署

  • 在 Unity 中,使用 >生成设置 重新生成应用程序。
  • 打开 "应用" 文件夹。
  • 如果尚未打开,请打开 ModelExplorer Visual Studio解决方案。
    • (如果在设置期间已在 Visual Studio 中生成/部署此项目,则你可以打开该 VS 实例,然后在系统提示时单击"全部重载) " 。
  • 在Visual Studio中,单击"调试 "->"启动而不调试", 或按 Ctrl + F5
  • 将应用程序部署到 HoloLens后,使用敲击手势关闭 fitbox。
  • 将手移入视图,将手指指向天上,开始手部跟踪。
  • 向左、向右、向上和向下移动手部。
  • 观察在检测到手部后从视图中丢失时光标的变化情况。
  • 如果位于沉浸式头戴显示设备上,必须连接并断开控制器。 这种反馈在沉浸式设备上变得不太有趣,因为连接的控制器将始终"可用"。

第 2 章 - 导航

目标

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

Instructions

若要在应用中使用导航手势,我们将编辑 GestureAction.cs, 以在导航手势发生时旋转对象。 此外,我们将向光标添加反馈,以在"导航"可用时显示。

  1. 在"层次结构"面板中,展开 "CursorWithFeedback"。
  2. "全息影像" 文件夹中,找到 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 推送到Toolkit的 InputSystem 模式堆栈上,因此,一旦旋转开始,用户就不必专注于宇航员。 相应地,完成手势后,我们会从堆栈中弹出 GameObject。

生成和部署

  1. 在 Unity 中重新生成应用程序,然后从 Visual Studio生成和部署,以在 HoloLens。
  2. 凝视宇航员,光标两侧应会出现两个箭头。 此新视觉对象指示宇航员可以旋转。
  3. 将手放在手部 (指指向天空的位置) 以便HoloLens开始跟踪手。
  4. 若要旋转宇航员,将手指下至收缩位置,然后向左或向右移动手以触发 NavigationX 手势。

第 3 章 - 手部指南

目标

  • 使用 手部指导 分数来帮助预测手部跟踪丢失的时间。
  • 提供有关 光标的反馈 ,以在用户手靠近照相机的视图边缘时显示。

说明

  1. 在" 层次结构"面板 中,选择 CursorWithFeedback 对象。
  2. 在" 检查器" 面板中,单击" 添加组件" 按钮。
  3. 在菜单中,键入搜索框"手动 指导"。 选择搜索结果。
  4. 在Project 面板****全息影像, 找到 HandGuidanceFeedback 资产。
  5. HandGuidanceFeedback 资产拖放到"检查器"面板中的" 部指导指示器 " 属性上。

生成和部署

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

第 4 章 - 操作

目标

  • 使用操作事件,用手移动宇航员。
  • 提供有关游标的反馈,让用户知道何时可以使用操作。

Instructions

GestureManager.cs 和 AstronautManager.cs 将使我们能够执行以下操作:

  1. 使用语音关键字"移动宇航员"启用 操作 手势,并使用"旋转宇航员"来禁用它们。
  2. 切换到响应操作 手势识别器

现在就开始吧。

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

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

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

如何为第 4 章设置语音输入源

接下来,我们将设置游标上的操作反馈。

  1. 在Project 面板****全息影像, 找到 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(&quot;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 章 - 模型扩展

目标

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

Instructions

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

  1. 添加新关键字"展开模型"以展开宇航员模型。
  2. 添加新的关键字"重置模型",将模型返回到其原始窗体。

为此,我们将向上一章中的语音输入源再添加两个关键字。 我们还将演示处理识别事件的另一种方法。

  1. 在检查 **器中单击"AstronautManager",**然后 展开检查器 中的 " 关键字 "部分
  2. 单击 + 右侧 以添加新关键字。
  3. 键入"关键字"作为 "展开模型"。 如果需要,可随意添加键快捷方式。
  4. 单击 + 右侧 以添加新关键字。
  5. 键入 关键字作为 "重置模型"。 如果需要,可随意添加键快捷方式。
  6. 在" 检查器" 面板中,单击" 添加组件" 按钮。
  7. 在菜单中,键入搜索框"语音 输入处理程序"。 选择搜索结果。
  8. 检查 "是全局侦听器",因为我们希望这些命令能够正常工作,而不管我们重点关注的 GameObject 是什么。
  9. 单击按钮 + ,然后 从"关键字"下拉列表 中选择"展开模型"。
  10. 单击" + 响应"下的 ,然后将 "AstronautManager" 从"层次结构"拖动到"无 (对象) 字段。
  11. 现在,单击"无函数" 下拉列表,选择 "AstronautManager", 然后选择 "ExpandModelCommand"。
  12. 单击"语音输入处理程序" + 按钮,然后从"关键字 "下拉列表 中选择"重置模型"。
  13. 单击" + 响应"下的 ,然后将 "AstronautManager" 从"层次结构"拖动到"无 (对象) 字段。
  14. 现在,单击"无函数" 下拉列表,选择 "AstronautManager", 然后选择 "ResetModelCommand"。

如何为第 5 章设置语音输入源和处理程序

生成和部署

  • 试试看! 生成应用并部署到HoloLens。
  • 展开模型 以查看展开的宇航员模型。
  • 使用 导航 来旋转宇航员服装的单个部件。
  • 假设 "移动宇航员 ",然后使用 "操作 "移动宇航员服装的单个部件。
  • 旋转宇航员 以再次旋转部件。
  • 假设 重置模型 将宇航员恢复其原始形式。

结束

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

  • 你知道如何检测和响应手部跟踪、导航和操作事件。
  • 你了解导航和操作手势的区别。
  • 你知道如何更改光标,以提供视觉反馈,说明何时检测到手、手即将丢失,以及当对象支持导航与操作 (交互时) 。