HoloLens(第一代)基础知识 101:使用设备完成项目


重要

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


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

完成本教程大约需要 1 小时。

设备支持

课程 HoloLens 沉浸式头戴显示设备
MR 基础 101 - 使用设备完成项目

开始之前

先决条件

项目文件

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

注意

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

第 1 章 -“全息”世界

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

目标

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

说明

  • 启动 “Unity”。
  • 选择打开
  • 输入先前解压缩的“Origami”文件夹所在的位置
  • 选择 “Origami”,然后单击“选择文件夹”
  • 由于 Origami 项目不包含场景,请使用以下命令将空的默认场景保存到新文件:“文件” / “场景另存为”
  • 将新场景命名为“Origami”,然后按“保存”按钮

设置主虚拟相机

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

设置场景

  • 在“层次结构”面板中,单击“创建”和“创建空白项”
  • 右键单击新的“GameObject”并选择“重命名”。 将 GameObject 重命名为“OrigamiCollection”
  • 从“项目”面板中的“Holograms”文件夹(展开“资产”并选择“Holograms”,或双击“项目”面板中的“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# 项目”

  • 单击“添加开放式场景”以添加场景

  • 单击“生成”

  • 在出现的文件资源管理器窗口中,创建名为“App”的新文件夹

  • 单击“App”文件夹

  • 按“选择文件夹”

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

  • 打开“App”文件夹。

  • 打开(双击)“Origami.sln”

  • 在 Visual Studio 中使用顶部工具栏,将目标从“调试”更改为“发布”,并从“ARM”更改为“X86”

  • 单击“设备”按钮旁边的箭头,然后选择“远程计算机”以通过 Wi-Fi 进行部署

    • 将“地址”设置为 HoloLens 的名称或 IP 地址。 如果你不知道自己的设备 IP 地址,可以在“设置”>“网络和 Internet”>“高级选项”中找到,或询问 Cortana“你好小娜,我的 IP 地址是什么?”
    • 如果 HoloLens 是通过 USB 连接的,你可以改为选择“设备”以通过 USB 进行部署
    • 将“身份验证模式”保留为“通用”
    • 单击“选择”
  • 单击“调试”>“开始执行(不调试)”或按 Ctrl+F5。 如果这是你第一次部署到设备,需要将设备与 Visual Studio 配对

  • Origami 项目现在将生成并部署到 HoloLens,然后运行。

  • 戴上 HoloLens 并环顾四周,以观看新的全息影像。

第 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 the cursor 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;
        }
    }
}
  • 选择“文件”>“生成设置”以重新生成应用
  • 返回到前面用于部署到 HoloLens 的 Visual Studio 解决方案。
  • 出现提示时,请选择“全部重新加载”。
  • 单击“调试”>“开始执行(不调试)”或按 Ctrl+F5
  • 现在环顾场景并注意光标如何与对象的形状交互。

第 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 Awake()
    {
        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。
  • 注视某个球体。
  • 执行选择手势,观察球体掉落到下方的表面上。

第 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”。 移动滑块时,值应从 0 更改为 1。
    • 选中“循环”属性
    • 展开“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 章 - 空间映射

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

目标

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

说明

  • 在 Unity 的“项目”面板中单击“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;
            }
        }
    }
}
  • 导出、生成并部署应用。
  • 现在,你应该可以通过以下方式将游戏放到特定的位置:凝视游戏,使用“选择”手势并将其移到新位置,然后再次使用“选择”手势。

第 7 章 - 全息的乐趣

目标

  • 揭示全息地下世界的入口。

说明

现在我们演示如何揭示全息地下世界:

  • 从“项目”面板中的“Holograms”文件夹中
    • 将“Underworld”拖放到“层次结构”中,使之成为“OrigamiCollection”的子项
  • 在“Scripts”文件夹中,创建名为“HitTarget”的脚本
  • 在“层次结构”中,展开“OrigamiCollection”
  • 展开“Stage”对象并选择“Target”对象(蓝色扇形)
  • 将“HitTarget”脚本拖放到“Target”对象上
  • 在 Visual Studio 中打开“HitTarget”脚本,将其更新为
using UnityEngine;

public class HitTarget : MonoBehaviour
{
    // These public fields become settable properties in the Unity editor.
    public GameObject underworld;
    public GameObject objectToHide;

    // Occurs when this object starts colliding with another object
    void OnCollisionEnter(Collision collision)
    {
        // Hide the stage and show the underworld.
        objectToHide.SetActive(false);
        underworld.SetActive(true);

        // Disable Spatial Mapping to let the spheres enter the underworld.
        SpatialMapping.Instance.MappingEnabled = false;
    }
}
  • 在 Unity 中,选择“Target”对象
  • 现在,“命中目标”组件上会显示两个公共属性,它们需要引用我们场景中的对象
    • 将“Underworld”从“层次结构”面板拖放到“命中目标”组件上的“地下世界”属性
    • 将“Stage”从“层次结构”面板拖放到“命中目标”组件上的“要隐藏的对象”属性
  • 导出、生成并部署应用。
  • 将折纸藏品放在地面上,然后使用“选择”手势使球体掉落。
  • 当球体命中目标(蓝色扇形)时,会发生爆炸。 藏品将会隐藏,并出现一个通往地下世界的洞口。

结束

本教程到此结束!

你已了解:

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

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

另请参阅