Tutoriel : Instructions pas à pas pour créer une application HoloLens Unity avec Azure Spatial Anchors

Ce tutoriel vous montre comment créer une application HoloLens Unity avec Azure Spatial Anchors.

Prérequis

Pour suivre ce tutoriel, veillez à disposer des éléments suivants :

  1. PC : Un PC sous Windows
  2. Visual Studio : Visual Studio 2019 installé avec la charge de travail de développement pour la plateforme Windows universelle et le composant SDK Windows 10 (version 10.0.18362.0 ou ultérieure). L’extension Visual Studio (VSIX) C++/WinRT pour Visual Studio doit être installée à partir de Visual Studio Marketplace.
  3. HoloLens : Un appareil HoloLens avec le mode développeur activé. L’appareil utilisé dans cet article est un HoloLens doté de la mise à jour Windows 10 de mai 2020. Pour mettre à jour votre appareil HoloLens vers la dernière version, ouvrez l’application Paramètres, accédez à Mise et sécurité, puis sélectionnez le bouton Vérifier les mises à jour.
  4. Unity : Unity 2020.3.25 avec les modules Universal Windows Platform Build Support (Prise en charge de build pour la plateforme Windows universelle) et Windows Build Support (IL2CPP) (Prise en charge de build Windows [IL2CPP])

Création et configuration d’Unity Project

Créer un projet

  1. Dans Unity Hub (Hub Unity), sélectionnez New project (Nouveau projet).
  2. Sélectionnez 3D.
  3. Entrez le nom de votre projet (Project name) et entrez un emplacement de sauvegarde (Location).
  4. Sélectionnez Create project (Créer un projet) et attendez qu’Unity crée votre projet.

Modifier la plateforme de génération

  1. Dans votre éditeur Unity, sélectionnez File (Fichier)>Build Settings (Paramètres de génération).
  2. Sélectionnez Universal Windows Platform (Plateforme Windows universelle), puis Switch Platform (Changer de plateforme). Attendez qu’Unity ait fini de traiter tous les fichiers.

Importer ASA et OpenXR

  1. Lancez l’outil Mixed Reality Feature Tool.
  2. Sélectionnez le chemin du projet : le dossier qui contient des dossiers tels que Assets, Packages, ProjectSettings, et ainsi de suite, puis sélectionnez Découvrir les fonctionnalités.
  3. Sous Services Azure Mixed Reality, sélectionnez ces deux options :
    1. Kit de développement logiciel (SDK) Azure Spatial Anchors Core
    2. Kit de développement logiciel (SDK) Azure Spatial Anchors pour Windows
  4. Sous Prise en charge de la plateforme, sélectionnez :
    1. Plug-in Mixed Reality OpenXR

Notes

Vérifiez que vous avez actualisé le catalogue et que la version la plus récente est sélectionnée pour chaque élément.

MRFT - Feature Selection

  1. Appuyez sur Obtenir des fonctionnalités -->Importer -->Approuver -->Quitter.
  2. Lorsque vous recentrez votre fenêtre Unity, Unity commence à importer les modules.
  3. Si vous recevez un message concernant l’utilisation du nouveau système d’entrée, sélectionnez Oui pour redémarrer Unity et activer les back-ends.

Configurer les paramètres du projet

Nous allons maintenant définir les paramètres du projet Unity qui nous permettent de cibler le kit SDK Windows Holographique pour le développement.

Modifier les paramètres OpenXR

  1. Sélectionnez File (Fichier) >Build Settings (Paramètres de génération) [il est peut-être encore ouvert après l’étape précédente].
  2. Sélectionnez Player Settings… (Paramètres du lecteur).
  3. Sélectionnez XR Plug-in Management (Gestion des plug-ins XR).
  4. Assurez-vous que l’onglet Universal Windows Platform Settings (Paramètres de la plateforme Windows universelle) est sélectionné et cochez la case à côté de OpenXR et à côté de Microsoft HoloLens feature group (Groupe de fonctionnalités Microsoft HoloLens).
  5. Sélectionnez le symbole d’avertissement jaune à côté d’OpenXR pour voir tous les problèmes liés à OpenXR.
  6. Sélectionnez Fix all (Tout corriger).
  7. Pour résoudre le problème « At least one interaction profile must be added » (Au moins un profil d’interaction doit être ajouté), sélectionnez Edit (Modifier) pour ouvrir les paramètres OpenXR Project. Puis, sous Interaction Profiles (Profils d’interaction), sélectionnez le symbole + et choisissez Microsoft Hand Interaction Profile (Profil d’interaction de la main Microsoft)Unity - OpenXR Setup

Modifier les paramètres de qualité

  1. Sélectionnez Edit>Project Settings>Quality
  2. Dans la colonne située sous le logo Plateforme Windows universelle, sélectionnez la flèche sur la ligne Default (Par défaut), puis sélectionnez Very Low (Très faible). Vous savez que le paramètre est correctement appliqué lorsque la zone dans la colonne Plateforme Windows universelle et la ligne Très faible est verte.

Définir les fonctionnalités

  1. Accédez à Edit (Modifier)>Project Settings (Paramètres du projet)>Player (Lecteur) [il est peut-être encore ouvert après l’étape précédente].
  2. Assurez-vous que l’onglet Universal Windows Platform Settings (Paramètres de la plateforme Windows universelle) est sélectionné.
  3. Dans la section Configuration de Publishing Settings (Paramètres de publication), activez les éléments suivants :
    1. InternetClient
    2. InternetClientServer
    3. PrivateNetworkClientServer
    4. SpatialPerception (peut-être déjà activé)

Configurer la caméra principale

  1. Dans Hierarchy Panel, sélectionnez Main Camera.
  2. Dans Inspector, définissez sa position de transformation sur 0,0,0.
  3. Recherchez la propriété Clear Flags et remplacez Skybox par Solid Color dans la liste déroulante.
  4. Sélectionnez le champ Background (Arrière-plan) pour ouvrir un sélecteur de couleurs.
  5. Définissez R, G, B et A sur 0.
  6. Sélectionnez Add Component (Ajouter un composant) dans la partie inférieure et ajoutez le composant Tracked Pose Driver (Pilote de suivi de pose) à la caméra Unity - Camera Setup

Essai no 1

Vous devriez maintenant avoir une scène vide prête à être déployée sur votre appareil HoloLens. Pour vérifier que tout fonctionne correctement, générez votre application Unity et déployez-la à partir de Visual Studio. Pour ce faire, suivez les instructions fournies dans Utilisation de Visual Studio pour le déploiement et le débogage. Vous devez voir l’écran de démarrage Unity, puis un écran clair.

Créer une ressource Spatial Anchors

Accédez au portail Azure.

Dans le volet de gauche, sélectionnez Créer une ressource.

Utilisez la zone de recherche pour rechercher Spatial Anchors.

Screenshot showing the results of a search for Spatial Anchors.

Sélectionnez Spatial Anchors, puis Créer.

Dans le volet Spatial Anchors Account (compte Spatial Anchors), procédez comme suit :

  • Entrez un nom de ressource unique, en utilisant des caractères alphanumériques normaux.

  • Sélectionnez l’abonnement auquel attacher la ressource.

  • Créer un groupe de ressources en sélectionnant Créer. Nommez-le myResourceGroup, puis sélectionnez OK.

    Un groupe de ressources est un conteneur logique dans lequel les ressources Azure, comme les applications web, les bases de données et les comptes de stockage, sont déployées et managées. Par exemple, vous pouvez choisir de supprimer le groupe de ressources complet ultérieurement en une seule étape.

  • Sélectionnez un emplacement (région) où placer la ressource.

  • Sélectionnez Créer pour commencer à créer la ressource.

Screenshot of the Spatial Anchors pane for creating a resource.

Une fois la ressource créée, le portail Azure indique que votre déploiement est terminé.

Screenshot showing that the resource deployment is complete.

Sélectionnez Accéder à la ressource. Vous pouvez à présent afficher les propriétés de la ressource.

Copiez la valeur ID de compte de la ressource dans un éditeur de texte en vue d’une utilisation ultérieure.

Screenshot of the resource properties pane.

Copiez également le Domaine du compte de la ressource dans un éditeur de texte en vue d’une utilisation ultérieure.

Screenshot showing the resource's account domain value.

Sous Paramètres, sélectionnez Clé d’accès. Copiez la valeur de Clé primaire, Clé de compte, dans un éditeur de texte en vue d’une utilisation ultérieure.

Screenshot of the Keys pane for the account.

Création et ajout de scripts

  1. Dans Unity, dans le volet Project (Projet), créez un dossier appelé Scripts sous le dossier Assets.
  2. Dans le dossier, cliquez avec le bouton droit sur >Create (Créer) ->C# Script (Script C#). Intitulez-le AzureSpatialAnchorsScript.
  3. Accédez à GameObject ->Create Empty.
  4. Sélectionnez-le, puis dans Inspector (Inspecteur), renommez GameObject en AzureSpatialAnchors.
  5. Toujours sur le GameObject :
    1. Définissez sa position sur 0,0,0.
    2. Sélectionnez Add Component (Ajouter un composant), puis recherchez et ajoutez le script AzureSpatialAnchorsScript.
    3. Sélectionnez à nouveau Add Component (Ajouter un composant), puis recherchez et ajoutez AR Anchor Manager (Gestionnaire des ancres de réalité augmentée). Cela ajoutera automatiquement AR Session Origin (Origine de session de réalité augmentée) également.
    4. Sélectionnez à nouveau Add Component (Ajouter un composant), puis recherchez et ajoutez le script SpatialAnchorManager.
    5. Dans le composant SpatialAnchorManager ajouté, renseignez les champs Account ID (ID de compte), Account Key (Clé de compte) et Account Domain (Domaine de compte) que vous avez copiés à l’étape précédente à partir de la ressource d’ancres spatiales dans le portail Azure.

Unity - ASA GameObject

Vue d’ensemble de l’application

Notre application prendra en charge les interactions suivantes :

Mouvement Action
Appuyer n’importe où Démarrer/continuer la session + créer une ancre à la position de la main
Appuyer sur une ancre Supprimer le GameObject + supprimer l’ancre dans le service cloud ASA
Appuyer + maintenir pendant 2 secondes (+ la session est en cours d’exécution) Arrêter la session et supprimer tous les GameObjects. Conserver les ancres dans le service cloud ASA
Appuyer + maintenir pendant 2 secondes (+ la session n’est pas en cours d’exécution) Démarrer la session et rechercher toutes les ancres

Ajouter la reconnaissance du tapotement

Ajoutons un peu de code à notre script pour pouvoir reconnaître le geste de tapotement d’un utilisateur.

  1. Ouvrez AzureSpatialAnchorsScript.cs dans Visual Studio en double-cliquant sur le script dans le volet Project (Projet) de votre application Unity.
  2. Ajoutez le tableau suivant à votre classe.
public class AzureSpatialAnchorsScript : MonoBehaviour
{
    /// <summary>
    /// Used to distinguish short taps and long taps
    /// </summary>
    private float[] _tappingTimer = { 0, 0 };
  1. Ajoutez les deux méthodes suivantes sous la méthode Update(). Nous ajouterons l’implémentation ultérieurement.
// 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. Ajoutez l’importation suivante :
using UnityEngine.XR;
  1. Ajoutez le code suivant au-dessus de la méthode Update(). Ceci permettra à l’application de reconnaître les gestes courts et longs (deux secondes) de tapotement de la main.
// 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
                }
            }
        }

    }
}

Ajouter & Configurer SpatialAnchorManager

Le Kit de développement logiciel (SDK) ASA offre une interface simple appelée SpatialAnchorManager pour effectuer des appels au service ASA. Nous allons l’ajouter en tant que variable à notre AzureSpatialAnchorsScript.cs.

Commencez par ajouter l’importation.

using Microsoft.Azure.SpatialAnchors.Unity;

Déclarez ensuite la variable.

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;

Dans la méthode Start(), affectez la variable au composant que nous avons ajouté à l’étape précédente.

// Start is called before the first frame update
void Start()
{
    _spatialAnchorManager = GetComponent<SpatialAnchorManager>();
}

Pour recevoir les journaux de débogage et d’erreur, nous devons nous abonner aux différents rappels.

// 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}");
}

Notes

Pour afficher les journaux, après avoir généré le projet à partir d’Unity et ouvert la solution .sln de Visual Studio, sélectionnez Debug (Déboguer) --> Run with Debugging (Exécuter avec débogage) et laissez votre HoloLens connecté à votre ordinateur pendant l’exécution de l’application.

Démarrer une session

Pour créer et rechercher des ancres, nous devons d’abord démarrer une session. Lors de l’appel de StartSessionAsync(), SpatialAnchorManager créera une session si nécessaire, puis la démarrera. Ajoutons ceci à notre méthode 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();
}

Créer une ancre

Maintenant que nous disposons d’une session en cours d’exécution, nous pouvons créer des ancres. Dans cette application, nous aimerions garder la trace des GameObjects et des identifiants des ancres créées (ID d’ancre). Ajoutons deux listes à notre code.

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>();

Créons une méthode CreateAnchor qui crée une ancre à une position définie par son paramètre.

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

Puisque les ancres spatiales ont non seulement une position, mais également une rotation, définissons la rotation pour qu’elle soit toujours orientée vers l’appareil HoloLens lors de la création.

/// <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);

}

Maintenant que nous avons la position et la rotation de l’ancre souhaitée, créons un GameObject visible. Notez que le service Spatial Anchors ne nécessite pas que le GameObject de l’ancre soit visible par l’utilisateur final, car l’objectif principal de Spatial Anchors est de fournir un cadre de référence commun et persistant. Dans le cadre de ce tutoriel, nous allons visualiser les ancres sous forme de cubes. Chaque ancre est initialisée sous la forme d’un cube blanc, qui se transformera en cube vert une fois le processus de création réussi.

/// <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;

}

Notes

Nous utilisons un nuanceur hérité, car il est inclus dans une build Unity par défaut. D’autres nuanceurs, comme le nuanceur par défaut, sont uniquement inclus s’ils sont spécifiés manuellement ou s’ils font directement partie de la scène. Si un nuanceur n’est pas inclus et que l’application essaie de l’afficher, il en résulte un matériau rose.

À présent, ajoutons et configurons les composants de l’ancre spatiale. Nous définissons l’expiration de l’ancre à trois jours à compter de sa création. Après cela, elle sera automatiquement supprimée du cloud. N’oubliez pas d’ajouter l’importation.

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);

}

Pour enregistrer une ancre, l’utilisateur doit collecter des données d’environnement.

/// <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%}");
    }

}

Notes

Un HoloLens peut éventuellement réutiliser les données d’environnement déjà capturées autour de l’ancre, ce qui fait que IsReadyForCreate est déjà vrai lorsqu’il est appelé pour la première fois.

Maintenant que l’ancre spatiale du cloud a été préparée, nous pouvons essayer de l’enregistrer.

/// <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);
    }
}

Enfin, ajoutons l’appel de fonction à notre méthode ShortTap.

/// <summary>
/// Called when a user is air tapping for a short time 
/// </summary>
/// <param name="handPosition">Location where tap was registered</param>
private async void ShortTap(Vector3 handPosition)
{
    await _spatialAnchorManager.StartSessionAsync();
        await CreateAnchor(handPosition);
}

Notre application peut maintenant créer plusieurs ancres. Tout appareil peut maintenant localiser les ancres créées (si elles n’ont pas encore expiré), à condition de connaître les ID des ancres et d’avoir accès à la même ressource Spatial Anchors sur Azure.

Arrêter la session & Détruire GameObjects

Pour émuler un deuxième appareil recherchant toutes les ancres, nous allons maintenant arrêter la session et supprimer tous les GameObjects des ancres (nous conservons les ID des ancres). Après cela, nous allons démarrer une nouvelle session et interroger les ancres à l’aide des ID d’ancres stockés.

SpatialAnchorManager peut se charger de l’arrêt de la session en appelant simplement sa méthode DestroySession(). Ajoutons ceci à notre méthode LongTap().

/// <summary>
/// Called when a user is air tapping for a long time (>=2 sec)
/// </summary>
private async void LongTap()
{
        _spatialAnchorManager.DestroySession();
}

Nous allons créer une méthode pour supprimer tous les GameObjects d’ancre.

/// <summary>
/// Destroys all Anchor GameObjects
/// </summary>
private void RemoveAllAnchorGameObjects()
{
    foreach (var anchorGameObject in _foundOrCreatedAnchorGameObjects)
    {
        Destroy(anchorGameObject);
    }
    _foundOrCreatedAnchorGameObjects.Clear();
}

Appelez-la après la destruction de la session dans 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");
}

Localiser l’ancre

Nous allons maintenant essayer de retrouver les ancres avec la position et la rotation correctes dans lesquelles nous les avons créées. Pour ce faire, nous devons démarrer une session et créer un Watcher qui recherchera les ancres correspondant aux critères spécifiés. Comme critères, nous lui fournirons les ID des ancres que nous avons créées précédemment. Créons une méthode LocateAnchor() et utilisons SpatialAnchorManager pour créer un Watcher. Pour les stratégies de localisation autres que l’utilisation des ID des ancres, consultez Stratégie de localisation des ancres.

/// <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!");
    }
}

Lorsqu’un watcher est démarré, il déclenche un rappel lorsqu’il trouve une ancre qui correspond aux critères spécifiés. Commençons par créer notre méthode de localisation d’ancre appelée SpatialAnchorManager_AnchorLocated() que nous allons configurer pour qu’elle soit appelée lorsque le watcher a localisé une ancre. Cette méthode crée un GameObject visuel et y attache le composant d’ancrage natif. Le composant d’ancrage natif s’assure que la position et la rotation correctes du GameObject sont définies.

Comme pour le processus de création, l’ancre est attachée à un GameObject. Ce GameObject ne doit pas nécessairement être visible dans votre scène pour que les ancres spatiales fonctionnent. Dans le cadre de ce tutoriel, nous allons visualiser chaque ancre sous la forme d’un cube bleu une fois qu’elle aura été localisée. Si vous utilisez l’ancre uniquement pour établir un système de coordonnées partagé, il n’est pas nécessaire de visualiser le GameObject créé.

/// <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);
        });
    }
}

Souscrivons maintenant au rappel AnchorLocated de SpatialAnchorManager pour nous assurer que notre méthode SpatialAnchorManager_AnchorLocated() est appelée dès que le watcher trouve une ancre.

// 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;
}

Enfin, étendons notre méthode LongTap() pour inclure la recherche de l’ancre. Nous allons utiliser la valeur booléenne IsSessionStarted pour décider si nous recherchons toutes les ancres ou si nous détruisons toutes les ancres, comme décrit dans la présentation de l’application.

/// <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();
    }
}

Essai no 2

Votre application prend désormais en charge la création d’ancres et leur localisation. Générez votre application dans Unity et déployez-la à partir de Visual Studio en suivant les instructions fournies dans Utilisation de Visual Studio pour le déploiement et le débogage.

Assurez-vous que votre HoloLens est connecté à Internet. Une fois l’application démarrée et le message « créée avec Unity » a disparu, appuyez brièvement dans votre environnement. Un cube blanc doit apparaître pour indiquer la position et la rotation de l’ancre à créer. Le processus de création de l’ancre est appelé automatiquement. En regardant lentement autour de vous, vous capturez des données sur l’environnement. Une fois que suffisamment de données sur l’environnement ont été collectées, notre application essaiera de créer une ancre à l’emplacement spécifié. Une fois le processus de création de l’ancre terminé, le cube devient vert. Vérifiez vos journaux de débogage dans Visual Studio pour voir si tout a fonctionné comme prévu.

Appuyez longuement pour supprimer tous les GameObjects de votre scène et arrêter la session d’ancrage spatial.

Une fois votre scène effacée, vous pouvez à nouveau appuyer longuement pour démarrer une session et rechercher les ancres que vous avez créées précédemment. Une fois trouvées, elles sont visualisées par des cubes bleus à la position et à la rotation ancrées. Ces ancres (tant qu’elles n’ont pas expiré) peuvent être trouvées par n’importe quel appareil pris en charge, à condition qu’il dispose des ID d’ancre corrects et qu’il ait accès à votre ressource d’ancre spatiale.

Supprimer une ancre

Pour l’instant, notre application peut créer et localiser des ancres. Si elle supprime bien les GameObjects, elle ne supprime pas l’ancre dans le cloud. Ajoutons la fonctionnalité permettant de la supprimer également dans le cloud si vous appuyez sur une ancre existante.

Ajoutons une méthode DeleteAnchor qui reçoit un GameObject. Nous utiliserons ensuite le SpatialAnchorManager conjointement avec le composant CloudNativeAnchor de l’objet pour demander la suppression de l’ancre dans le cloud.

/// <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!");
}

Pour appeler cette méthode à partir de ShortTap, nous devons être en mesure de déterminer si un tapotement a été effectué près d’une ancre visible existante. Créons une méthode d’assistance qui s’en chargera.

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;
    }
}

Nous pouvons maintenant étendre notre méthode ShortTap pour inclure l’appel 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);
    }
}

Essai no 3

Générez votre application dans Unity et déployez-la à partir de Visual Studio en suivant les instructions fournies dans Utilisation de Visual Studio pour le déploiement et le débogage.

Notez que l’emplacement de votre geste de tapotement de la main désigne le centre de votre main dans cette application, et non le bout de vos doigts.

Lorsque vous appuyez sur une ancre, qu’elle soit créée (vert) ou localisée (bleu), une demande est envoyée au service Spatial Anchors pour supprimer cette ancre du compte. Arrêtez la session (tapotement long) et recommencez la session (tapotement long) pour rechercher toutes les ancres. Les ancres supprimées ne seront plus localisées.

Regroupement

Voici comment le fichier de classe AzureSpatialAnchorsScript complet doit se présenter une fois que les différents éléments ont été mis en ensemble. Vous pouvez vous en servir d’élément de comparaison pour votre propre fichier et pour repérer les différences.

Notes

Vous remarquerez que nous avons inclus [RequireComponent(typeof(SpatialAnchorManager))] dans le script. Grâce à cela, Unity s’assure que SpatialAnchorManager est également attaché au GameObject auquel nous attachons AzureSpatialAnchorsScript.

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>

}

Étapes suivantes

Dans ce tutoriel, vous avez appris à implémenter une application Spatial Anchors de base pour HoloLens à l’aide d’Unity. Pour en savoir plus sur l’utilisation d’Azure Spatial Anchors dans une nouvelle application Android, passez au tutoriel suivant.