教學課程:使用 Azure Spatial Anchors 建立新 HoloLens Unity 應用程式的逐步指示
本教學課程將示範如何使用 Azure Spatial Anchors 建立新的 HoloLens Unity 應用程式。
必要條件
若要完成本教學課程,請確定您具有下列項目︰
- 計算機 - 執行 Windows 的電腦
- Visual Studio Visual Studio 2019 隨 通用 Windows 平台 開發工作負載和 Windows 10 SDK(10.0.18362.0 或更新版本)元件一起安裝。 - visual Studio 的 C++/WinRT Visual Studio 延伸模組 (VSIX) 應該從 Visual Studio Marketplace 安裝。
- HoloLens - 已啟用開發人員模式的 HoloLens 裝置。 本文需要具有 Windows 10 2020 年 5 月更新的 HoloLens 裝置。 若要更新至 HoloLens 上的最新版本,請開啟 [設定 ] 應用程式,移至 [ 更新與安全性],然後選取 [ 檢查更新 ] 按鈕。
- Unity - Unity 2020.3.25 模組 通用 Windows 平台 組建支援和 Windows 組建支援 (IL2CPP)
建立和設定 Unity 專案
Create New Project] (Azure Functions:建立新專案)
- 在 Unity 中 樞中,選取 [ 新增專案]
- 選取 3D
- 輸入您的項目名稱並輸入儲存位置
- 選取 [建立專案 ],然後等候 Unity 建立您的專案
變更建置平臺
- 在您的 Unity 編輯器中,選取 [ 檔案>建置設定]
- 選取 [通用 Windows 平台 切換平臺]。 等到 Unity 完成處理所有檔案為止。
匯入 ASA 和 OpenXR
- 啟動 混合實境功能工具
- 選取您的項目路徑 - 包含資產、套件、ProjectSettings 等資料夾的資料夾 ,然後選取 [探索功能]
- 在 [Azure 混合實境服務] 底下,選取兩者
- Azure Spatial Anchors SDK Core
- 適用於 Windows 的 Azure Spatial Anchors SDK
- 在 [平台支援] 底下,選取
- 混合實境 OpenXR 外掛程式
注意
請確定您已重新整理目錄,且已針對每個版本選取最新版本
- 按 [取得功能] -->Import -->核准 -->Exit
- 重新聚焦 Unity 視窗時,Unity 會開始匯入模組
- 如果您收到有關使用新輸入系統的訊息,請選取 [ 是 ] 以重新啟動 Unity 並啟用後端。
設定項目設定
我們現在會設定一些 Unity 項目設定,以協助我們以 Windows 全像攝影 SDK 為目標進行開發。
變更 OpenXR 設定
- 選取 [檔案>建置設定] (可能仍可從上一個步驟開啟)
- 選取 [ 播放程式設定...
- 選取 XR 外掛程式管理
- 確定已選取 [通用 Windows 平台 設定] 索引標籤,然後核取 OpenXR 旁的方塊,然後選取 [Microsoft HoloLens 功能群組旁的方塊
- 選取 OpenXR 旁的黃色警告符號,以顯示所有 OpenXR 問題。
- 選取[ 全部修正]
- 若要修正「至少必須新增一個互動配置檔」的問題,請選取 [編輯] 以開啟OpenXR項目設定。 然後在 [互動配置檔] 下選取符號,+然後選取 [Microsoft手部互動配置檔]
變更質量設定
- 選取 [編輯>項目設定>品質]
- 在 通用 Windows 平台 標誌下方的數據行中,選取 [預設] 數據列中的箭號,然後選取 [非常低]。 當 [通用 Windows 平台] 數據行中的方塊為綠色時,您將知道設定已正確套用。
設定功能
- 移至 [ 編輯>項目設定>播放機 ] (您可能仍可從上一個步驟開啟它)。
- 確定已選取 [通用 Windows 平台 設定] 索引標籤
- 在 [ 發佈設定 組態] 區段中,啟用下列專案
- InternetClient
- InternetClientServer
- PrivateNetworkClientServer
- SpatialPerception (可能已啟用)
設定主相機
- 在 [ 階層面板] 中,選取 [主要相機]。
- 在 Inspector 中,將其轉換位置設定為 0,0,0。
- 尋找 Clear Flags 屬性,並將下拉式清單從 Skybox 變更為純色。
- 選取 [ 背景] 欄位以開啟色彩選擇器。
- 將 R、G、B 和 A 設定為 0。
- 選取底部的 [新增元件],然後將追蹤的姿勢驅動程式元件新增至相機
試試看 #1
您現在應該會有空的場景,可部署至 HoloLens 裝置。 若要測試所有專案是否正常運作,請在 Unity 中建置您的應用程式,並從 Visual Studio 加以部署。 請遵循 使用 Visual Studio 進行部署和偵錯 以執行此動作。 您應該會看到 Unity 開始畫面,然後顯示清楚的顯示。
建立 Spatial Anchors 資源
前往 Azure 入口網站。
在左窗格中,選取 [建立資源]。
使用搜尋方塊來搜尋 Spatial Anchors。
選取 [空間錨點],然後選取 [ 建立]。
在 [ 空間錨點帳戶 ] 窗格上,執行下列動作:
使用一般英數位元輸入唯一的資源名稱。
選取您要附加資源的訂用帳戶。
選取 [新建] 以建立資源群組。 將它命名為 myResourceGroup,然後選取 [ 確定]。
資源群組是一個邏輯容器,可在其中部署與管理 Azure 資源 (例如 Web 應用程式、資料庫和儲存體帳戶)。 例如,您可以選擇在稍候透過一個簡單的步驟刪除整個資源群組。
選取要放置資源的位置(區域)。
選取 [建立] 開始建立資源。
建立資源之後,Azure 入口網站 會顯示您的部署已完成。
選取 [前往資源] 。 您現在可以檢視資源屬性。
將資源的 [帳戶標識符 ] 值複製到文本編輯器中,以供稍後使用。
此外,將資源的 帳戶網域 值複製到文本編輯器中,以供稍後使用。
在 [設定] 底下,選取 [存取金鑰]。 將 [主要金鑰] 值 [帳戶金鑰] 複製到文字編輯器,以供稍後使用。
建立和新增腳本
- 在 [專案] 窗格中的 Unity 中,於 [資產] 資料夾中建立名為 Scripts 的新資料夾。
- 在資料夾中,以滑鼠右鍵按兩下 ->Create ->C# 腳稿。 將它命名為 AzureSpatialAnchorsScript
- 移至 GameObject ->Create Empty。
- 選取它,然後在 Inspector 中將它從 GameObject 重新命名為 AzureSpatialAnchors。
- 仍在
GameObject
- 將其位置設定為 0,0,0
- 選取 [新增元件 ] 並搜尋並新增 AzureSpatialAnchorsScript
- 再次選取 [新增元件 ],然後搜尋並新增 AR Anchor Manager。 這也會自動新增 AR會話來源 。
- 再次選取 [新增元件 ],然後搜尋並新增 SpatialAnchorManager 腳本
- 在新增的 SpatialAnchorManager 元件中,填寫您在上一個步驟中從 Azure 入口網站 空間錨點資源複製的帳戶標識碼、帳戶密鑰和帳戶網域。
應用程式概觀
我們的應用程式將支援下列互動:
手勢 | 動作 |
---|---|
點選 任何位置 | 開始/繼續會話 + 在手部位置建立錨點 |
點選錨點 | 刪除 + 刪除 GameObject ASA 雲端服務中的錨點 |
點選 +保留 2 秒 (+ 工作階段正在執行) | 停止工作階段並移除所有 GameObjects 。 在 ASA 雲端服務中保留錨點 |
點選 +保留 2 秒 (+ 工作階段未執行 ) | 啟動工作階段並尋找所有錨點。 |
新增點選辨識
讓我們將一些程式代碼新增至腳本,以辨識用戶的 點選手勢。
- 按兩下 Unity 專案窗格中的腳本,在 Visual Studio 中開啟
AzureSpatialAnchorsScript.cs
。 - 將下列數位新增至您的類別
public class AzureSpatialAnchorsScript : MonoBehaviour
{
/// <summary>
/// Used to distinguish short taps and long taps
/// </summary>
private float[] _tappingTimer = { 0, 0 };
- 在 Update() 方法下方新增下列兩種方法。 我們將在稍後階段新增實作
// Update is called once per frame
void Update()
{
}
/// <summary>
/// Called when a user is air tapping for a short time
/// </summary>
/// <param name="handPosition">Location where tap was registered</param>
private async void ShortTap(Vector3 handPosition)
{
}
/// <summary>
/// Called when a user is air tapping for a long time (>=2 sec)
/// </summary>
private async void LongTap()
{
}
- 新增下列匯入
using UnityEngine.XR;
- 將下列程式代碼新增到
Update()
方法頂端。 這可讓應用程式辨識短和長 (2 秒) 的手拍手勢
// Update is called once per frame
void Update()
{
//Check for any air taps from either hand
for (int i = 0; i < 2; i++)
{
InputDevice device = InputDevices.GetDeviceAtXRNode((i == 0) ? XRNode.RightHand : XRNode.LeftHand);
if (device.TryGetFeatureValue(CommonUsages.primaryButton, out bool isTapping))
{
if (!isTapping)
{
//Stopped Tapping or wasn't tapping
if (0f < _tappingTimer[i] && _tappingTimer[i] < 1f)
{
//User has been tapping for less than 1 sec. Get hand position and call ShortTap
if (device.TryGetFeatureValue(CommonUsages.devicePosition, out Vector3 handPosition))
{
ShortTap(handPosition);
}
}
_tappingTimer[i] = 0;
}
else
{
_tappingTimer[i] += Time.deltaTime;
if (_tappingTimer[i] >= 2f)
{
//User has been air tapping for at least 2sec. Get hand position and call LongTap
if (device.TryGetFeatureValue(CommonUsages.devicePosition, out Vector3 handPosition))
{
LongTap();
}
_tappingTimer[i] = -float.MaxValue; // reset the timer, to avoid retriggering if user is still holding tap
}
}
}
}
}
Add & Configure SpatialAnchorManager
ASA SDK 提供稱為 SpatialAnchorManager
的簡單介面,以呼叫 ASA 服務。 讓我們將其新增為變數至我們的 AzureSpatialAnchorsScript.cs
首先新增匯入
using Microsoft.Azure.SpatialAnchors.Unity;
然後宣告變數
public class AzureSpatialAnchorsScript : MonoBehaviour
{
/// <summary>
/// Used to distinguish short taps and long taps
/// </summary>
private float[] _tappingTimer = { 0, 0 };
/// <summary>
/// Main interface to anything Spatial Anchors related
/// </summary>
private SpatialAnchorManager _spatialAnchorManager = null;
在方法中 Start()
,將變數指派給我們在上一個步驟中新增的元件
// Start is called before the first frame update
void Start()
{
_spatialAnchorManager = GetComponent<SpatialAnchorManager>();
}
若要接收偵錯和錯誤記錄檔,我們需要訂閱不同的回呼
// Start is called before the first frame update
void Start()
{
_spatialAnchorManager = GetComponent<SpatialAnchorManager>();
_spatialAnchorManager.LogDebug += (sender, args) => Debug.Log($"ASA - Debug: {args.Message}");
_spatialAnchorManager.Error += (sender, args) => Debug.LogError($"ASA - Error: {args.ErrorMessage}");
}
注意
若要檢視記錄,請確定從 Unity 建置項目並開啟 Visual Studio 解決方案 .sln
之後,請選取 [偵錯] -- [使用偵錯執行],> 並在應用程式執行時讓 HoloLens 連線到您的電腦。
啟動工作階段
若要建立並尋找錨點,我們必須先啟動會話。 StartSessionAsync()
呼叫 時,SpatialAnchorManager
會視需要建立會話,然後加以啟動。 讓我們將此新增至 我們的 ShortTap()
方法。
/// <summary>
/// Called when a user is air tapping for a short time
/// </summary>
/// <param name="handPosition">Location where tap was registered</param>
private async void ShortTap(Vector3 handPosition)
{
await _spatialAnchorManager.StartSessionAsync();
}
建立錨點
現在,我們有一個執行中的會話,我們可以建立錨點。 在此應用程式中,我們想要追蹤建立的錨點 GameObjects
和建立的錨點標識碼(錨點標識符)。 讓我們在程式代碼中新增兩個清單。
using Microsoft.Azure.SpatialAnchors.Unity;
using System;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.XR;
/// <summary>
/// Main interface to anything Spatial Anchors related
/// </summary>
private SpatialAnchorManager _spatialAnchorManager = null;
/// <summary>
/// Used to keep track of all GameObjects that represent a found or created anchor
/// </summary>
private List<GameObject> _foundOrCreatedAnchorGameObjects = new List<GameObject>();
/// <summary>
/// Used to keep track of all the created Anchor IDs
/// </summary>
private List<String> _createdAnchorIDs = new List<String>();
讓我們建立方法CreateAnchor
,以在其 參數所定義的位置建立錨點。
using System.Threading.Tasks;
/// <summary>
/// Creates an Azure Spatial Anchor at the given position rotated towards the user
/// </summary>
/// <param name="position">Position where Azure Spatial Anchor will be created</param>
/// <returns>Async Task</returns>
private async Task CreateAnchor(Vector3 position)
{
//Create Anchor GameObject. We will use ASA to save the position and the rotation of this GameObject.
}
由於空間錨點不僅有位置,而且有旋轉,因此讓我們將旋轉設定為一律以建立 HoloLens 的方向方向。
/// <summary>
/// Creates an Azure Spatial Anchor at the given position rotated towards the user
/// </summary>
/// <param name="position">Position where Azure Spatial Anchor will be created</param>
/// <returns>Async Task</returns>
private async Task CreateAnchor(Vector3 position)
{
//Create Anchor GameObject. We will use ASA to save the position and the rotation of this GameObject.
if (!InputDevices.GetDeviceAtXRNode(XRNode.Head).TryGetFeatureValue(CommonUsages.devicePosition, out Vector3 headPosition))
{
headPosition = Vector3.zero;
}
Quaternion orientationTowardsHead = Quaternion.LookRotation(position - headPosition, Vector3.up);
}
既然我們有 所需錨點的位置 和 旋轉 ,讓我們建立可見 GameObject
的 。 請注意,Spatial Anchors 不需要用戶能夠看見錨點 GameObject
,因為 Spatial Anchors 的主要用途是提供一般且持續性的參考框架。 為了進行本教學課程,我們將錨點可視化為 Cube。 每個錨點都會初始化為 白色 Cube,一旦建立程式成功,就會變成 綠色 Cube。
/// <summary>
/// Creates an Azure Spatial Anchor at the given position rotated towards the user
/// </summary>
/// <param name="position">Position where Azure Spatial Anchor will be created</param>
/// <returns>Async Task</returns>
private async Task CreateAnchor(Vector3 position)
{
//Create Anchor GameObject. We will use ASA to save the position and the rotation of this GameObject.
if (!InputDevices.GetDeviceAtXRNode(XRNode.Head).TryGetFeatureValue(CommonUsages.devicePosition, out Vector3 headPosition))
{
headPosition = Vector3.zero;
}
Quaternion orientationTowardsHead = Quaternion.LookRotation(position - headPosition, Vector3.up);
GameObject anchorGameObject = GameObject.CreatePrimitive(PrimitiveType.Cube);
anchorGameObject.GetComponent<MeshRenderer>().material.shader = Shader.Find("Legacy Shaders/Diffuse");
anchorGameObject.transform.position = position;
anchorGameObject.transform.rotation = orientationTowardsHead;
anchorGameObject.transform.localScale = Vector3.one * 0.1f;
}
注意
我們使用舊版著色器,因為它包含在預設 Unity 組建中。 只有在手動指定或直接屬於場景時,才會包含其他著色器,例如預設著色器。 如果未包含著色器,而且應用程式嘗試轉譯它,則會產生粉紅色材質。
現在讓我們新增和設定 Spatial Anchor 元件。 我們將錨點的到期日設定為從錨點建立到 3 天。 之後,系統會自動從雲端刪除它們。 請記得新增匯入
using Microsoft.Azure.SpatialAnchors;
/// <summary>
/// Creates an Azure Spatial Anchor at the given position rotated towards the user
/// </summary>
/// <param name="position">Position where Azure Spatial Anchor will be created</param>
/// <returns>Async Task</returns>
private async Task CreateAnchor(Vector3 position)
{
//Create Anchor GameObject. We will use ASA to save the position and the rotation of this GameObject.
if (!InputDevices.GetDeviceAtXRNode(XRNode.Head).TryGetFeatureValue(CommonUsages.devicePosition, out Vector3 headPosition))
{
headPosition = Vector3.zero;
}
Quaternion orientationTowardsHead = Quaternion.LookRotation(position - headPosition, Vector3.up);
GameObject anchorGameObject = GameObject.CreatePrimitive(PrimitiveType.Cube);
anchorGameObject.GetComponent<MeshRenderer>().material.shader = Shader.Find("Legacy Shaders/Diffuse");
anchorGameObject.transform.position = position;
anchorGameObject.transform.rotation = orientationTowardsHead;
anchorGameObject.transform.localScale = Vector3.one * 0.1f;
//Add and configure ASA components
CloudNativeAnchor cloudNativeAnchor = anchorGameObject.AddComponent<CloudNativeAnchor>();
await cloudNativeAnchor.NativeToCloud();
CloudSpatialAnchor cloudSpatialAnchor = cloudNativeAnchor.CloudAnchor;
cloudSpatialAnchor.Expiration = DateTimeOffset.Now.AddDays(3);
}
若要儲存錨點,用戶必須收集環境數據。
/// <summary>
/// Creates an Azure Spatial Anchor at the given position rotated towards the user
/// </summary>
/// <param name="position">Position where Azure Spatial Anchor will be created</param>
/// <returns>Async Task</returns>
private async Task CreateAnchor(Vector3 position)
{
//Create Anchor GameObject. We will use ASA to save the position and the rotation of this GameObject.
if (!InputDevices.GetDeviceAtXRNode(XRNode.Head).TryGetFeatureValue(CommonUsages.devicePosition, out Vector3 headPosition))
{
headPosition = Vector3.zero;
}
Quaternion orientationTowardsHead = Quaternion.LookRotation(position - headPosition, Vector3.up);
GameObject anchorGameObject = GameObject.CreatePrimitive(PrimitiveType.Cube);
anchorGameObject.GetComponent<MeshRenderer>().material.shader = Shader.Find("Legacy Shaders/Diffuse");
anchorGameObject.transform.position = position;
anchorGameObject.transform.rotation = orientationTowardsHead;
anchorGameObject.transform.localScale = Vector3.one * 0.1f;
//Add and configure ASA components
CloudNativeAnchor cloudNativeAnchor = anchorGameObject.AddComponent<CloudNativeAnchor>();
await cloudNativeAnchor.NativeToCloud();
CloudSpatialAnchor cloudSpatialAnchor = cloudNativeAnchor.CloudAnchor;
cloudSpatialAnchor.Expiration = DateTimeOffset.Now.AddDays(3);
//Collect Environment Data
while (!_spatialAnchorManager.IsReadyForCreate)
{
float createProgress = _spatialAnchorManager.SessionStatus.RecommendedForCreateProgress;
Debug.Log($"ASA - Move your device to capture more environment data: {createProgress:0%}");
}
}
注意
HoloLens 可能會重複使用錨點周圍的已擷取環境數據,導致 IsReadyForCreate
第一次呼叫時已為 true。
現在已備妥雲端空間錨點,我們可以在這裡嘗試實際儲存。
/// <summary>
/// Creates an Azure Spatial Anchor at the given position rotated towards the user
/// </summary>
/// <param name="position">Position where Azure Spatial Anchor will be created</param>
/// <returns>Async Task</returns>
private async Task CreateAnchor(Vector3 position)
{
//Create Anchor GameObject. We will use ASA to save the position and the rotation of this GameObject.
if (!InputDevices.GetDeviceAtXRNode(XRNode.Head).TryGetFeatureValue(CommonUsages.devicePosition, out Vector3 headPosition))
{
headPosition = Vector3.zero;
}
Quaternion orientationTowardsHead = Quaternion.LookRotation(position - headPosition, Vector3.up);
GameObject anchorGameObject = GameObject.CreatePrimitive(PrimitiveType.Cube);
anchorGameObject.GetComponent<MeshRenderer>().material.shader = Shader.Find("Legacy Shaders/Diffuse");
anchorGameObject.transform.position = position;
anchorGameObject.transform.rotation = orientationTowardsHead;
anchorGameObject.transform.localScale = Vector3.one * 0.1f;
//Add and configure ASA components
CloudNativeAnchor cloudNativeAnchor = anchorGameObject.AddComponent<CloudNativeAnchor>();
await cloudNativeAnchor.NativeToCloud();
CloudSpatialAnchor cloudSpatialAnchor = cloudNativeAnchor.CloudAnchor;
cloudSpatialAnchor.Expiration = DateTimeOffset.Now.AddDays(3);
//Collect Environment Data
while (!_spatialAnchorManager.IsReadyForCreate)
{
float createProgress = _spatialAnchorManager.SessionStatus.RecommendedForCreateProgress;
Debug.Log($"ASA - Move your device to capture more environment data: {createProgress:0%}");
}
Debug.Log($"ASA - Saving cloud anchor... ");
try
{
// Now that the cloud spatial anchor has been prepared, we can try the actual save here.
await _spatialAnchorManager.CreateAnchorAsync(cloudSpatialAnchor);
bool saveSucceeded = cloudSpatialAnchor != null;
if (!saveSucceeded)
{
Debug.LogError("ASA - Failed to save, but no exception was thrown.");
return;
}
Debug.Log($"ASA - Saved cloud anchor with ID: {cloudSpatialAnchor.Identifier}");
_foundOrCreatedAnchorGameObjects.Add(anchorGameObject);
_createdAnchorIDs.Add(cloudSpatialAnchor.Identifier);
anchorGameObject.GetComponent<MeshRenderer>().material.color = Color.green;
}
catch (Exception exception)
{
Debug.Log("ASA - Failed to save anchor: " + exception.ToString());
Debug.LogException(exception);
}
}
最後,讓我們將函式呼叫新增至我們的 ShortTap
方法
/// <summary>
/// Called when a user is air tapping for a short time
/// </summary>
/// <param name="handPosition">Location where tap was registered</param>
private async void ShortTap(Vector3 handPosition)
{
await _spatialAnchorManager.StartSessionAsync();
await CreateAnchor(handPosition);
}
我們的應用程式現在可以建立多個錨點。 只要裝置知道錨點標識符,且可存取 Azure 上的相同 Spatial Anchors 資源,任何裝置都可以找到已建立的錨點(如果尚未過期)。
停止會話和終結 GameObjects
為了模擬第二個尋找所有錨點的裝置,我們現在會停止會話,並移除所有錨點 GameObjects(我們將保留錨點標識符)。 之後,我們將啟動新的會話,並使用預存的錨點標識碼來查詢錨點。
SpatialAnchorManager
只要呼叫其 DestroySession()
方法,即可處理會話停止。 讓我們將此新增至我們的 LongTap()
方法
/// <summary>
/// Called when a user is air tapping for a long time (>=2 sec)
/// </summary>
private async void LongTap()
{
_spatialAnchorManager.DestroySession();
}
讓我們建立方法來移除所有錨點 GameObjects
/// <summary>
/// Destroys all Anchor GameObjects
/// </summary>
private void RemoveAllAnchorGameObjects()
{
foreach (var anchorGameObject in _foundOrCreatedAnchorGameObjects)
{
Destroy(anchorGameObject);
}
_foundOrCreatedAnchorGameObjects.Clear();
}
並在 中終結會話之後呼叫它 LongTap()
/// <summary>
/// Called when a user is air tapping for a long time (>=2 sec)
/// </summary>
private async void LongTap()
{
// Stop Session and remove all GameObjects. This does not delete the Anchors in the cloud
_spatialAnchorManager.DestroySession();
RemoveAllAnchorGameObjects();
Debug.Log("ASA - Stopped Session and removed all Anchor Objects");
}
尋找錨點
我們現在會嘗試再次尋找錨點,並找出我們在其中建立錨點的正確位置和旋轉。 若要這樣做,我們需要啟動會話,並建立 Watcher
,以尋找符合指定準則的錨點。 作為準則,我們將提供我們先前建立之錨點的標識碼。 讓我們建立方法 LocateAnchor()
,並使用 SpatialAnchorManager
來建立 Watcher
。 如需使用錨點標識符以外的尋找策略,請參閱 錨點尋找策略
/// <summary>
/// Looking for anchors with ID in _createdAnchorIDs
/// </summary>
private void LocateAnchor()
{
if (_createdAnchorIDs.Count > 0)
{
//Create watcher to look for all stored anchor IDs
Debug.Log($"ASA - Creating watcher to look for {_createdAnchorIDs.Count} spatial anchors");
AnchorLocateCriteria anchorLocateCriteria = new AnchorLocateCriteria();
anchorLocateCriteria.Identifiers = _createdAnchorIDs.ToArray();
_spatialAnchorManager.Session.CreateWatcher(anchorLocateCriteria);
Debug.Log($"ASA - Watcher created!");
}
}
當監看員啟動時,它會在找到符合指定準則的錨點時引發回呼。 讓我們先建立名為 SpatialAnchorManager_AnchorLocated()
的錨點定位方法,我們將設定為在監看員找到錨點時呼叫。 此方法會建立視覺效果 GameObject
,並將原生錨點元件附加至該元件。 原生錨點元件會確定 已設定正確的位置和旋轉 GameObject
。
類似於建立程式,錨點會附加至 GameObject。 此 GameObject 不需要在您的場景中顯示,空間錨點才能運作。 為了本教學課程的目的,我們會在找到錨點之後,將每個錨點可視化為 藍色 Cube。 如果您只使用錨點來建立共用座標系統,就不需要將建立的 GameObject 可視化。
/// <summary>
/// Callback when an anchor is located
/// </summary>
/// <param name="sender">Callback sender</param>
/// <param name="args">Callback AnchorLocatedEventArgs</param>
private void SpatialAnchorManager_AnchorLocated(object sender, AnchorLocatedEventArgs args)
{
Debug.Log($"ASA - Anchor recognized as a possible anchor {args.Identifier} {args.Status}");
if (args.Status == LocateAnchorStatus.Located)
{
//Creating and adjusting GameObjects have to run on the main thread. We are using the UnityDispatcher to make sure this happens.
UnityDispatcher.InvokeOnAppThread(() =>
{
// Read out Cloud Anchor values
CloudSpatialAnchor cloudSpatialAnchor = args.Anchor;
//Create GameObject
GameObject anchorGameObject = GameObject.CreatePrimitive(PrimitiveType.Cube);
anchorGameObject.transform.localScale = Vector3.one * 0.1f;
anchorGameObject.GetComponent<MeshRenderer>().material.shader = Shader.Find("Legacy Shaders/Diffuse");
anchorGameObject.GetComponent<MeshRenderer>().material.color = Color.blue;
// Link to Cloud Anchor
anchorGameObject.AddComponent<CloudNativeAnchor>().CloudToNative(cloudSpatialAnchor);
_foundOrCreatedAnchorGameObjects.Add(anchorGameObject);
});
}
}
現在,讓我們訂閱 AnchorLocated 回呼 SpatialAnchorManager
,以確保一旦監看員找到錨點,就會呼叫我們的 SpatialAnchorManager_AnchorLocated()
方法。
// Start is called before the first frame update
void Start()
{
_spatialAnchorManager = GetComponent<SpatialAnchorManager>();
_spatialAnchorManager.LogDebug += (sender, args) => Debug.Log($"ASA - Debug: {args.Message}");
_spatialAnchorManager.Error += (sender, args) => Debug.LogError($"ASA - Error: {args.ErrorMessage}");
_spatialAnchorManager.AnchorLocated += SpatialAnchorManager_AnchorLocated;
}
最後,讓我們展開 方法 LongTap()
,以包含尋找錨點。 我們將使用 IsSessionStarted
布爾值來決定是否要尋找所有錨點或終結所有錨點, 如應用程式概觀中所述
/// <summary>
/// Called when a user is air tapping for a long time (>=2 sec)
/// </summary>
private async void LongTap()
{
if (_spatialAnchorManager.IsSessionStarted)
{
// Stop Session and remove all GameObjects. This does not delete the Anchors in the cloud
_spatialAnchorManager.DestroySession();
RemoveAllAnchorGameObjects();
Debug.Log("ASA - Stopped Session and removed all Anchor Objects");
}
else
{
//Start session and search for all Anchors previously created
await _spatialAnchorManager.StartSessionAsync();
LocateAnchor();
}
}
試試看 #2
您的應用程式現在支援建立錨點並尋找它們。 遵循使用 Visual Studio 部署和偵錯,在 Unity 中建置您的應用程式,並從 Visual Studio 進行部署。
確定 HoloLens 裝置已連線至網際網路。 一旦應用程式啟動,且 使用 Unity 訊息建立的 會消失,請簡短點選您的環境。 白色立方體應該會顯示所要建立錨點的位置和旋轉。 系統會自動呼叫錨點建立程式。 當您慢慢地環顧周圍的環境時,您正在擷取環境數據。 收集到足夠的環境數據之後,應用程式會嘗試在指定的位置建立錨點。 錨點建立程式完成後,Cube 會變成 綠色。 檢查 Visual Studio 中的偵錯記錄,以查看所有專案是否如預期般運作。
只要點選即可從場景中移除所有 GameObjects
專案,並停止空間錨點會話。
清除場景之後,您可以再次點選,這會開始會話,並尋找您先前建立的錨點。 找到它們之後,會以 藍色 立方體在錨定的位置和旋轉方式可視化。 只要這些錨點具有正確的錨點標識符且可存取空間錨點資源,任何支援的裝置都可以找到這些錨點(只要它們未過期)。
刪除錨點
現在,我們的應用程式可以建立並尋找錨點。 GameObjects
刪除 時,不會刪除雲端中的錨點。 如果您點選現有的錨點,讓我們新增功能以在雲端中刪除此功能。
讓我們新增接收 GameObject
的方法DeleteAnchor
。 接著,我們將與 物件的CloudNativeAnchor
元件一SpatialAnchorManager
起使用,要求刪除雲端中的錨點。
/// <summary>
/// Deleting Cloud Anchor attached to the given GameObject and deleting the GameObject
/// </summary>
/// <param name="anchorGameObject">Anchor GameObject that is to be deleted</param>
private async void DeleteAnchor(GameObject anchorGameObject)
{
CloudNativeAnchor cloudNativeAnchor = anchorGameObject.GetComponent<CloudNativeAnchor>();
CloudSpatialAnchor cloudSpatialAnchor = cloudNativeAnchor.CloudAnchor;
Debug.Log($"ASA - Deleting cloud anchor: {cloudSpatialAnchor.Identifier}");
//Request Deletion of Cloud Anchor
await _spatialAnchorManager.DeleteAnchorAsync(cloudSpatialAnchor);
//Remove local references
_createdAnchorIDs.Remove(cloudSpatialAnchor.Identifier);
_foundOrCreatedAnchorGameObjects.Remove(anchorGameObject);
Destroy(anchorGameObject);
Debug.Log($"ASA - Cloud anchor deleted!");
}
若要從 ShortTap
呼叫這個方法,我們必須能夠判斷點選是否靠近現有的可見錨點。 讓我們建立一個協助程式方法,以處理該方法
using System.Linq;
/// <summary>
/// Returns true if an Anchor GameObject is within 15cm of the received reference position
/// </summary>
/// <param name="position">Reference position</param>
/// <param name="anchorGameObject">Anchor GameObject within 15cm of received position. Not necessarily the nearest to this position. If no AnchorObject is within 15cm, this value will be null</param>
/// <returns>True if a Anchor GameObject is within 15cm</returns>
private bool IsAnchorNearby(Vector3 position, out GameObject anchorGameObject)
{
anchorGameObject = null;
if (_foundOrCreatedAnchorGameObjects.Count <= 0)
{
return false;
}
//Iterate over existing anchor gameobjects to find the nearest
var (distance, closestObject) = _foundOrCreatedAnchorGameObjects.Aggregate(
new Tuple<float, GameObject>(Mathf.Infinity, null),
(minPair, gameobject) =>
{
Vector3 gameObjectPosition = gameobject.transform.position;
float distance = (position - gameObjectPosition).magnitude;
return distance < minPair.Item1 ? new Tuple<float, GameObject>(distance, gameobject) : minPair;
});
if (distance <= 0.15f)
{
//Found an anchor within 15cm
anchorGameObject = closestObject;
return true;
}
else
{
return false;
}
}
我們現在可以擴充 方法 ShortTap
以包含 DeleteAnchor
呼叫
/// <summary>
/// Called when a user is air tapping for a short time
/// </summary>
/// <param name="handPosition">Location where tap was registered</param>
private async void ShortTap(Vector3 handPosition)
{
await _spatialAnchorManager.StartSessionAsync();
if (!IsAnchorNearby(handPosition, out GameObject anchorGameObject))
{
//No Anchor Nearby, start session and create an anchor
await CreateAnchor(handPosition);
}
else
{
//Delete nearby Anchor
DeleteAnchor(anchorGameObject);
}
}
試試看 #3
遵循使用 Visual Studio 部署和偵錯,在 Unity 中建置您的應用程式,並從 Visual Studio 進行部署。
請注意,手部點選手勢的位置是 此應用程式中手 部的中心,而不是手指尖。
當您點選錨點時,系統會將要求傳送至空間錨點服務,以從帳戶中移除此錨點。 停止會話(長時間點選),然後再次啟動會話(長時間點選),以搜尋所有錨點。 已刪除的錨點將不再找到。
合併所有元素
以下是所有不同元素都放在一起之後,完整 AzureSpatialAnchorsScript
類別檔案的外觀。 您可以使用它做為參考來與您自己的檔案進行比較,並找出是否有任何差異。
注意
您會發現文稿已包含 [RequireComponent(typeof(SpatialAnchorManager))]
我們。 如此一來,Unity 將確保我們附加 AzureSpatialAnchorsScript
至的 GameObject 也已 SpatialAnchorManager
附加至它。
using Microsoft.Azure.SpatialAnchors;
using Microsoft.Azure.SpatialAnchors.Unity;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using UnityEngine;
using UnityEngine.XR;
[RequireComponent(typeof(SpatialAnchorManager))]
public class AzureSpatialAnchorsScript : MonoBehaviour
{
/// <summary>
/// Used to distinguish short taps and long taps
/// </summary>
private float[] _tappingTimer = { 0, 0 };
/// <summary>
/// Main interface to anything Spatial Anchors related
/// </summary>
private SpatialAnchorManager _spatialAnchorManager = null;
/// <summary>
/// Used to keep track of all GameObjects that represent a found or created anchor
/// </summary>
private List<GameObject> _foundOrCreatedAnchorGameObjects = new List<GameObject>();
/// <summary>
/// Used to keep track of all the created Anchor IDs
/// </summary>
private List<String> _createdAnchorIDs = new List<String>();
// <Start>
// Start is called before the first frame update
void Start()
{
_spatialAnchorManager = GetComponent<SpatialAnchorManager>();
_spatialAnchorManager.LogDebug += (sender, args) => Debug.Log($"ASA - Debug: {args.Message}");
_spatialAnchorManager.Error += (sender, args) => Debug.LogError($"ASA - Error: {args.ErrorMessage}");
_spatialAnchorManager.AnchorLocated += SpatialAnchorManager_AnchorLocated;
}
// </Start>
// <Update>
// Update is called once per frame
void Update()
{
//Check for any air taps from either hand
for (int i = 0; i < 2; i++)
{
InputDevice device = InputDevices.GetDeviceAtXRNode((i == 0) ? XRNode.RightHand : XRNode.LeftHand);
if (device.TryGetFeatureValue(CommonUsages.primaryButton, out bool isTapping))
{
if (!isTapping)
{
//Stopped Tapping or wasn't tapping
if (0f < _tappingTimer[i] && _tappingTimer[i] < 1f)
{
//User has been tapping for less than 1 sec. Get hand position and call ShortTap
if (device.TryGetFeatureValue(CommonUsages.devicePosition, out Vector3 handPosition))
{
ShortTap(handPosition);
}
}
_tappingTimer[i] = 0;
}
else
{
_tappingTimer[i] += Time.deltaTime;
if (_tappingTimer[i] >= 2f)
{
//User has been air tapping for at least 2sec. Get hand position and call LongTap
if (device.TryGetFeatureValue(CommonUsages.devicePosition, out Vector3 handPosition))
{
LongTap();
}
_tappingTimer[i] = -float.MaxValue; // reset the timer, to avoid retriggering if user is still holding tap
}
}
}
}
}
// </Update>
// <ShortTap>
/// <summary>
/// Called when a user is air tapping for a short time
/// </summary>
/// <param name="handPosition">Location where tap was registered</param>
private async void ShortTap(Vector3 handPosition)
{
await _spatialAnchorManager.StartSessionAsync();
if (!IsAnchorNearby(handPosition, out GameObject anchorGameObject))
{
//No Anchor Nearby, start session and create an anchor
await CreateAnchor(handPosition);
}
else
{
//Delete nearby Anchor
DeleteAnchor(anchorGameObject);
}
}
// </ShortTap>
// <LongTap>
/// <summary>
/// Called when a user is air tapping for a long time (>=2 sec)
/// </summary>
private async void LongTap()
{
if (_spatialAnchorManager.IsSessionStarted)
{
// Stop Session and remove all GameObjects. This does not delete the Anchors in the cloud
_spatialAnchorManager.DestroySession();
RemoveAllAnchorGameObjects();
Debug.Log("ASA - Stopped Session and removed all Anchor Objects");
}
else
{
//Start session and search for all Anchors previously created
await _spatialAnchorManager.StartSessionAsync();
LocateAnchor();
}
}
// </LongTap>
// <RemoveAllAnchorGameObjects>
/// <summary>
/// Destroys all Anchor GameObjects
/// </summary>
private void RemoveAllAnchorGameObjects()
{
foreach (var anchorGameObject in _foundOrCreatedAnchorGameObjects)
{
Destroy(anchorGameObject);
}
_foundOrCreatedAnchorGameObjects.Clear();
}
// </RemoveAllAnchorGameObjects>
// <IsAnchorNearby>
/// <summary>
/// Returns true if an Anchor GameObject is within 15cm of the received reference position
/// </summary>
/// <param name="position">Reference position</param>
/// <param name="anchorGameObject">Anchor GameObject within 15cm of received position. Not necessarily the nearest to this position. If no AnchorObject is within 15cm, this value will be null</param>
/// <returns>True if a Anchor GameObject is within 15cm</returns>
private bool IsAnchorNearby(Vector3 position, out GameObject anchorGameObject)
{
anchorGameObject = null;
if (_foundOrCreatedAnchorGameObjects.Count <= 0)
{
return false;
}
//Iterate over existing anchor gameobjects to find the nearest
var (distance, closestObject) = _foundOrCreatedAnchorGameObjects.Aggregate(
new Tuple<float, GameObject>(Mathf.Infinity, null),
(minPair, gameobject) =>
{
Vector3 gameObjectPosition = gameobject.transform.position;
float distance = (position - gameObjectPosition).magnitude;
return distance < minPair.Item1 ? new Tuple<float, GameObject>(distance, gameobject) : minPair;
});
if (distance <= 0.15f)
{
//Found an anchor within 15cm
anchorGameObject = closestObject;
return true;
}
else
{
return false;
}
}
// </IsAnchorNearby>
// <CreateAnchor>
/// <summary>
/// Creates an Azure Spatial Anchor at the given position rotated towards the user
/// </summary>
/// <param name="position">Position where Azure Spatial Anchor will be created</param>
/// <returns>Async Task</returns>
private async Task CreateAnchor(Vector3 position)
{
//Create Anchor GameObject. We will use ASA to save the position and the rotation of this GameObject.
if (!InputDevices.GetDeviceAtXRNode(XRNode.Head).TryGetFeatureValue(CommonUsages.devicePosition, out Vector3 headPosition))
{
headPosition = Vector3.zero;
}
Quaternion orientationTowardsHead = Quaternion.LookRotation(position - headPosition, Vector3.up);
GameObject anchorGameObject = GameObject.CreatePrimitive(PrimitiveType.Cube);
anchorGameObject.GetComponent<MeshRenderer>().material.shader = Shader.Find("Legacy Shaders/Diffuse");
anchorGameObject.transform.position = position;
anchorGameObject.transform.rotation = orientationTowardsHead;
anchorGameObject.transform.localScale = Vector3.one * 0.1f;
//Add and configure ASA components
CloudNativeAnchor cloudNativeAnchor = anchorGameObject.AddComponent<CloudNativeAnchor>();
await cloudNativeAnchor.NativeToCloud();
CloudSpatialAnchor cloudSpatialAnchor = cloudNativeAnchor.CloudAnchor;
cloudSpatialAnchor.Expiration = DateTimeOffset.Now.AddDays(3);
//Collect Environment Data
while (!_spatialAnchorManager.IsReadyForCreate)
{
float createProgress = _spatialAnchorManager.SessionStatus.RecommendedForCreateProgress;
Debug.Log($"ASA - Move your device to capture more environment data: {createProgress:0%}");
}
Debug.Log($"ASA - Saving cloud anchor... ");
try
{
// Now that the cloud spatial anchor has been prepared, we can try the actual save here.
await _spatialAnchorManager.CreateAnchorAsync(cloudSpatialAnchor);
bool saveSucceeded = cloudSpatialAnchor != null;
if (!saveSucceeded)
{
Debug.LogError("ASA - Failed to save, but no exception was thrown.");
return;
}
Debug.Log($"ASA - Saved cloud anchor with ID: {cloudSpatialAnchor.Identifier}");
_foundOrCreatedAnchorGameObjects.Add(anchorGameObject);
_createdAnchorIDs.Add(cloudSpatialAnchor.Identifier);
anchorGameObject.GetComponent<MeshRenderer>().material.color = Color.green;
}
catch (Exception exception)
{
Debug.Log("ASA - Failed to save anchor: " + exception.ToString());
Debug.LogException(exception);
}
}
// </CreateAnchor>
// <LocateAnchor>
/// <summary>
/// Looking for anchors with ID in _createdAnchorIDs
/// </summary>
private void LocateAnchor()
{
if (_createdAnchorIDs.Count > 0)
{
//Create watcher to look for all stored anchor IDs
Debug.Log($"ASA - Creating watcher to look for {_createdAnchorIDs.Count} spatial anchors");
AnchorLocateCriteria anchorLocateCriteria = new AnchorLocateCriteria();
anchorLocateCriteria.Identifiers = _createdAnchorIDs.ToArray();
_spatialAnchorManager.Session.CreateWatcher(anchorLocateCriteria);
Debug.Log($"ASA - Watcher created!");
}
}
// </LocateAnchor>
// <SpatialAnchorManagerAnchorLocated>
/// <summary>
/// Callback when an anchor is located
/// </summary>
/// <param name="sender">Callback sender</param>
/// <param name="args">Callback AnchorLocatedEventArgs</param>
private void SpatialAnchorManager_AnchorLocated(object sender, AnchorLocatedEventArgs args)
{
Debug.Log($"ASA - Anchor recognized as a possible anchor {args.Identifier} {args.Status}");
if (args.Status == LocateAnchorStatus.Located)
{
//Creating and adjusting GameObjects have to run on the main thread. We are using the UnityDispatcher to make sure this happens.
UnityDispatcher.InvokeOnAppThread(() =>
{
// Read out Cloud Anchor values
CloudSpatialAnchor cloudSpatialAnchor = args.Anchor;
//Create GameObject
GameObject anchorGameObject = GameObject.CreatePrimitive(PrimitiveType.Cube);
anchorGameObject.transform.localScale = Vector3.one * 0.1f;
anchorGameObject.GetComponent<MeshRenderer>().material.shader = Shader.Find("Legacy Shaders/Diffuse");
anchorGameObject.GetComponent<MeshRenderer>().material.color = Color.blue;
// Link to Cloud Anchor
anchorGameObject.AddComponent<CloudNativeAnchor>().CloudToNative(cloudSpatialAnchor);
_foundOrCreatedAnchorGameObjects.Add(anchorGameObject);
});
}
}
// </SpatialAnchorManagerAnchorLocated>
// <DeleteAnchor>
/// <summary>
/// Deleting Cloud Anchor attached to the given GameObject and deleting the GameObject
/// </summary>
/// <param name="anchorGameObject">Anchor GameObject that is to be deleted</param>
private async void DeleteAnchor(GameObject anchorGameObject)
{
CloudNativeAnchor cloudNativeAnchor = anchorGameObject.GetComponent<CloudNativeAnchor>();
CloudSpatialAnchor cloudSpatialAnchor = cloudNativeAnchor.CloudAnchor;
Debug.Log($"ASA - Deleting cloud anchor: {cloudSpatialAnchor.Identifier}");
//Request Deletion of Cloud Anchor
await _spatialAnchorManager.DeleteAnchorAsync(cloudSpatialAnchor);
//Remove local references
_createdAnchorIDs.Remove(cloudSpatialAnchor.Identifier);
_foundOrCreatedAnchorGameObjects.Remove(anchorGameObject);
Destroy(anchorGameObject);
Debug.Log($"ASA - Cloud anchor deleted!");
}
// </DeleteAnchor>
}
下一步
在本教學課程中,您已瞭解如何使用 Unity 實作 HoloLens 的基本 Spatial Anchors 應用程式。 若要深入瞭解如何在新的 Android 應用程式中使用 Azure Spatial Anchors,請繼續進行下一個教學課程。