HoloLens(第一代)基础知识 101E:使用仿真器完成项目

重要

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


本教程引导你完成一个在 Unity 中生成的完整项目,该项目演示 HoloLens 上的核心 Windows Mixed Reality 功能,包括凝视手势语音输入空间音效空间映射。 完成本教程大约需要 1 小时。

设备支持

课程 HoloLens 沉浸式头戴显示设备
MR 基础 101E:使用模拟器完成项目

开始之前

先决条件

项目文件

  • 下载项目所需的文件。 需要 Unity 2017.2 或更高版本。
    • 如果仍需要 Unity 5.6 支持,请使用此版本
    • 如果仍需要 Unity 5.5 支持,请使用此版本
    • 如果仍需要 Unity 5.4 支持,请使用此版本
  • 将文件解压缩到桌面或其他易于访问的位置。 将文件夹名称保留为 Origami

注意

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

第 1 章 -“全息”世界

在本章中,我们将设置第一个 Unity 项目,并逐步完成生成和部署过程。

目标

  • 设置 Unity 进行全息开发。
  • 制作全息影像。
  • 查看制作的全息影像。

说明

  • 启动 “Unity”。
  • 选择打开
  • 输入先前解压缩的“Origami”文件夹所在的位置
  • 选择 “Origami”,然后单击“选择文件夹”
  • 保存新场景:“文件” / “将场景另存为”。
  • 将场景命名为 “Origami”,然后按“保存”按钮

设置主相机

  • 在“层次结构面板”中,选择“主摄像头” 。
  • 在“检查器”中,将其转换位置设置为“0,0,0”
  • 找到“清除标志”属性,将下拉列表从“天空盒”更改为“纯色”
  • 单击“背景”字段以打开颜色选取器
  • 将“R、G、B 和 A”设置为“0” 。

设置场景

  • 在“层次结构”面板中,单击“创建”和“创建空白项”
  • 右键单击新的“GameObject”并选择“重命名”。 将 GameObject 重命名为“OrigamiCollection”
  • 从“项目面板”中的 “Holograms” 文件夹
    • 将 “Stage” 拖放到“层次结构”中,使之成为 “OrigamiCollection” 的子项
    • 将“Sphere1”拖放到“层次结构”中,使之成为“OrigamiCollection”的子项
    • 将“Sphere2”拖放到“层次结构”中,使之成为“OrigamiCollection”的子项
  • 在“层次结构”面板中右键单击“定向光”对象,然后选择“删除”
  • 从“Holograms”文件夹中,将“Lights”拖放到“层次结构”面板的根目录中
  • 在“层次结构”中,选择“OrigamiCollection”
  • 在“检查器”中,将转换位置设置为“0, -0.5, 2.0”
  • 在 Unity 中按“播放”按钮预览全息影像
  • 预览窗口中应会显示 Origami 对象。
  • 再次按“播放”以停止预览模式

将项目从 Unity 导出到 Visual Studio

  • 在 Unity 中,选择“文件”>“生成设置”
  • 在“平台”列表中选择 “Windows 应用商店”,然后单击“切换平台”
  • 将“SDK”设置为“通用 10”,并将“生成类型”设置为“D3D”
  • 选中“Unity C# 项目”
  • 单击“添加开放式场景”以添加场景
  • 单击“播放器设置...”
  • 在“检查器”面板中,选择 “Windows 应用商店徽标”。 然后选择“发布设置”
  • 在“功能”部分,选择“麦克风”和 “SpatialPerception” 功能。
  • 回到“生成设置”窗口,单击“生成”
  • 创建名为“App”的新文件夹
  • 单击“App”文件夹
  • 按“选择文件夹”
  • 完成 Unity 设置后,将出现一个文件资源管理器窗口。
  • 打开“App”文件夹。
  • 打开 “Origami Visual Studio 解决方案”
  • 在 Visual Studio 中使用顶部工具栏,将目标从“调试”更改为“发布”,并从“ARM”更改为“X86”
    • 单击“设备”按钮旁边的箭头,然后选择 “HoloLens 仿真器”
    • 单击“调试”>“开始执行(不调试)”或按 Ctrl+F5
    • 经过一段时间后,仿真器将从 Origami 项目启动。 首次启动仿真器时,可能需要 15 分钟的时间。 启动后,请不要将其关闭。

第 2 章 - 凝视

本章介绍与全息影像交互的三种方式中的第一种 - 凝视

目标

  • 使用世界锁定光标来可视化凝视。

说明

  • 返回到 Unity 项目,如果“生成设置”窗口仍然打开,请将其关闭。
  • 在“项目面板”中选择“Holograms”文件夹
  • 将“Cursor”对象拖放到“层次结构面板”中的根级别
  • 双击“Cursor”对象以仔细查看
  • 在“项目”面板中右键单击“Scripts”文件夹
  • 单击“创建”子菜单
  • 选择“C# 脚本”
  • 将脚本命名为“WorldCursor”。 注意:名称区分大小写。 无需添加 .cs 扩展名。
  • 在“层次结构面板”中选择“Cursor”对象
  • 将“WorldCursor”脚本拖放到“检查器”面板中
  • 双击“WorldCursor”脚本以在 Visual Studio 中将其打开
  • 将此代码复制并粘贴到“WorldCursor.cs”中并选择“全部保存”
using UnityEngine;

public class WorldCursor : MonoBehaviour
{
    private MeshRenderer meshRenderer;

    // Use this for initialization
    void Start()
    {
        // Grab the mesh renderer that's on the same object as this script.
        meshRenderer = this.gameObject.GetComponentInChildren<MeshRenderer>();
    }

    // Update is called once per frame
    void Update()
    {
        // Do a raycast into the world based on the user's
        // head position and orientation.
        var headPosition = Camera.main.transform.position;
        var gazeDirection = Camera.main.transform.forward;

        RaycastHit hitInfo;

        if (Physics.Raycast(headPosition, gazeDirection, out hitInfo))
        {
            // If the raycast hit a hologram...
            // Display the cursor mesh.
            meshRenderer.enabled = true;

            // Move thecursor to the point where the raycast hit.
            this.transform.position = hitInfo.point;

            // Rotate the cursor to hug the surface of the hologram.
            this.transform.rotation = Quaternion.FromToRotation(Vector3.up, hitInfo.normal);
        }
        else
        {
            // If the raycast did not hit a hologram, hide the cursor mesh.
            meshRenderer.enabled = false;
        }
    }
}
  • 选择“文件”>“生成设置”以重新生成应用
  • 返回到前面用于部署到仿真器的 Visual Studio 解决方案。
  • 出现提示时,请选择“全部重新加载”。
  • 单击“调试”>“开始执行(不调试)”或按 Ctrl+F5
  • 使用 Xbox 控制器来环视场景。 注意光标如何与对象的形状交互。

第 3 章 - 手势

在本章中,我们将添加对手势的支持。 当用户选择某个纸球时,我们将使用 Unity 的物理引擎打开重力,使纸球掉落。

目标

  • 使用“选择”手势控制全息影像。

说明

我们首先创建一个脚本,然后就可以检测“选择”手势。

  • 在 “Scripts” 文件夹中,创建名为 “GazeGestureManager” 的脚本
  • 将“GazeGestureManager”脚本拖放到“层次结构”中的“OrigamiCollection”对象上
  • 在 Visual Studio 中打开“GazeGestureManager”脚本并添加以下代码
using UnityEngine;
using UnityEngine.XR.WSA.Input;

public class GazeGestureManager : MonoBehaviour
{
    public static GazeGestureManager Instance { get; private set; }

    // Represents the hologram that is currently being gazed at.
    public GameObject FocusedObject { get; private set; }

    GestureRecognizer recognizer;

    // Use this for initialization
    void Start()
    {
        Instance = this;

        // Set up a GestureRecognizer to detect Select gestures.
        recognizer = new GestureRecognizer();
        recognizer.Tapped += (args) =>
        {
            // Send an OnSelect message to the focused object and its ancestors.
            if (FocusedObject != null)
            {
                FocusedObject.SendMessageUpwards("OnSelect", SendMessageOptions.DontRequireReceiver);
            }
        };
        recognizer.StartCapturingGestures();
    }

    // Update is called once per frame
    void Update()
    {
        // Figure out which hologram is focused this frame.
        GameObject oldFocusObject = FocusedObject;

        // Do a raycast into the world based on the user's
        // head position and orientation.
        var headPosition = Camera.main.transform.position;
        var gazeDirection = Camera.main.transform.forward;

        RaycastHit hitInfo;
        if (Physics.Raycast(headPosition, gazeDirection, out hitInfo))
        {
            // If the raycast hit a hologram, use that as the focused object.
            FocusedObject = hitInfo.collider.gameObject;
        }
        else
        {
            // If the raycast did not hit a hologram, clear the focused object.
            FocusedObject = null;
        }

        // If the focused object changed this frame,
        // start detecting fresh gestures again.
        if (FocusedObject != oldFocusObject)
        {
            recognizer.CancelGestures();
            recognizer.StartCapturingGestures();
        }
    }
}
  • 在“Scripts”文件夹中创建另一个脚本,这次请将该脚本命名为“SphereCommands”
  • 在“层次结构”视图中展开“OrigamiCollection”对象。
  • 将“SphereCommands”脚本拖放到“层次结构”面板中的“Sphere1”对象上
  • 将“SphereCommands”脚本拖放到“层次结构”面板中的“Sphere2”对象上
  • 在 Visual Studio 中打开该脚本进行编辑,并将默认代码替换为以下代码:
using UnityEngine;

public class SphereCommands : MonoBehaviour
{
    // Called by GazeGestureManager when the user performs a Select gesture
    void OnSelect()
    {
        // If the sphere has no Rigidbody component, add one to enable physics.
        if (!this.GetComponent<Rigidbody>())
        {
            var rigidbody = this.gameObject.AddComponent<Rigidbody>();
            rigidbody.collisionDetectionMode = CollisionDetectionMode.Continuous;
        }
    }
}
  • 导出应用,生成应用并将其部署到 HoloLens 仿真器。
  • 环视场景,将注意力集中在其中一个球体上。
  • 按 Xbox 控制器上的 A 按钮,或按空格键来模拟“选择”手势。

第 4 章 - 语音

在本章中,我们将添加对两个语音命令的支持:“重置世界”(将掉落的球体返回其原始位置)和“掉落球体”(使球体掉落)。

目标

  • 添加始终在后台聆听的语音命令。
  • 创建对语音命令做出反应的全息影像。

说明

  • 在“Scripts”文件夹中,创建名为“SpeechManager”的脚本
  • 将“SpeechManager”脚本拖放到“层次结构”中的“OrigamiCollection”对象上
  • 在 Visual Studio 中打开“SpeechManager”脚本
  • 将以下代码复制并粘贴到“SpeechManager.cs”中并选择“全部保存”
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using UnityEngine.Windows.Speech;

public class SpeechManager : MonoBehaviour
{
    KeywordRecognizer keywordRecognizer = null;
    Dictionary<string, System.Action> keywords = new Dictionary<string, System.Action>();

    // Use this for initialization
    void Start()
    {
        keywords.Add("Reset world", () =>
        {
            // Call the OnReset method on every descendant object.
            this.BroadcastMessage("OnReset");
        });

        keywords.Add("Drop Sphere", () =>
        {
            var focusObject = GazeGestureManager.Instance.FocusedObject;
            if (focusObject != null)
            {
                // Call the OnDrop method on just the focused object.
                focusObject.SendMessage("OnDrop", SendMessageOptions.DontRequireReceiver);
            }
        });

        // Tell the KeywordRecognizer about our keywords.
        keywordRecognizer = new KeywordRecognizer(keywords.Keys.ToArray());

        // Register a callback for the KeywordRecognizer and start recognizing!
        keywordRecognizer.OnPhraseRecognized += KeywordRecognizer_OnPhraseRecognized;
        keywordRecognizer.Start();
    }

    private void KeywordRecognizer_OnPhraseRecognized(PhraseRecognizedEventArgs args)
    {
        System.Action keywordAction;
        if (keywords.TryGetValue(args.text, out keywordAction))
        {
            keywordAction.Invoke();
        }
    }
}
  • 在 Visual Studio 中打开“SphereCommands”脚本
  • 按如下所示更新脚本:
using UnityEngine;

public class SphereCommands : MonoBehaviour
{
    Vector3 originalPosition;

    // Use this for initialization
    void Start()
    {
        // Grab the original local position of the sphere when the app starts.
        originalPosition = this.transform.localPosition;
    }

    // Called by GazeGestureManager when the user performs a Select gesture
    void OnSelect()
    {
        // If the sphere has no Rigidbody component, add one to enable physics.
        if (!this.GetComponent<Rigidbody>())
        {
            var rigidbody = this.gameObject.AddComponent<Rigidbody>();
            rigidbody.collisionDetectionMode = CollisionDetectionMode.Continuous;
        }
    }

    // Called by SpeechManager when the user says the "Reset world" command
    void OnReset()
    {
        // If the sphere has a Rigidbody component, remove it to disable physics.
        var rigidbody = this.GetComponent<Rigidbody>();
        if (rigidbody != null)
        {
            rigidbody.isKinematic = true;
            Destroy(rigidbody);
        }

        // Put the sphere back into its original local position.
        this.transform.localPosition = originalPosition;
    }

    // Called by SpeechManager when the user says the "Drop sphere" command
    void OnDrop()
    {
        // Just do the same logic as a Select gesture.
        OnSelect();
    }
}
  • 导出应用,生成应用并将其部署到 HoloLens 仿真器。
  • 仿真器会支持电脑的麦克风并对你的语音做出响应:调整视图,使光标位于球体之一上,并说“掉落球体”。
  • 说出“重置世界”,让球体回到初始位置

第 5 章 - 空间音效

在本章中,我们将在应用中添加音乐,然后对某些操作触发音效。 我们将使用空间音效在 3D 空间中的特定位置提供音效。

目标

  • 在世界中聆听全息影像。

说明

  • 在 Unity 中,从顶部菜单中选择“编辑”>“项目设置”>“音频”
  • 找到“空间定位器插件”设置,选择“MS HRTF 空间定位器”
  • 从 “Holograms” 文件夹中,将 “Ambience” 对象拖放到“层次结构面板”中的 “OrigamiCollection”对象上
  • 选择 “OrigamiCollection”,并找到“音频源”组件。 更改这些属性:
    • 选中“空间化”属性
    • 选中“唤醒时播放”
    • 朝右侧拖动滑块,将“空间混合”更改为“3D”
    • 选中“循环”属性
    • 展开“3D 音效设置”,然后为“多普勒级别”输入“0.1”
    • 将“音量衰减”设置为“对数衰减”
    • 将“最大距离”设置为“20”
  • 在“Scripts”文件夹中,创建名为“SphereSounds”的脚本
  • 将 “SphereSounds” 拖到“层次结构”中的 “Sphere1” 和 “Sphere2” 对象上
  • 在 Visual Studio 中打开 “SphereSounds”,更新以下代码并选择“全部保存”
using UnityEngine;

public class SphereSounds : MonoBehaviour
{
    AudioSource impactAudioSource = null;
    AudioSource rollingAudioSource = null;

    bool rolling = false;

    void Start()
    {
        // Add an AudioSource component and set up some defaults
        impactAudioSource = gameObject.AddComponent<AudioSource>();
        impactAudioSource.playOnAwake = false;
        impactAudioSource.spatialize = true;
        impactAudioSource.spatialBlend = 1.0f;
        impactAudioSource.dopplerLevel = 0.0f;
        impactAudioSource.rolloffMode = AudioRolloffMode.Logarithmic;
        impactAudioSource.maxDistance = 20f;

        rollingAudioSource = gameObject.AddComponent<AudioSource>();
        rollingAudioSource.playOnAwake = false;
        rollingAudioSource.spatialize = true;
        rollingAudioSource.spatialBlend = 1.0f;
        rollingAudioSource.dopplerLevel = 0.0f;
        rollingAudioSource.rolloffMode = AudioRolloffMode.Logarithmic;
        rollingAudioSource.maxDistance = 20f;
        rollingAudioSource.loop = true;

        // Load the Sphere sounds from the Resources folder
        impactAudioSource.clip = Resources.Load<AudioClip>("Impact");
        rollingAudioSource.clip = Resources.Load<AudioClip>("Rolling");
    }

    // Occurs when this object starts colliding with another object
    void OnCollisionEnter(Collision collision)
    {
        // Play an impact sound if the sphere impacts strongly enough.
        if (collision.relativeVelocity.magnitude >= 0.1f)
        {
            impactAudioSource.Play();
        }
    }

    // Occurs each frame that this object continues to collide with another object
    void OnCollisionStay(Collision collision)
    {
        Rigidbody rigid = gameObject.GetComponent<Rigidbody>();

        // Play a rolling sound if the sphere is rolling fast enough.
        if (!rolling && rigid.velocity.magnitude >= 0.01f)
        {
            rolling = true;
            rollingAudioSource.Play();
        }
        // Stop the rolling sound if rolling slows down.
        else if (rolling && rigid.velocity.magnitude < 0.01f)
        {
            rolling = false;
            rollingAudioSource.Stop();
        }
    }

    // Occurs when this object stops colliding with another object
    void OnCollisionExit(Collision collision)
    {
        // Stop the rolling sound if the object falls off and stops colliding.
        if (rolling)
        {
            rolling = false;
            impactAudioSource.Stop();
            rollingAudioSource.Stop();
        }
    }
}
  • 保存脚本,然后返回 Unity。
  • 导出应用,生成应用并将其部署到 HoloLens 仿真器。
  • 戴耳机可以听到全部效果,靠近场地和远离场地可以听出声音变化。

第 6 章 - 空间映射

现在我们使用空间映射将棋盘放在现实世界中的真实对象上。

目标

  • 将现实世界带入虚拟世界。
  • 将全息影像放在对你最重要的位置。

说明

  • 在“项目”面板中单击 “Holograms” 文件夹
  • 将“空间映射”资产拖放到“层次结构”的根目录中
  • 在“层次结构”中单击“空间映射”对象
  • 在“检查器面板”中更改以下属性
    • 选中“绘制视觉网格”框
    • 找到“绘制材料”并单击右侧的圆圈。 在顶部的搜索字段中输入“线框”。 单击结果,然后关闭窗口。
  • 导出应用,生成应用并将其部署到 HoloLens 仿真器。
  • 当应用运行时,先前扫描的现实世界客厅的网格将以线框形式渲染。
  • 观察滚动的球体如何从场地掉落到地面上!

现在我们演示如何将 OrigamiCollection 移到新位置:

  • 在“Scripts”文件夹中,创建名为“TapToPlaceParent”的脚本
  • 在“层次结构”中,展开“OrigamiCollection”并选择“Stage”对象
  • 将“TapToPlaceParent”脚本拖放到“Stage”对象上
  • 在 Visual Studio 中打开“TapToPlaceParent”脚本,将其更新为
using UnityEngine;

public class TapToPlaceParent : MonoBehaviour
{
    bool placing = false;

    // Called by GazeGestureManager when the user performs a Select gesture
    void OnSelect()
    {
        // On each Select gesture, toggle whether the user is in placing mode.
        placing = !placing;

        // If the user is in placing mode, display the spatial mapping mesh.
        if (placing)
        {
            SpatialMapping.Instance.DrawVisualMeshes = true;
        }
        // If the user is not in placing mode, hide the spatial mapping mesh.
        else
        {
            SpatialMapping.Instance.DrawVisualMeshes = false;
        }
    }

    // Update is called once per frame
    void Update()
    {
        // If the user is in placing mode,
        // update the placement to match the user's gaze.

        if (placing)
        {
            // Do a raycast into the world that will only hit the Spatial Mapping mesh.
            var headPosition = Camera.main.transform.position;
            var gazeDirection = Camera.main.transform.forward;

            RaycastHit hitInfo;
            if (Physics.Raycast(headPosition, gazeDirection, out hitInfo,
                30.0f, SpatialMapping.PhysicsRaycastMask))
            {
                // Move this object's parent object to
                // where the raycast hit the Spatial Mapping mesh.
                this.transform.parent.position = hitInfo.point;

                // Rotate this object's parent object to face the user.
                Quaternion toQuat = Camera.main.transform.localRotation;
                toQuat.x = 0;
                toQuat.z = 0;
                this.transform.parent.rotation = toQuat;
            }
        }
    }
}
  • 导出、生成并部署应用。
  • 现在,你应该可以通过以下方式将游戏放到特定的位置:凝视游戏,使用“选择”手势(A 或空格键)并将其移到新位置,然后再次使用“选择”手势。

结束

本教程到此结束!

你已了解:

  • 如何在 Unity 中创建全息应用。
  • 如何使用凝视、手势、语音、音效和空间映射。
  • 如何使用 Visual Studio 生成和部署应用。

现在你可以开始创建自己的全息应用了!

另请参阅