本文我们将通过Unity创建一个包括:凝视(gaze)、手势(gesture)、语音输入(voice)、空间声音(spatial sound)和空间映射(spatial mapping)的完整的 Hololens 项目,并在 Hololens Emulator 模拟器上运行。
译者注:本文与 Holograms 101 类似,区别在于部署部分,本文 101E 是使用模拟器运行,101 是直接使用 HoloLens。
目录:
Chapter0 - 预先准备
开发环境
一个正确配置Hololens 开发环境的 Win10 PC机。
项目文件
Chapter1 - “Holo” World
在这个章节中,我们将新建一个Unity项目,并且走一遍build和deploy的流程。
- 准备
- 打开 Unity。
- 点击 Open。
- 找到你之前解压的 Origami 文件夹。
- 选中 Origami,然后点击 Select Folder。
- 保存当前 Scene : File > Save Scene As。
- 把 Scene 命名为 Origami ,然后单击 Save。
- 设置 Main Camera
- 选中 Main Camera。
- 设置其 Transform 组件中 Position 属性为 (0, 0, 0)。
- 找到 Clear Flags 属性, 把其值从 Skybox 改为 Solid color。
- 修改 Background 属性,其颜色 RGBA 为(0, 0, 0, 0)。
- 修改 Clipping Planes 属性的 Near 值为 0.85。(译者注:参考Holograms 100)
- 设置场景
- 在 Hierarchy 中, 单击 Create > Create Empty。
- 右键单击新的 GameObject 选择 Rename。 将 GameObject 重命名为 OrigamiCollection。
- 从 Project 面板中的 Holograms 文件夹里:
- 拖拽 Stage 进入 Hierarchy ,并作为 OrigamiCollection 的子物体。
- 拖拽 Sphere1 进入 Hierarchy, 并作为 OrigamiCollection 的子物体。
- 拖拽 Sphere2 进入 Hierarchy, 并作为 OrigamiCollection 的子物体。
- 右键单击 Directional Light 物体,选择 Delete 删除。
- 从 Holograms 文件夹中, 拖拽 Lights 到 Hierarchy 面板的根部。
- 选中 OrigamiCollection。
- 修改其 Transform 组件中的 Position 属性为 (0, -0.5, 2.0)。
- 单击 Play 按钮,看看现在效果如何。
- 再次单击 Play 按钮,退出预览模式。
- 导出项目到Visual Studio 2015
- 选择 File > Build Settings。
- 把 运行平台 即 Platform 修改为 Windows Store 并且单击 Switch Platform。.
- 设置 SDK 为 Universal 10 ,Build Type 为 D3D。
- UWP SDK 可以选 Latest installed。(译者注:最好与你装VS2015时安装的那个版本一致,否则VS会提示项目需要更新)
- 勾上 Unity C# Projects。
- 单击 Add Open Scenes ,添加当前场景.
- 单击 Player Settings….
- 在 Inspector 面板里, 选中 Windows Store logo。接着展开 Publishing Settings。
- 在 Capabilities 部分, 选中 Microphone 和 SpatialPerception。
- 回到 Build Settings 窗口, 单击 Build。
- 新建一个文件夹,命名为 APP。
- 单击选择 App 文件夹。
- 单击 Select Folder 按钮。
- 当Unity 完成 Building 的时候,会自动打开一个资源管理器。
- 打开 APP 文件夹。
- 打开生成的 Visual Studio Solution。
- 在VS工具栏中,把 target 从 Debug 改为 Release,从 ARM 改为 x86。
- 单击 本地计算机(Local Machine) 旁边的小箭头,将部署目标变为 Hololens Emulator。
- 点击 调试(Debug) > 开始执行不调试(Debug without debugging)
- 等待一会后,Emulator 将会打开 Origami 项目。当第一次运行Emulator时,它可能需要加载15分钟左右。当开始后,注意不要关闭它。
Hololens Emulator 效果图
Chapter 2 - Gaze(凝视)
在这个章节中,我们将介绍Hololens中的三种交互方式之一,Gaze(凝视)。
- 从 Holograms 文件夹中拖拽 Cursor 物体进入 Hierarchy。
- 在 Scripts 文件夹中新建一个 Script,命名为 WorldCursor。
- 把 WorldCursor 添加给 Cursor。
- 双击 WorldCursor ,在VS中编辑脚本。
- 复制并粘贴以下脚本,并保存。
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;
}
}
}
- 再次build该项目,并在 VS 中部署到模拟器运行查看效果(模拟器可以不用关闭~以便于下一次调试)。
Gaze 效果图 注意图中箭头所指
Chapter3 - Gestures(手势)
在本章节中,我们将添加手势控制的功能。当用户选择到一个场景中的纸球时,我们将使纸球基于Unity中的物理引擎下落。
- 在 Scripts 文件夹下,新创建一个脚本 GazeGestureManager。
- 把脚本 GazeGestureManager 添加给 OrigamiCollection 物体。
- 编辑 GazeGestureManager 脚本,添加以下代码:
using UnityEngine;
using UnityEngine.VR.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.TappedEvent += (source, tapCount, ray) =>
{
// Send an OnSelect message to the focused object and its ancestors.
if (FocusedObject != null)
{
FocusedObject.SendMessageUpwards("OnSelect");
}
};
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();
}
}
}
- 再新建一个脚本,命名为 SphereCommands。
- 将脚本 SphereCommands 添加给 Sphere1 和 Sphere2 物体。
- 编辑 SPhereCommands 脚本,添加以下代码:
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 Emulator上进行测试。
- 凝视一个纸球。
- 按下 Spacebar 键,用来模拟选择的手势。观察效果,小球落下。
Chapter4 - Voice 声音控制
在本章节中,我们将添加两种声音控制命令:
- “Reset World”:让已经降落的小球回到一开始的位置。
- “Drop Sphere”:让小球降落
- 首先,新建一个脚本 SpeechManager。
- 把脚本 SpeechManager 添加给 OrigamiCollection 物体。
- 打开脚本 SpeechManager 并添加如下代码:
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");
}
});
// 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();
}
}
}
- 打开脚本 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)
{
DestroyImmediate(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 Emulator上进行测试。
- 调整视点,凝视一个小球。然后说:”Drop Sphere!”
- 说 “Reset World” 让小球回到原来的位置。
Chapter5 - Spatial sound 空间声音
在本章节中,我们将为应用添加音乐,并给关键动作添加音效。我们将使用 Spatial sound 来给声音一个三维空间中具体的方位。
- 在 Unity Editor 中,选择 Edit > Project Settings > Audio。
- 找到 Spatializer Plugin 选项并选择 MS HRTF Spatializer。
- 从 Holograms 文件夹拖拽 Ambience 物体到 Hierarchy 面板中的 OrigamiCollection 物体上。
- 选中 OrigamiCollection 物体,找到 Audio Source 组件,修改以下属性:
- 勾选 Spatialize
- 勾选 Play on Awake
- 把 Spatial Blend 的滑块拖到最右,使其值为 3D。
- 勾选 Loop。
- 展开 3D Sound Settings,把 Doppler Level 的值修改为 0.1。
- 把 Volume Rolloff 设置为 Logarithmic Rolloff。
- 把 Max Distance 设置为 20。
- 新建一个脚本命名为 SphereSounds。
- 把脚本 SphereSounds 添加给 Sphere1 和Sphere2。
- 打开脚本添加如下代码:
using UnityEngine;
public class SphereSounds : MonoBehaviour
{
AudioSource audioSource = null;
AudioClip impactClip = null;
AudioClip rollingClip = null;
bool rolling = false;
void Start()
{
// Add an AudioSource component and set up some defaults
audioSource = gameObject.AddComponent<AudioSource>();
audioSource.playOnAwake = false;
audioSource.spatialize = true;
audioSource.spatialBlend = 1.0f;
audioSource.dopplerLevel = 0.0f;
audioSource.rolloffMode = AudioRolloffMode.Logarithmic;
audioSource.maxDistance = 20f;
// Load the Sphere sounds from the Resources folder
impactClip = Resources.Load<AudioClip>("Impact");
rollingClip = 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)
{
audioSource.clip = impactClip;
audioSource.Play();
}
}
// Occurs each frame that this object continues to collide with another object
void OnCollisionStay(Collision collision)
{
Rigidbody rigid = this.gameObject.GetComponent<Rigidbody>();
// Play a rolling sound if the sphere is rolling fast enough.
if (!rolling && rigid.velocity.magnitude >= 0.01f)
{
rolling = true;
audioSource.clip = rollingClip;
audioSource.Play();
}
// Stop the rolling sound if rolling slows down.
else if (rolling && rigid.velocity.magnitude < 0.01f)
{
rolling = false;
audioSource.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;
audioSource.Stop();
}
}
}
- 导出项目,部署到 Hololens Emulator上进行测试。
- 戴上耳机体验3D效果,靠近远离感受音量的变化。
Chapter6 - Spatial mapping 空间映射
在本章节,我们将通过 Spatial mapping 去实现把场景中的物体放置在真实世界的真实物体之上。
- 把 Holograms 文件夹中的 Spatial Mapping 拖入 Hierarchy。
- 选中 Spatial Mapping,修改其以下属性:
- 勾选 Draw Visual Meshes
- 为 Draw Material 指定 wireframe 材质。
- 导出项目,部署到 Hololens Emulator上进行测试。
- 当应用运行时,可以观察到一个提前扫描过的房间的网格以 wireframe 材质的样式出现了。
- 观察小球是如何掉出桌子,掉到地上的。
现在我将教给你如何让 ORIgamiCollections 移动到一个新的位置。
- 新建一个脚本,命名为 TapToPlaceParent。
- 把脚本拖给 Stage 物体。
- 打开脚本进行编辑,添加如下代码:
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;
}
}
}
}
- 导出项目,部署到 Hololens Emulator上进行测试。
- 现在,你可以移动物体到一个新的位置:首先凝视物体,然后使用选择手势(Spacebar),把视点移动到新的位置后,再次使用选择手势即可。
The end
到此结束啦~
你已经学会了:
- 如何在Unity里创建一个Hololens应用。
- 如何使用gaze, gesture, voice, sounds, 以及 spatial mapping。
- 如何导出项目、部署应用到模拟器上。
相信你现在已经准备好开发自己的 Hololens 应用啦~