Hololens 实现全息体验的一个特性就是场景保持。当用户离开场景或关闭应用时,场景中的全息图会被保存在所放置的位置,当用户回到场景或重新打开应用时,能够准确的还原之前场景内的全息内容。
World Anchor(空间锚)提供了一种能够将物体保留在特定位置和旋转状态上的方法,以此来保证全息对象的稳定性(即静止参考框架),也通过它来实现场景保持。
脚本WorldAnchorStore.cs 是实现空间锚特性的关键 API,为了能够真正保持一个全息对象,通常为根 GameObject 添加空间锚,同时对其子 GameObject 也附上具有相对位置偏移的空间锚组件。
一、实例程序
(一)、 用 unity2018.4.9 vs2017 创建一个新的 Unity 项目 VoiceDemo,初始化项目:
1.导入 MRTK 包 (版本 HoloToolkit-Unity-2017.4.2.0)
2.应用项目设置为 MR 项目 (一键设置成为可以部署的环境)
3.使用 HoloLensCamera 替代默认相机
4.添加 CursorWithFeedback (识别并反馈手势的光标控件)
5.添加 InputManager (作为输入源管理器,管理 gaze,gesture,speech等)
6.设置 InputManager 的 SimpleSinglePointerSelector 脚本的 Cursor 属性为添加的 CursorWithFeedback (添加手势源到inputmanger)
7.添加一个 Cube 改动z:4
最终 Hierarchy 结构如下:
(二)、编写脚本 CubeCommand.cs 并将其添加到 Cube 上。
当添加脚本遇到错误时,更改脚本名称试试。
项目实验效果: 打开程序,立方体位于前方4m处。 点击立方体时,立方体会随着视野移动,再次点击则被放置。 重新打开程序时,立方体的位置已经改变。
using UnityEngine;
using HoloToolkit.Unity.InputModule;
using UnityEngine.XR.WSA;
using UnityEngine.XR.WSA.Persistence;
using System.Linq;
public class CubeCommand1 : MonoBehaviour, IInputClickHandler //因为要《点击跟随视野》和《点击放置》,引用该接口 IInputClickHandler
{
// 定义对象: 被保存的锚点
public string ObjectAnchorStoreName;
//定义对象: 存储锚点的仓库 属于UnityEngine.XR.WSA.Persistence
WorldAnchorStore anchorStore;
// 是否可被移动
bool HasMove = false;
void Start()
{
WorldAnchorStore.GetAsync(AnchorStoreReady); //WorldAnchorStore的静态方法,获取WorldAnchorStore实例。
}
private void AnchorStoreReady(WorldAnchorStore store)
{
anchorStore = store;
if (anchorStore.GetAllIds().Contains(ObjectAnchorStoreName)) //GetAllIds获取当前持久化的WorldAnchors的所有标识符,返回的时string。判断是否有当前的锚点
{
anchorStore.Load(ObjectAnchorStoreName, gameObject); //若锚点存在,则加载到游戏对象上。
}
}
void Update()
{
// 如果立方体可移动,更新其位置
if (HasMove)
{
gameObject.transform.position = Camera.main.transform.position + Camera.main.transform.forward * 2;
}
}
public void OnInputClicked(InputClickedEventData eventData) // IInputClickHandler接口调用的方法 点击就调用
{
if (anchorStore == null)
{
return;
}
if (HasMove)
{
// 当物体处于移动状态,且再次被点击后
WorldAnchor anchor = gameObject.AddComponent<WorldAnchor>();
if (anchor.isLocated)
{
anchorStore.Save(ObjectAnchorStoreName, anchor);
}
else
{
anchor.OnTrackingChanged += Anchor_OnTrackingChanged;
}
}
else
{
// 当物体处于不可移动,且再次被点击后
WorldAnchor anchor = gameObject.GetComponent<WorldAnchor>();
if (anchor != null)
{
DestroyImmediate(anchor);
}
if (anchorStore.GetAllIds().Contains(ObjectAnchorStoreName))
{
anchorStore.Delete(ObjectAnchorStoreName);
}
}
HasMove = !HasMove;
}
void Anchor_OnTrackingChanged(WorldAnchor self, bool located)
{
if (located)
{
anchorStore.Save(ObjectAnchorStoreName, self);
// 取消事件监听
self.OnTrackingChanged -= Anchor_OnTrackingChanged;
}
}
}
三、相关API解析
添加命名空间:
using UnityEngine.XR.WSA;
using UnityEngine.XR.WSA.Persistence;
(1)为物体添加空间锚
WorldAnchor anchor = gameObject.AddComponent<WorldAnchor>();
(2)销毁物体上的空间锚
当物体被添加空间锚后,该物体不能够再移动。
假设要单纯的销毁空间锚,不需要移动物体,则使用Destroy():
Destroy(gameObject.GetComponent<WorldAnchor>());
假设销毁空间锚,之后需要移动物体,使用 DestroyImmediate() 来销毁空间锚:
DestroyImmediate(gameObject.GetComponent<WorldAnchor>());
(3)移动已经添加空间锚的物体
之前说过物体被添加空间锚后无法移动,因此步骤如下:
- 销毁空间锚
- 移动物体
- 重新添加空间锚
DestroyImmediate(gameObject.GetComponent<WorldAnchor>());
gameObject.transform.position = new Vector3(0, 0, 2);
WorldAnchor anchor = gameObject.AddComponent<WorldAnchor>();
(4)读取已保存的所有空间锚
通过调用 WorldAnchorStore.GetAsync() 来加载所有保存的空间锚。
void Start () {
WorldAnchorStore.GetAsync(AnchorStoreReady);
}
private void AnchorStoreReady(WorldAnchorStore store)
{
// 读取所有已保存的空间锚
WorldAnchorStore anchorStore = store;
string[] ids = anchorStore.GetAllIds();
}
(5)保存空间锚
/**
* 返回是否保存成功
* @Param anchorName: 保存的锚点名
* @Param anchor: 物体上的锚点组件
*/
bool saved = anchorStore.Save(anchorName, anchor);
(6)加载已保存的空间锚到物体上
/**
* 当加载成功时返回锚点对象
* @Param anchorName: 保存的锚点名
* @Param gameObject: 被添加空间锚的目标对象
*/
WorldAnchor anchor = anchorStore.Load(anchorName, gameObject);
(7)删除已保存的空间锚
/**
* 返回是否删除成功
* @Param anchorName: 删除的锚点名
*/
bool deleted = anchorStore.Delete(anchorName);
(8)OnTrackingChanged 事件
当我们为物体添加空间锚的情况下,有些情况空间锚会被立即定位到,即:
WorldAnchor anchor = gameObject.AddComponent<WorldAnchor>();
// anchor.isLocated == true
但是有些情况下不会被立即定位到,我们可以为空间锚绑定 OnTrackingChanged 事件,当它定位成功后,再继续后面的逻辑。
anchor.OnTrackingChanged += Anchor_OnTrackingChanged;
例如,我们需要为物体添加空间锚,等到被定位后将其保存起来,那么代码大概如下:
void OnSelect() {
WorldAnchor anchor = gameObject.AddComponent<WorldAnchor>();
if(anchor.isLocated) {
anchorStore.Save("测试锚点名", anchor);
} else {
anchor.OnTrackingChanged += Anchor_OnTrackingChanged;
}
}
void Anchor_OnTrackingChanged(WorldAnchor self, bool located) {
if(located) {
anchorStore.Save("测试锚点名", self);
// 取消事件监听
self.OnTrackingChanged -= Anchor_OnTrackingChanged;
}
}
四、锚点共享
锚点可以在多个设备间共享,来使得不同设备可以使用相同的空间位置,可以通过 WorldAnchorTransferBatch
将锚点信息导出为byte数组,在另外一台设备中加载这个数组并重新还原出锚点信息。