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í:

  1. POČÍTAČ – Počítač s Windows
  2. 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.
  3. 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.
  4. 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).

  1. V Centru Unity vyberte Nový projekt.
  2. Výběr 3D
  3. Zadejte název projektu a zadejte umístění pro uložení.
  4. Vyberte Vytvořit projekt a počkejte, až Unity vytvoří váš projekt.

Změna platformy sestavení

  1. V editoru Unity vyberte Nastavení sestavení souboru>.
  2. Vyberte Univerzální platforma Windows a pak přepnout platformu. Počkejte, až Unity dokončí zpracování všech souborů.

Import ASA a OpenXR

  1. Spuštění nástroje pro funkce hybridní reality
  2. Vyberte cestu k projektu – složku, která obsahuje složky, jako jsou prostředky, balíčky, projekt Nastavení atd. a vyberte Zjistit funkce.
  3. V části Azure Mixed Reality Services vyberte obě možnosti.
    1. Azure Spatial Anchors SDK Core
    2. Azure Spatial Anchors SDK pro Windows
  4. V části Podpora platformy vyberte
    1. 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.

MRFT - Feature Selection

  1. Stiskněte tlačítko Získat funkce -->Import -->Approve -->Exit.
  2. Při refokusování okna Unity začne Unity importovat moduly.
  3. 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

  1. >Vyberte souborový build Nastavení (může být stále otevřený z předchozího kroku).
  2. Vyberte Nastavení přehrávače...
  3. Výběr správy modulů plug-in XR
  4. 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.
  5. Pokud chcete zobrazit všechny problémy s OpenXR, vyberte žluté znaménko upozornění vedle OpenXR .
  6. Výběr možnosti Opravit vše
  7. 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.Unity - OpenXR Setup

Nastavení ke změně kvality

  1. Výběr možnosti Upravit>projekt Nastavení> Quality
  2. 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í

  1. Přejděte na Upravit>projekt Nastavení> Player (možná ho máte otevřený z předchozího kroku).
  2. Ujistěte se, že je vybraná karta Univerzální platforma Windows Nastavení.
  3. V části Publikování Nastavení Konfigurace povolte následující:
    1. InternetClient
    2. InternetClientServer
    3. PrivateNetworkClientServer
    4. SpatialPerception (možná už je povolené)

Nastavení hlavní kamery

  1. Na panelu hierarchie vyberte hlavní Kamera.
  2. V inspektoru nastavte jeho pozici transformace na 0,0,0.
  3. Najděte vlastnost Vymazat příznaky a změňte rozevírací seznam ze skyboxu na plnou barvu.
  4. Výběrem pole Pozadí otevřete výběr barvy.
  5. Nastavte R, G, B a A na hodnotu 0.
  6. V dolní části vyberte Přidat komponentu a přidejte do kamery komponentu ovladače Tracked Pose Driver . Unity - Camera Setup

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.

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

Screenshot of the Spatial Anchors pane for creating a resource.

Po vytvoření prostředku se na webu Azure Portal zobrazí, že je vaše nasazení dokončené.

Screenshot showing that the resource deployment is complete.

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í.

Screenshot of the resource properties pane.

Zkopírujte také hodnotu domény účtu prostředku do textového editoru pro pozdější použití.

Screenshot showing the resource's account domain value.

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í.

Screenshot of the Keys pane for the account.

Vytváření a přidávání skriptů

  1. V Unity v podokně Projekt vytvořte novou složku s názvem Skripty ve složce Assets .
  2. Ve složce klikněte pravým tlačítkem myši na skript ->Create ->C#. Pojmete ho AzureSpatialAnchorsScript
  3. Přejděte na GameObject ->Create Empty.
  4. Vyberte ho a v inspektoru ho přejmenujte z Objektu GameObject na AzureSpatialAnchors.
  5. Stále na GameObject
    1. Nastavit jeho pozici na 0,0,0
    2. Vyberte Přidat komponentu a vyhledejte a přidejte AzureSpatialAnchorsScript.
    3. 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.
    4. Znovu vyberte Přidat komponentua vyhledejte a přidejte skript SpatialAnchorManager .
    5. 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.

Unity - ASA GameObject

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.

  1. Otevřete AzureSpatialAnchorsScript.cs v sadě Visual Studio poklikáním na skript v podokně projektu Unity.
  2. 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 };
  1. 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()
{
}
  1. Přidejte následující import.
using UnityEngine.XR;
  1. 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í .slnsady 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.