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:

  1. PC – en dator som kör Windows
  2. 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.
  3. 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.
  4. 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

  1. I Unity Hub väljer du Nytt projekt
  2. Välj 3D
  3. Ange projektnamnet och ange en spara-plats
  4. Välj Skapa projekt och vänta tills Unity har skapat projektet

Ändra byggplattform

  1. I unity-redigeraren väljer du Filskapa>Inställningar
  2. 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

  1. Starta funktionsverktyget för Mixad verklighet
  2. 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
  3. Under Azure Mixed Reality Services väljer du båda
    1. Azure Spatial Anchors SDK Core
    2. Azure Spatial Anchors SDK för Windows
  4. Under Plattformsstöd väljer du
    1. Plugin-program för Mixed Reality OpenXR

Kommentar

Kontrollera att du har uppdaterat katalogen och att den senaste versionen har valts för varje

MRFT - Feature Selection

  1. Tryck på Hämta funktioner -->Import -->Godkänn -->Avsluta
  2. När du fokuserar om Unity-fönstret börjar Unity importera modulerna
  3. 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

  1. Välj Filversion>Inställningar (den kan fortfarande vara öppen från föregående steg)
  2. Välj Spelare Inställningar...
  3. Välj XR-plugin-hantering
  4. Kontrollera att fliken Universell Windows-plattform Inställningar är markerad och markera kryssrutan bredvid OpenXR och bredvid Microsoft HoloLens-funktionsgruppen
  5. Välj det gula varningstecknet bredvid OpenXR för att visa alla OpenXR-problem.
  6. Välj Åtgärda alla
  7. 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)Unity - OpenXR Setup

Ändra kvalitet Inställningar

  1. Välj Redigera>projekt Inställningar Kvalitet>
  2. 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

  1. Gå till Redigera>project Inställningar> Player (du kanske fortfarande har det öppet från föregående steg).
  2. Kontrollera att fliken Universell Windows-plattform Inställningar är markerad
  3. I avsnittet Publicering Inställningar Konfiguration aktiverar du följande
    1. InternetClient
    2. InternetClientServer
    3. PrivateNetworkClientServer
    4. SpatialPerception (kan redan vara aktiverat)

Konfigurera huvudkameran

  1. I hierarkipanelen väljer du Main Kamera.
  2. I inspektören anger du dess transformeringsposition till 0,0,0.
  3. Leta reda på egenskapen Clear Flags och ändra listrutan från Skybox till Solid Color.
  4. Välj fältet Bakgrund för att öppna en färgväljare.
  5. Ange R, G, B och A till 0.
  6. Välj Lägg till komponent längst ned och lägg till komponenten Tracked Pose Driver (Spårad posedrivrutin ) i kameran Unity - Camera Setup

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.

Screenshot showing the results of a search for 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.

Screenshot of the Spatial Anchors pane for creating a resource.

När resursen har skapats visar Azure-portalen att distributionen är klar.

Screenshot showing that the resource deployment is complete.

Välj Gå till resurs. Nu kan du visa resursegenskaperna.

Kopiera resursens konto-ID-värde till en textredigerare för senare användning.

Screenshot of the resource properties pane.

Kopiera även resursens kontodomänvärde till en textredigerare för senare användning.

Screenshot showing the resource's account domain value.

Under Inställningar väljer du Åtkomstnyckel. Kopiera värdet Primärnyckel , Kontonyckel, till en textredigerare för senare användning.

Screenshot of the Keys pane for the account.

Skapa och lägga till skript

  1. I Unity i projektfönstret skapar du en ny mapp med namnet Skript i mappen Tillgångar .
  2. Högerklicka på ->Skapa ->C#-skript i mappen. Ge den titeln AzureSpatialAnchorsScript
  3. Gå till GameObject -> Skapa tom.
  4. Välj den och byt namn på den i Inspector från GameObject till AzureSpatialAnchors.
  5. Fortfarande på GameObject
    1. Ange dess position till 0,0,0
    2. Välj Lägg till komponent och sök efter och lägg till AzureSpatialAnchorsScript
    3. 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å.
    4. Välj Lägg till komponent igen och sök efter och lägg till SpatialAnchorManager-skriptet
    5. 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.

Unity - ASA GameObject

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.

  1. Öppna AzureSpatialAnchorsScript.cs i Visual Studio genom att dubbelklicka på skriptet i unity-projektfönstret.
  2. 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 };
  1. 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()
{
}
  1. Lägg till följande import
using UnityEngine.XR;
  1. 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 .slngenom 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 GameObjectstar 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 ShortTapmå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.