本文提供相关源码,请放心食用,详见网页侧边栏或底部,有疑问请评论或 Issue
HoloLens 实现全息体验的一个特性就是场景保持。当用户离开场景或关闭应用时,场景中的全息图会被保存在所放置的位置,当用户回到场景或重新打开应用时,能够准确的还原之前场景内的全息内容。
World Anchor(空间锚)
提供了一种能够将物体保留在特定位置和旋转状态上的方法,以此来保证全息对象的稳定性(即静止参考框架),也通过它来实现场景保持。
WorldAnchorStore
是实现空间锚特性的关键 API,为了能够真正保持一个全息对象,通常为根 GameObject 添加空间锚,同时对其子 GameObject 也附上具有相对位置偏移的空间锚组件。
一、相关 API
添加命名空间:
1 2
| using UnityEngine.XR.WSA; using UnityEngine.XR.WSA.Persistence;
|
(1)为物体添加空间锚
1
| WorldAnchor anchor = gameObject.AddComponent<WorldAnchor>();
|
(2)销毁物体上的空间锚
当物体被添加空间锚后,该物体不能够再移动。
单纯的销毁空间锚,不需要移动物体:
1
| Destroy(gameObject.GetComponent<WorldAnchor>());
|
需要移动物体,使用 DestroyImmediate 来销毁空间锚:
1
| DestroyImmediate(gameObject.GetComponent<WorldAnchor>());
|
(3)移动已经添加空间锚的物体
之前说过物体被添加空间锚后无法移动,因此步骤如下:
- 销毁空间锚
- 移动物体
- 重新添加空间锚
1 2 3
| DestroyImmediate(gameObject.GetComponent<WorldAnchor>()); gameObject.transform.position = new Vector3(0, 0, 2); WorldAnchor anchor = gameObject.AddComponent<WorldAnchor>();
|
(4)读取已保存的所有空间锚
通过调用 WorldAnchorStore.GetAsync() 来加载所有保存的空间锚。
1 2 3 4 5 6 7 8 9 10
| void Start () { WorldAnchorStore.GetAsync(AnchorStoreReady); }
private void AnchorStoreReady(WorldAnchorStore store) { WorldAnchorStore anchorStore = store; string[] ids = anchorStore.GetAllIds(); }
|
(5)保存空间锚
1 2 3 4 5 6
|
bool saved = anchorStore.Save(anchorName, anchor);
|
(6)加载已保存的空间锚到物体上
1 2 3 4 5 6
|
WorldAnchor anchor = anchorStore.Load(anchorName, gameObject);
|
(7)删除已保存的空间锚
1 2 3 4 5
|
bool deleted = anchorStore.Delete(anchorName);
|
(8)OnTrackingChanged 事件
当我们为物体添加空间锚的情况下,有些情况空间锚会被立即定位到,即:
1 2
| WorldAnchor anchor = gameObject.AddComponent<WorldAnchor>();
|
但是有些情况下不会被立即定位到,我们可以为空间锚绑定 OnTrackingChanged 事件,当它定位成功后,再继续后面的逻辑。
1
| anchor.OnTrackingChanged += Anchor_OnTrackingChanged;
|
例如,我们需要为物体添加空间锚,等到被定位后将其保存起来,那么代码大概如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| 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; } }
|
二、示例程序
使用 MRTK 初始化一个 HoloLens 应用:
- 删除默认相机,使用 HoloToolkit / Input / Prefabs / HoloLensCamera 替代
- 添加 HoloToolkit / Input / Prefabs / Cursor / CursorWithFeedback
- 添加 HoloToolkit / Input / Prefabs / InputManager,设置其 Simple Single Pointer Selector 的 Cursor 为上一步的 Cursor。
- 添加一个 Cube,它的 Position 为 (X:0, Y:0, Z:4), Scale 为 (X:0.25, Y:0.25, Z:0.25)
- 编写脚本 CubeCommand 并将其添加到 Cube 上。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88
| using UnityEngine; using HoloToolkit.Unity.InputModule; using UnityEngine.XR.WSA; using UnityEngine.XR.WSA.Persistence; using System.Linq;
public class CubeCommand : MonoBehaviour, IInputClickHandler { public string ObjectAnchorStoreName;
WorldAnchorStore anchorStore;
bool HasMove = false; void Start () { WorldAnchorStore.GetAsync(AnchorStoreReady); }
private void AnchorStoreReady(WorldAnchorStore store) { anchorStore = store;
if (anchorStore.GetAllIds().Contains(ObjectAnchorStoreName)) { 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) { 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; } } }
|
- 运行程序
初始位置位于靠近屋顶:
通过点击事件,将其拖拽到地上:
关闭程序,重新打开后,物体仍然停留在地上。
三、锚点共享
锚点可以在多个设备间共享,来使得不同设备可以使用相同的空间位置,可以通过 WorldAnchorTransferBatch
将锚点信息导出为byte数组,在另外一台设备中加载这个数组并重新还原出锚点信息。
(1)锚点导出方
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| using System.Collections.Generic; using UnityEngine; using UnityEngine.XR.WSA; using UnityEngine.XR.WSA.Sharing;
public class ExportAnchorScript : MonoBehaviour { public string exportingAnchorName; private List<byte> exportingAnchorBytes = new List<byte>();
void Start () { WorldAnchorTransferBatch transferBatch = new WorldAnchorTransferBatch(); transferBatch.AddWorldAnchor(exportingAnchorName, transform.GetComponent<WorldAnchor>()); WorldAnchorTransferBatch.ExportAsync(transferBatch, OnExportDataAvailable, OnExportComplete); }
private void OnExportDataAvailable(byte[] data) { exportingAnchorBytes.AddRange(data); }
private void OnExportComplete(SerializationCompletionReason completionReason) { if (completionReason == SerializationCompletionReason.Succeeded) { Debug.Log("share anchor complete"); } else {
} } }
|
(2)锚点导入方
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50
| using System.Collections.Generic; using UnityEngine; using UnityEngine.XR.WSA; using UnityEngine.XR.WSA.Sharing;
public class ImportAnchorScript : MonoBehaviour { public string exportingAnchorName; public GameObject targetObject; int retryCount = 5;
private List<byte> exportingAnchorBytes = new List<byte>(); void Start () { WorldAnchorTransferBatch.ImportAsync(exportingAnchorBytes.ToArray(), OnImportComplete); }
private void OnImportComplete(SerializationCompletionReason completionReason, WorldAnchorTransferBatch deserializedTransferBatch) { if (completionReason != SerializationCompletionReason.Succeeded) { Debug.Log("Failed to import: " + completionReason.ToString()); if (retryCount > 0) { retryCount--; WorldAnchorTransferBatch.ImportAsync(exportingAnchorBytes.ToArray(), OnImportComplete); } return; }
string[] ids = deserializedTransferBatch.GetAllIds(); Debug.Log("load anchor count " + ids.Length); foreach (string id in ids) { Debug.Log("load anchor " + id); if (targetObject != null && id.Equals(exportingAnchorName)) { Debug.Log("find anchor form share"); if (targetObject.GetComponent<WorldAnchor>() == null) { targetObject.AddComponent<WorldAnchor>(); }
deserializedTransferBatch.LockObject(id, targetObject); return; } } } }
|
HoloLens 开发笔记(10)——World Anchor