Kurz: Podrobné pokyny k vytvoření nové aplikace HoloLens Unity pomocí Azure Spatial Anchors
V tomto kurzu se dozvíte, jak vytvořit novou aplikaci HoloLens Unity pomocí Azure Spatial Anchors.
Požadavky
Abyste mohli absolvovat tento kurz, ujistěte se, že máte následující:
- POČÍTAČ – Počítač s Windows
- Visual Studio Visual Studio - 2019 nainstalované se sadou funkcí pro vývoj Univerzální platforma Windows a sadou Windows 10 SDK (10.0.18362.0 nebo novější). Rozšíření sady Visual Studio C++/WinRT (VSIX) pro Visual Studio by se mělo nainstalovat z webu Visual Studio Marketplace.
- HoloLens – Zařízení HoloLens s povoleným režimem vývojáře Tento článek vyžaduje zařízení HoloLens s aktualizací Windows 10 z května 2020. Pokud chcete aktualizovat na nejnovější verzi v HoloLensu, otevřete aplikaci Nastavení, přejděte na Aktualizovat a zabezpečení a pak vyberte tlačítko Vyhledat aktualizace.
- Unity Unity - 2020.3.25 s moduly Univerzální platforma Windows podpora sestavení a podpora sestavení Windows (IL2CPP)
Vytvoření a nastavení projektu Unity
Create New Project (Azure Functions: Vytvořit nový projekt).
- V Centru Unity vyberte Nový projekt.
- Výběr 3D
- Zadejte název projektu a zadejte umístění pro uložení.
- Vyberte Vytvořit projekt a počkejte, až Unity vytvoří váš projekt.
Změna platformy sestavení
- V editoru Unity vyberte Nastavení sestavení souboru>.
- Vyberte Univerzální platforma Windows a pak přepnout platformu. Počkejte, až Unity dokončí zpracování všech souborů.
Import ASA a OpenXR
- Spuštění nástroje pro funkce hybridní reality
- Vyberte cestu k projektu – složku, která obsahuje složky, jako jsou prostředky, balíčky, projekt Nastavení atd. a vyberte Zjistit funkce.
- V části Azure Mixed Reality Services vyberte obě možnosti.
- Azure Spatial Anchors SDK Core
- Azure Spatial Anchors SDK pro Windows
- V části Podpora platformy vyberte
- Modul plug-in Mixed Reality OpenXR
Poznámka:
Ujistěte se, že jste aktualizovali katalog a pro každou z nich je vybraná nejnovější verze.
- Stiskněte tlačítko Získat funkce -->Import -->Approve -->Exit.
- Při refokusování okna Unity začne Unity importovat moduly.
- Pokud se zobrazí zpráva o použití nového vstupního systému, vyberte Ano , restartujte Unity a povolte back-endy.
Nastavení projektu
Teď nastavíme některá nastavení projektu Unity, která nám pomůžou cílit na sadu Windows Holographic SDK pro vývoj.
Změna Nastavení OpenXR
- >Vyberte souborový build Nastavení (může být stále otevřený z předchozího kroku).
- Vyberte Nastavení přehrávače...
- Výběr správy modulů plug-in XR
- Ujistěte se, že je vybraná karta Univerzální platforma Windows Nastavení, a zaškrtněte políčko vedle položky OpenXR a vedle skupiny funkcí Microsoft HoloLens.
- Pokud chcete zobrazit všechny problémy s OpenXR, vyberte žluté znaménko upozornění vedle OpenXR .
- Výběr možnosti Opravit vše
- Pokud chcete tento problém vyřešit , musíte přidat aspoň jeden profil interakce, vyberte Upravit a otevřete nastavení projektu OpenXR. Potom v části Profily interakce vyberte + symbol a vyberte Profil interakce rukou Microsoftu.
Nastavení ke změně kvality
- Výběr možnosti Upravit>projekt Nastavení> Quality
- Ve sloupci pod logem Univerzální platforma Windows vyberte šipku na výchozím řádku a vyberte Velmi nízká. Nastavení se správně použije, když je pole ve sloupci Univerzální platforma Windows a řádek Velmi nízký je zelený.
Nastavení možností
- Přejděte na Upravit>projekt Nastavení> Player (možná ho máte otevřený z předchozího kroku).
- Ujistěte se, že je vybraná karta Univerzální platforma Windows Nastavení.
- V části Publikování Nastavení Konfigurace povolte následující:
- InternetClient
- InternetClientServer
- PrivateNetworkClientServer
- SpatialPerception (možná už je povolené)
Nastavení hlavní kamery
- Na panelu hierarchie vyberte hlavní Kamera.
- V inspektoru nastavte jeho pozici transformace na 0,0,0.
- Najděte vlastnost Vymazat příznaky a změňte rozevírací seznam ze skyboxu na plnou barvu.
- Výběrem pole Pozadí otevřete výběr barvy.
- Nastavte R, G, B a A na hodnotu 0.
- V dolní části vyberte Přidat komponentu a přidejte do kamery komponentu ovladače Tracked Pose Driver .
Vyzkoušejte si to č. 1
Teď byste měli mít prázdnou scénu, která je připravená k nasazení do zařízení HoloLens. Pokud chcete otestovat, že všechno funguje, sestavte aplikaci v Unity a nasaďte ji ze sady Visual Studio. Postupujte podle pokynů v sadě Visual Studio, abyste to mohli nasadit a ladit . Měla by se zobrazit úvodní obrazovka Unity a pak jasné zobrazení.
Vytvoření prostředku Spatial Anchors
Přejděte na Azure Portal.
V levém podokně vyberte Vytvořit prostředek.
Pomocí vyhledávacího pole vyhledejte Spatial Anchors.
Vyberte Spatial Anchors a pak vyberte Vytvořit.
V podokně Účet prostorových ukotvení postupujte takto:
Zadejte jedinečný název prostředku pomocí běžných alfanumerických znaků.
Vyberte předplatné, ke kterému chcete prostředek připojit.
Výběrem možnosti Vytvořit novou vytvořte skupinu prostředků. Pojmenujte ji myResourceGroup a pak vyberte OK.
Skupina prostředků je logický kontejner, do kterého se nasazují a spravují prostředky Azure, jako jsou webové aplikace, databáze a účty úložiště. Později se například můžete rozhodnout odstranit celou skupinu prostředků v jednom jednoduchém kroku.
Vyberte umístění (oblast), do kterého chcete prostředek umístit.
Výběrem možnosti Vytvořit zahájíte vytváření prostředku.
Po vytvoření prostředku se na webu Azure Portal zobrazí, že je vaše nasazení dokončené.
Vyberte Přejít k prostředku. Teď můžete zobrazit vlastnosti prostředku.
Zkopírujte hodnotu ID účtu prostředku do textového editoru pro pozdější použití.
Zkopírujte také hodnotu domény účtu prostředku do textového editoru pro pozdější použití.
V části Nastavení vyberte Přístupový klíč. Zkopírujte hodnotu primárního klíče Account Key (Klíč účtu) do textového editoru pro pozdější použití.
Vytváření a přidávání skriptů
- V Unity v podokně Projekt vytvořte novou složku s názvem Skripty ve složce Assets .
- Ve složce klikněte pravým tlačítkem myši na skript ->Create ->C#. Pojmete ho AzureSpatialAnchorsScript
- Přejděte na GameObject ->Create Empty.
- Vyberte ho a v inspektoru ho přejmenujte z Objektu GameObject na AzureSpatialAnchors.
- Stále na
GameObject
- Nastavit jeho pozici na 0,0,0
- Vyberte Přidat komponentu a vyhledejte a přidejte AzureSpatialAnchorsScript.
- Znovu vyberte Přidat komponentu a vyhledejte a přidejte Správce ukotvení ar. Tím se automaticky přidá také zdroj relace rozšířené reality.
- Znovu vyberte Přidat komponentua vyhledejte a přidejte skript SpatialAnchorManager .
- V přidané komponentě SpatialAnchorManager vyplňte ID účtu, klíč účtu a doménu účtu, kterou jste zkopírovali v předchozím kroku z prostředku prostorových ukotvení na webu Azure Portal.
Přehled aplikace
Naše aplikace bude podporovat následující interakce:
Gesto | Akce |
---|---|
Klepněte kamkoli | Start/Continue Session + Create anchor at Hand Position |
Klepnutí na ukotvení | Odstranění GameObject a odstranění ukotvení v cloudové službě ASA |
Klepněte na + Podržte po dobu 2 sekund (+ relace je spuštěná) | Zastavte relaci a odeberte vše GameObjects . Zachování ukotvení v cloudové službě ASA |
Klepněte na +Podržte pro 2 sekundy (+ relace není spuštěná) | Spusťte relaci a vyhledejte všechny kotvy. |
Přidání rozpoznávání klepnutím
Pojďme do našeho skriptu přidat nějaký kód, abychom mohli rozpoznat gesto klepnutí uživatele.
- Otevřete
AzureSpatialAnchorsScript.cs
v sadě Visual Studio poklikáním na skript v podokně projektu Unity. - Přidejte do třídy následující pole.
public class AzureSpatialAnchorsScript : MonoBehaviour
{
/// <summary>
/// Used to distinguish short taps and long taps
/// </summary>
private float[] _tappingTimer = { 0, 0 };
- Pod metodu Update() přidejte následující dvě metody. Implementaci přidáme v pozdější fázi.
// 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()
{
}
- Přidejte následující import.
using UnityEngine.XR;
- Přidejte následující kód nad metodu
Update()
. To umožní aplikaci rozpoznat krátká a dlouhá gesta (2 sekundy) klepnutím na ruku.
// 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
}
}
}
}
}
Přidat a nakonfigurovat SpatialAnchorManager
Sada ASA SDK nabízí jednoduché rozhraní volané SpatialAnchorManager
k volání služby ASA. Pojďme ji přidat jako proměnnou do naší AzureSpatialAnchorsScript.cs
Nejprve přidejte import.
using Microsoft.Azure.SpatialAnchors.Unity;
Potom deklarujte proměnnou.
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()
V metodě přiřaďte proměnnou komponentě, která jsme přidali v předchozím kroku.
// Start is called before the first frame update
void Start()
{
_spatialAnchorManager = GetComponent<SpatialAnchorManager>();
}
Abychom mohli přijímat protokoly ladění a chyb, musíme se přihlásit k odběru různých zpětných volání.
// 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}");
}
Poznámka:
Pokud chcete zobrazit protokoly, ujistěte se, že po sestavení projektu z Unity a otevření řešení .sln
sady Visual Studio vyberte Ladit –> Spusťte s laděním a nechte HoloLens připojený k počítači, když je aplikace spuštěná.
Spustit relaci
Abychom mohli vytvořit a najít ukotvení, musíme nejprve zahájit relaci. Při volání StartSessionAsync()
SpatialAnchorManager
vytvoří relaci v případě potřeby a pak ji spustí. Pojďme to přidat do naší ShortTap()
metody.
/// <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();
}
Vytvořit kotvu
Teď, když máme spuštěnou relaci, můžeme vytvořit ukotvení. V této aplikaci bychom chtěli sledovat vytvořenou ukotvení a vytvořené identifikátory ukotvení GameObjects
(ID ukotvení). Pojďme do našeho kódu přidat dva seznamy.
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>();
Pojďme vytvořit metodu CreateAnchor
, která vytvoří ukotvení na pozici definované jeho parametrem.
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.
}
Vzhledem k tomu, že prostorové kotvy mají nejen pozici , ale také otočení, nastavíme rotaci tak, aby se vždy orientovaly na HoloLens při vytváření.
/// <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);
}
Teď, když máme pozici a otočení požadované kotvy, vytvoříme viditelné GameObject
. Všimněte si, že Spatial Anchors nevyžaduje, aby bylo ukotvení GameObject
viditelné koncovému uživateli, protože hlavním účelem Spatial Anchors je poskytnout společný a trvalý referenční rámec. Pro účely tohoto kurzu vizualizujeme ukotvení jako datové krychle. Každá kotva se inicializuje jako bílá krychle, která se po úspěšném vytvoření změní na zelenou datovou krychli.
/// <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;
}
Poznámka:
Používáme starší shader, protože je součástí výchozího sestavení Unity. Jiné shadery, jako je výchozí shader, se zahrnou jenom v případě, že jsou ručně zadané nebo jsou přímo součástí scény. Pokud shader není zahrnutý a aplikace se ji pokouší vykreslit, bude mít za následek růžový materiál.
Teď přidáme a nakonfigurujeme komponenty Spatial Anchor. Nastavujeme vypršení platnosti ukotvení na 3 dny od vytvoření ukotvení. Potom se automaticky odstraní z cloudu. Nezapomeňte přidat import.
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);
}
Aby uživatel uložil ukotvení, musí shromažďovat data prostředí.
/// <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%}");
}
}
Poznámka:
HoloLens může pravděpodobně znovu použít zachycená data prostředí obklopící ukotvení, což vede IsReadyForCreate
k tomu, že už při prvním zavolání bude true.
Teď, když je prostorová kotva cloudu připravená, můžeme vyzkoušet skutečnou úsporu.
/// <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);
}
}
Nakonec přidáme volání funkce do naší ShortTap
metody.
/// <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);
}
Naše aplikace teď může vytvořit více ukotvení. Všechna zařízení teď můžou vytvořené kotvy najít (pokud ještě nevypršela platnost), pokud znají ID ukotvení a mají přístup ke stejnému prostředku Spatial Anchors v Azure.
Zastavit relaci a zničit GameObjects
Abychom emulaci druhého zařízení našli všechna ukotvení, zastavíme relaci a odebereme všechny objekty GameObject (zachováme ID ukotvení). Potom spustíme novou relaci a dotazujeme se na ukotvení pomocí uložených ID ukotvení.
SpatialAnchorManager
může se postarat o zastavení relace jednoduše voláním jeho DestroySession()
metody. Pojďme to přidat do naší LongTap()
metody.
/// <summary>
/// Called when a user is air tapping for a long time (>=2 sec)
/// </summary>
private async void LongTap()
{
_spatialAnchorManager.DestroySession();
}
Pojďme vytvořit metodu pro odebrání všech ukotvení. GameObjects
/// <summary>
/// Destroys all Anchor GameObjects
/// </summary>
private void RemoveAllAnchorGameObjects()
{
foreach (var anchorGameObject in _foundOrCreatedAnchorGameObjects)
{
Destroy(anchorGameObject);
}
_foundOrCreatedAnchorGameObjects.Clear();
}
A zavolejte ji po zničení relace v 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");
}
Najít ukotvení
Nyní se pokusíme najít kotvy znovu se správnou polohou a otočením, ve které jsme je vytvořili. Abychom to mohli udělat, musíme zahájit relaci a vytvořit Watcher
kotvy, které odpovídají zadaným kritériím. Vzhledem k tomu, že kritéria ho budeme doplňovat, ID ukotvení, které jsme vytvořili dříve. Pojďme vytvořit metodu LocateAnchor()
a použít SpatialAnchorManager
k vytvoření Watcher
. Pro vyhledání jiných strategií než použití ID ukotvení naleznete v tématu Hledání strategie ukotvení.
/// <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!");
}
}
Jakmile sledovací proces spustí, aktivuje zpětné volání, jakmile najde kotvu, která odpovídá zadaným kritériím. Nejprve vytvoříme metodu umístěnou v ukotvení, SpatialAnchorManager_AnchorLocated()
kterou nakonfigurujeme tak, aby byla volána, když sledovací proces našel ukotvení. Tato metoda vytvoří vizuál GameObject
a připojí k ní nativní komponentu ukotvení. Nativní komponenta ukotvení zajistí, že je nastavena správná pozice a otočení GameObject
.
Podobně jako při vytváření je ukotvení připojené k objektu GameObject. Tento GameObject nemusí být ve vaší scéně viditelný, aby prostorové kotvy fungovaly. Pro účely tohoto kurzu vizualizujeme každou kotvu jako modrou datovou krychli, jakmile se nacházejí. Pokud k vytvoření sdíleného souřadnicového systému použijete ukotvení, není nutné vizualizovat vytvořený 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);
});
}
}
Teď se přihlásíme k odběru zpětného volání AnchorLocated, SpatialAnchorManager
abychom se ujistili, že naše SpatialAnchorManager_AnchorLocated()
metoda je volána, jakmile sledovací proces najde ukotvení.
// 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;
}
Nakonec pojďme naši LongTap()
metodu rozšířit tak, aby zahrnovala vyhledání ukotvení. Logickou hodnotu použijeme IsSessionStarted
k rozhodnutí, jestli hledáme všechny kotvy nebo zničíme všechny kotvy, jak je popsáno v přehledu aplikace.
/// <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();
}
}
Vyzkoušejte si to č. 2
Vaše aplikace teď podporuje vytváření ukotvení a jejich vyhledání. Sestavte aplikaci v Unity a nasaďte ji ze sady Visual Studio pomocí sady Visual Studio k nasazení a ladění.
Ujistěte se, že je váš HoloLens připojený k internetu. Po spuštění aplikace a vytvoření zprávy Unity zmizí krátké klepnutí v okolí. Měla by se zobrazit bílá datová krychle, která zobrazuje pozici a otočení ukotvení, které se má vytvořit. Proces vytváření ukotvení se volá automaticky. Když se pomalu díváte kolem okolí, zachytáváte data prostředí. Jakmile se shromažďuje dostatek dat prostředí, naše aplikace se pokusí vytvořit ukotvení v zadaném umístění. Po dokončení procesu vytváření ukotvení se datová krychle změní na zelenou. Zkontrolujte protokoly ladění v sadě Visual Studio a zjistěte, jestli všechno fungovalo podle očekávání.
Dlouhé klepnutí odeberete vše GameObjects
ze scény a zastavíte relaci prostorového ukotvení.
Jakmile se scéna vymaže, můžete znovu klepnout, což spustí relaci a vyhledá ukotvení, která jste vytvořili dříve. Jakmile jsou nalezeny, jsou vizualizovány modrými datovými krychlemi na ukotvené pozici a otočení. Tyto kotvy (pokud nevypršela jejich platnost) najdete v libovolném podporovaném zařízení, pokud mají správná ID ukotvení a mají přístup k vašemu prostředku prostorového ukotvení.
Odstranit ukotvení
Naše aplikace teď může vytvářet a vyhledávat ukotvení. Když odstraní GameObjects
, neodstraní ukotvení v cloudu. Pokud klepnete na existující ukotvení, přidáme funkci, která ji také odstraní v cloudu.
Pojďme přidat metoduDeleteAnchor
, která přijímá .GameObject
Potom použijeme SpatialAnchorManager
společně s komponentou objektu CloudNativeAnchor
k vyžádání odstranění ukotvení v cloudu.
/// <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!");
}
Abychom mohli tuto metodu volat z ShortTap
, musíme být schopni určit, jestli klepnutí bylo blízko existující viditelné ukotvení. Pojďme vytvořit pomocnou metodu, která se o to postará.
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;
}
}
Nyní můžeme rozšířit metodu ShortTap
tak, aby zahrnovala DeleteAnchor
volání.
/// <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);
}
}
Vyzkoušet č. 3
Sestavte aplikaci v Unity a nasaďte ji ze sady Visual Studio pomocí sady Visual Studio k nasazení a ladění.
Všimněte si, že umístění gesta pro ruční klepnutí je středem ruky v této aplikaci, nikoli špičkou prstů.
Když klepnete na ukotvení, buď se vytvoří (zelená) nebo se požadavek (modrý) odešle do služby prostorového ukotvení, aby se tato ukotvení z účtu odebrala. Zastavte relaci (dlouhé klepnutí) a znovu spusťte relaci (dlouhé klepnutí) a vyhledejte všechny ukotvení. Odstraněné kotvy se už nebudou nacházet.
Spojení všeho dohromady
Tady je postup, jak by měl soubor kompletní AzureSpatialAnchorsScript
třídy vypadat po vytvoření všech různých prvků. Můžete ho použít jako referenci k porovnání s vlastním souborem a zjistit, jestli vám můžou zůstat nějaké rozdíly.
Poznámka:
Všimněte si, že jsme do skriptu zahrnuli [RequireComponent(typeof(SpatialAnchorManager))]
. Díky tomu Unity zajistí, aby objekt GameObject, ke kterému se připojujeme AzureSpatialAnchorsScript
, má SpatialAnchorManager
také připojený.
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>
}
Další kroky
V tomto kurzu jste se naučili implementovat základní aplikaci Spatial Anchors pro HoloLens pomocí Unity. Další informace o používání Azure Spatial Anchors v nové aplikaci pro Android najdete v dalším kurzu.