Självstudie: Stegvisa instruktioner för att skapa en ny HoloLens Unity-app med Hjälp av Azure Spatial Anchors
Den här självstudien visar hur du skapar en ny HoloLens Unity-app med Azure Spatial Anchors.
Förutsättningar
För att kunna följa den här självstudien måste du ha:
- PC – en dator som kör Windows
- Visual Studio - Visual Studio 2019 installerat med arbetsbelastningen Universell Windows-plattform utveckling och Windows 10 SDK (10.0.18362.0 eller senare). C++/WinRT Visual Studio-tillägget (VSIX) för Visual Studio bör installeras från Visual Studio Marketplace.
- HoloLens – En HoloLens-enhet med utvecklarläge aktiverat. Den här artikeln kräver en HoloLens-enhet med Windows 10 Maj 2020 Update. Uppdatera till den senaste versionen på HoloLens genom att öppna appen Inställningar, gå till Uppdatering och säkerhet och välja Sök efter uppdateringar.
- Unity - Unity 2020.3.25 med moduler Universell Windows-plattform Build Support och Windows Build Support (IL2CPP)
Skapa och konfigurera Unity Project
Skapa nytt projekt
- I Unity Hub väljer du Nytt projekt
- Välj 3D
- Ange projektnamnet och ange en spara-plats
- Välj Skapa projekt och vänta tills Unity har skapat projektet
Ändra byggplattform
- I unity-redigeraren väljer du Filskapa>Inställningar
- Välj Universell Windows-plattform och sedan Växla plattform. Vänta tills Unity har slutfört bearbetningen av alla filer.
Importera ASA och OpenXR
- Starta funktionsverktyget för Mixad verklighet
- Välj din projektsökväg – mappen som innehåller mappar som Tillgångar, Paket, Projekt Inställningar och så vidare – och välj Identifiera funktioner
- Under Azure Mixed Reality Services väljer du båda
- Azure Spatial Anchors SDK Core
- Azure Spatial Anchors SDK för Windows
- Under Plattformsstöd väljer du
- Plugin-program för Mixed Reality OpenXR
Kommentar
Kontrollera att du har uppdaterat katalogen och att den senaste versionen har valts för varje
- Tryck på Hämta funktioner -->Import -->Godkänn -->Avsluta
- När du fokuserar om Unity-fönstret börjar Unity importera modulerna
- Om du får ett meddelande om hur du använder det nya indatasystemet väljer du Ja för att starta om Unity och aktivera serverdelarna.
Konfigurera projektinställningarna
Nu ska vi ange några Unity-projektinställningar som hjälper oss att rikta in oss på Windows Holographic SDK för utveckling.
Ändra OpenXR-Inställningar
- Välj Filversion>Inställningar (den kan fortfarande vara öppen från föregående steg)
- Välj Spelare Inställningar...
- Välj XR-plugin-hantering
- Kontrollera att fliken Universell Windows-plattform Inställningar är markerad och markera kryssrutan bredvid OpenXR och bredvid Microsoft HoloLens-funktionsgruppen
- Välj det gula varningstecknet bredvid OpenXR för att visa alla OpenXR-problem.
- Välj Åtgärda alla
- Om du vill åtgärda problemet "Minst en interaktionsprofil måste läggas till" väljer du Redigera för att öppna OpenXR-projektinställningarna. Under Interaktionsprofiler väljer du sedan symbolen + och väljer Microsoft Hand Interaction Profile (Microsoft Hand Interaction Profile)
Ändra kvalitet Inställningar
- Välj Redigera>projekt Inställningar Kvalitet>
- I kolumnen under logotypen Universell Windows-plattform väljer du pilen på raden Standard och väljer Mycket låg. Du vet att inställningen tillämpas korrekt när rutan i kolumnen Universell Windows-plattform och raden Mycket låg är grön.
Ange funktioner
- Gå till Redigera>project Inställningar> Player (du kanske fortfarande har det öppet från föregående steg).
- Kontrollera att fliken Universell Windows-plattform Inställningar är markerad
- I avsnittet Publicering Inställningar Konfiguration aktiverar du följande
- InternetClient
- InternetClientServer
- PrivateNetworkClientServer
- SpatialPerception (kan redan vara aktiverat)
Konfigurera huvudkameran
- I hierarkipanelen väljer du Main Kamera.
- I inspektören anger du dess transformeringsposition till 0,0,0.
- Leta reda på egenskapen Clear Flags och ändra listrutan från Skybox till Solid Color.
- Välj fältet Bakgrund för att öppna en färgväljare.
- Ange R, G, B och A till 0.
- Välj Lägg till komponent längst ned och lägg till komponenten Tracked Pose Driver (Spårad posedrivrutin ) i kameran
Prova #1
Nu bör du ha en tom scen som är redo att distribueras till din HoloLens-enhet. Om du vill testa att allt fungerar skapar du din app i Unity och distribuerar den från Visual Studio. Följ Använda Visual Studio för att distribuera och felsöka för att göra det. Du bör se Unity-startskärmen och sedan en tydlig visning.
Skapa en Spatial Anchors-resurs
Gå till Azure-portalen.
Välj Skapa en resurs i den vänstra rutan.
Använd sökrutan för att söka efter Spatial Anchors.
Välj Spatial Anchors och välj sedan Skapa.
Gör följande i fönstret Spatial Anchors-konto :
Ange ett unikt resursnamn med hjälp av vanliga alfanumeriska tecken.
Välj den prenumeration som du vill koppla resursen till.
Skapa en resursgrupp genom att välja Skapa ny. Ge den namnet myResourceGroup och välj sedan OK.
En resursgrupp är en logisk container där Azure-resurser, till exempel webbappar, databaser och lagringskonton, distribueras och hanteras. Du kan exempelvis välja att ta bort hela resursgruppen i ett enkelt steg längre fram.
Välj en plats (region) där du vill placera resursen.
Välj Skapa för att börja skapa resursen.
När resursen har skapats visar Azure-portalen att distributionen är klar.
Välj Gå till resurs. Nu kan du visa resursegenskaperna.
Kopiera resursens konto-ID-värde till en textredigerare för senare användning.
Kopiera även resursens kontodomänvärde till en textredigerare för senare användning.
Under Inställningar väljer du Åtkomstnyckel. Kopiera värdet Primärnyckel , Kontonyckel, till en textredigerare för senare användning.
Skapa och lägga till skript
- I Unity i projektfönstret skapar du en ny mapp med namnet Skript i mappen Tillgångar .
- Högerklicka på ->Skapa ->C#-skript i mappen. Ge den titeln AzureSpatialAnchorsScript
- Gå till GameObject -> Skapa tom.
- Välj den och byt namn på den i Inspector från GameObject till AzureSpatialAnchors.
- Fortfarande på
GameObject
- Ange dess position till 0,0,0
- Välj Lägg till komponent och sök efter och lägg till AzureSpatialAnchorsScript
- Välj Lägg till komponent igen och sök efter och lägg till AR Anchor Manager. Detta lägger automatiskt till AR-sessions ursprung också.
- Välj Lägg till komponent igen och sök efter och lägg till SpatialAnchorManager-skriptet
- I den tillagda SpatialAnchorManager-komponenten fyller du i konto-ID, kontonyckel och kontodomän som du har kopierat i föregående steg från resursen spatial anchors i Azure-portalen.
Appöversikt
Vår app stöder följande interaktioner:
Gest | Åtgärd |
---|---|
Tryck var som helst | Starta/fortsätt session + Skapa fästpunkt vid handposition |
Trycka på ett fästpunkt | Ta bort GameObject + ta bort fästpunkt i ASA Cloud Service |
Tryck på + Håll ned i 2 sekunder (+ sessionen körs) | Stoppa sessionen och ta bort alla GameObjects . Behåll fästpunkter i ASA Cloud Service |
Tryck på + Håll ned i 2 sekunder (+ sessionen körs inte) | Starta sessionen och leta efter alla fästpunkter. |
Lägg till tryckigenkänning
Nu ska vi lägga till lite kod i skriptet för att kunna känna igen en användares tryckgest.
- Öppna
AzureSpatialAnchorsScript.cs
i Visual Studio genom att dubbelklicka på skriptet i unity-projektfönstret. - Lägg till följande matris i klassen
public class AzureSpatialAnchorsScript : MonoBehaviour
{
/// <summary>
/// Used to distinguish short taps and long taps
/// </summary>
private float[] _tappingTimer = { 0, 0 };
- Lägg till följande två metoder under metoden Update(). Vi kommer att lägga till implementeringen i ett senare skede
// 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()
{
}
- Lägg till följande import
using UnityEngine.XR;
- Lägg till följande kod överst i
Update()
metoden. Detta gör det möjligt för appen att känna igen korta och långa (2 sek) hand knackande gester
// 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
}
}
}
}
}
Lägg till och konfigurera SpatialAnchorManager
ASA SDK erbjuder ett enkelt gränssnitt som kallas SpatialAnchorManager
för att göra anrop till ASA-tjänsten. Nu ska vi lägga till den som en variabel i vår AzureSpatialAnchorsScript.cs
Lägg först till importen
using Microsoft.Azure.SpatialAnchors.Unity;
Deklarera sedan variabeln
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()
I -metoden tilldelar du variabeln till komponenten som vi lade till i ett tidigare steg
// Start is called before the first frame update
void Start()
{
_spatialAnchorManager = GetComponent<SpatialAnchorManager>();
}
För att kunna ta emot felsöknings- och felloggar måste vi prenumerera på de olika återanropen
// 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}");
}
Kommentar
Om du vill visa loggarna kontrollerar du när du har skapat projektet från Unity och öppnar visual studio-lösningen .sln
genom att välja Felsöka –> Kör med felsökning och lämna HoloLens ansluten till datorn medan appen körs.
Starta session
För att skapa och hitta fästpunkter måste vi först starta en session. När du anropar StartSessionAsync()
SpatialAnchorManager
skapar du en session om det behövs och startar den. Nu ska vi lägga till detta i vår ShortTap()
metod.
/// <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();
}
Skapa ankarpunkt
Nu när vi har en session igång kan vi skapa fästpunkter. I det här programmet vill vi hålla reda på den skapade fästpunkten GameObjects
och de skapade fästpunktsidentifierarna (fästpunkts-ID:t). Nu ska vi lägga till två listor i vår kod.
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>();
Nu ska vi skapa en metod CreateAnchor
som skapar en fästpunkt vid en position som definieras av parametern .
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.
}
Eftersom rumsliga fästpunkter inte bara har en position utan också en rotation, ska vi ställa in rotationen så att den alltid orienterar sig mot HoloLens när den skapas.
/// <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);
}
Nu när vi har positionen och rotationen för den önskade fästpunkten ska vi skapa en synlig GameObject
. Observera att Spatial Anchors inte kräver att fästpunkten GameObject
är synlig för slutanvändaren eftersom det huvudsakliga syftet med Spatial Anchors är att tillhandahålla en gemensam och beständig referensram. I den här självstudien visualiserar vi fästpunkterna som kuber. Varje fästpunkt initieras som en vit kub, som förvandlas till en grön kub när skapandeprocessen har slutförts.
/// <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;
}
Kommentar
Vi använder en äldre skuggning eftersom den ingår i en Unity-standardversion. Andra skuggningar som standardskuggningen inkluderas endast om de anges manuellt eller om de är direkt en del av scenen. Om en skuggning inte ingår och programmet försöker återge den resulterar det i ett rosa material.
Nu ska vi lägga till och konfigurera Spatial Anchor-komponenterna. Vi ställer in förfallodatumet för fästpunkten till 3 dagar från det att fästpunkten skapades. Därefter tas de bort automatiskt från molnet. Kom ihåg att lägga till importen
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);
}
För att spara en fästpunkt måste användaren samla in miljödata.
/// <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%}");
}
}
Kommentar
En HoloLens kan eventuellt återanvända redan insamlade miljödata som omger fästpunkten, vilket resulterar i IsReadyForCreate
att de redan är sanna när de anropas för första gången.
Nu när den rumsliga molnankaret har förberetts kan vi prova själva spara här.
/// <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);
}
}
Slutligen ska vi lägga till funktionsanropet till vår ShortTap
metod
/// <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);
}
Vår app kan nu skapa flera fästpunkter. Alla enheter kan nu hitta de skapade fästpunkterna (om de inte har upphört att gälla ännu) så länge de känner till fästpunkts-ID:n och har åtkomst till samma Spatial Anchors-resurs i Azure.
Stoppa session och förstöra GameObjects
För att emulera en andra enhet som hittar alla fästpunkter stoppar vi nu sessionen och tar bort alla fästpunkts-GameObjects (vi behåller fästpunkts-ID:n). Därefter startar vi en ny session och frågar fästpunkterna med hjälp av lagrade fästpunkts-ID:n.
SpatialAnchorManager
kan ta hand om sessionen stoppas genom att helt enkelt anropa dess DestroySession()
metod. Nu ska vi lägga till detta i vår LongTap()
metod
/// <summary>
/// Called when a user is air tapping for a long time (>=2 sec)
/// </summary>
private async void LongTap()
{
_spatialAnchorManager.DestroySession();
}
Nu ska vi skapa en metod för att ta bort alla fästpunkter GameObjects
/// <summary>
/// Destroys all Anchor GameObjects
/// </summary>
private void RemoveAllAnchorGameObjects()
{
foreach (var anchorGameObject in _foundOrCreatedAnchorGameObjects)
{
Destroy(anchorGameObject);
}
_foundOrCreatedAnchorGameObjects.Clear();
}
Och kalla det efter att ha förstört sessionen i 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");
}
Leta upp fästpunkt
Nu ska vi försöka hitta fästpunkterna igen med rätt position och rotation som vi skapade dem i. För att göra det måste vi starta en session och skapa en Watcher
som söker efter fästpunkter som passar de angivna kriterierna. Som villkor matar vi det ID:t för de fästpunkter som vi skapade tidigare. Nu ska vi skapa en metod LocateAnchor()
och använda SpatialAnchorManager
för att skapa en Watcher
. För att hitta andra strategier än att använda fästpunkts-ID:t, se Anchor locate strategy (Fästpunktslokaliseringsstrategi)
/// <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!");
}
}
När en bevakare har startats utlöses ett återanrop när den hittade en fästpunkt som passar de angivna kriterierna. Nu ska vi först skapa vår fästpunktsmetoden med namnet SpatialAnchorManager_AnchorLocated()
som vi konfigurerar så att den anropas när bevakaren har hittat ett fästpunkt. Den här metoden skapar ett visuellt objekt GameObject
och kopplar den inbyggda fästpunktskomponenten till den. Den inbyggda fästpunktskomponenten ser till att rätt position och rotation av GameObject
är inställd.
I likhet med skapandeprocessen är fästpunkten kopplad till en GameObject. Denna GameObject behöver inte vara synlig i din scen för att rumsliga fästpunkter ska fungera. I den här självstudien visualiserar vi varje fästpunkt som en blå kub när de har hittats. Om du bara använder fästpunkten för att upprätta ett delat koordinatsystem behöver du inte visualisera det skapade 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);
});
}
}
Nu ska vi prenumerera på det anchorLocated-återanropet från SpatialAnchorManager
för att se till att vår SpatialAnchorManager_AnchorLocated()
metod anropas när bevakaren hittar en fästpunkt.
// 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;
}
Slutligen ska vi expandera vår LongTap()
metod så att den inkluderar att hitta fästpunkten. Vi använder booleskt IsSessionStarted
värde för att avgöra om vi letar efter alla fästpunkter eller förstör alla fästpunkter enligt beskrivningen i appöversikten
/// <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();
}
}
Prova #2
Appen har nu stöd för att skapa fästpunkter och hitta dem. Skapa din app i Unity och distribuera den från Visual Studio genom att följa Använda Visual Studio för att distribuera och felsöka den.
Kontrollera att HoloLens är ansluten till internet. När appen har startats och enheten med Unity-meddelandet försvinner, trycker du kort i din omgivning. En vit kub bör visas för att visa positionen och rotationen för fästpunkten som ska skapas. Processen för att skapa fästpunkter anropas automatiskt. När du långsamt ser dig omkring i din omgivning samlar du in miljödata. När tillräckligt med miljödata har samlats in försöker appen skapa en fästpunkt på den angivna platsen. När ankarprocessen är klar blir kuben grön. Kontrollera felsökningsloggarna i Visual Studio för att se om allt fungerade som avsett.
Tryck länge för att ta bort allt GameObjects
från scenen och stoppa den rumsliga fästpunktssessionen.
När scenen har rensats kan du trycka länge igen, vilket startar en session och letar efter fästpunkterna som du har skapat tidigare. När de hittas visualiseras de av blå kuber vid förankrad position och rotation. Dessa fästpunkter (så länge de inte har upphört att gälla) kan hittas av alla enheter som stöds så länge de har rätt fästpunkts-ID:n och har åtkomst till din rumsliga fästpunktsresurs.
Ta bort fästpunkt
Just nu kan vår app skapa och hitta fästpunkter. Även om den tar bort GameObjects
tar den inte bort fästpunkten i molnet. Nu ska vi lägga till funktionen för att även ta bort den i molnet om du trycker på ett befintligt fästpunkt.
Nu ska vi lägga till en metod DeleteAnchor
som tar emot en GameObject
. Sedan använder SpatialAnchorManager
vi tillsammans med objektets CloudNativeAnchor
komponent för att begära borttagning av fästpunkten i molnet.
/// <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!");
}
För att anropa den här metoden från ShortTap
måste vi kunna avgöra om en tryckning har varit nära ett befintligt synligt fästpunkt. Nu ska vi skapa en hjälpmetod som tar hand om den
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;
}
}
Vi kan nu utöka vår ShortTap
metod för att inkludera anropet 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);
}
}
Prova det #3
Skapa din app i Unity och distribuera den från Visual Studio genom att följa Använda Visual Studio för att distribuera och felsöka den.
Observera att platsen för din hand knackande gest är mitten av din hand i den här appen och inte spetsen på fingrarna.
När du trycker på en fästpunkt, antingen skapas (grön) eller finns (blå) skickas en begäran till den rumsliga fästpunktstjänsten för att ta bort fästpunkten från kontot. Stoppa sessionen (långt tryck) och starta sessionen igen (långt tryck) för att söka efter alla fästpunkter. De borttagna fästpunkterna kommer inte längre att finnas.
Sätta ihop allt
Så här ska den fullständiga AzureSpatialAnchorsScript
klassfilen se ut när alla olika element har sammanställts. Du kan använda den som referens för att jämföra med din egen fil och upptäcka om du kan ha några skillnader kvar.
Kommentar
Du kommer att märka att vi har inkluderat [RequireComponent(typeof(SpatialAnchorManager))]
i skriptet. Med detta kommer Unity att se till att GameObject där vi ansluter AzureSpatialAnchorsScript
till, också har bifogats SpatialAnchorManager
den.
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>
}
Nästa steg
I den här självstudien har du lärt dig hur du implementerar ett grundläggande Spatial Anchors-program för HoloLens med unity. Om du vill veta mer om hur du använder Azure Spatial Anchors i en ny Android-app fortsätter du till nästa självstudie.