Tutoriel : Instructions pas à pas pour créer une application Android à l’aide d’Azure Spatial Anchors

Ce tutoriel vous montre comment créer une application Android qui intègre la fonctionnalité ARCore avec Azure Spatial Anchors.

Prérequis

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

Prise en main

Démarrez Android Studio. Dans la fenêtre d’accueil Welcome to Android Studio, cliquez sur Start a new Android Studio project.

  1. Sélectionnez Fichier->Nouveau projet.
  2. Dans la fenêtre Create New Projets, sous la section Phone and Tablet , choisissez Empty Activity, puis cliquez sur Next.
  3. Dans la fenêtre Nouveau projet - Activité vide, modifiez les valeurs suivantes :
    • Modifiez le Nom, le Nom du package et l’Emplacement d’enregistrement aux valeurs souhaitées
    • Réglez Langage sur Java
    • Réglez Niveau d’API minimal sur API 26: Android 8.0 (Oreo)
    • Laissez les autres options telles quelles
    • Cliquez sur Terminer.
  4. Le programme d’installation de composants Component Installer s’exécute. Après quelques traitements, Android Studio ouvre l’IDE.

Android Studio - New Project

Essai

Pour tester votre nouvelle application, connectez votre appareil prêt pour le développement à votre ordinateur de développement à l’aide d’un câble USB. En haut à droite d’Android Studio, sélectionnez votre appareil connecté, puis cliquez sur l’icône Exécuter l’application. Android Studio installe l’application sur votre appareil connecté et le démarre. Vous devez maintenant voir « Hello World ! » affiché dans l’application qui s’exécute sur votre appareil. Cliquez sur Exécuter->Arrêter 'application'. Android Studio - Run

Intégration d’ARCore

ARCore est la plateforme de Google dédiée à la création d’expériences de réalité augmentée, ce qui permet à votre appareil de suivre sa position au fur et à mesure de ses déplacements et de développer sa propre compréhension du monde réel.

Modifiez app\manifests\AndroidManifest.xml pour inclure les entrées suivantes au sein du nœud <manifest> racine. Cet extrait de code fait plusieurs choses :

  • Il permet à votre application d’accéder à la caméra de votre appareil.
  • Il garantit aussi que seuls les appareils qui prennent en charge ARCore peuvent voir votre application sur le Google Play Store.
  • Il configure le Google Play Store de façon à télécharger et à installer ARCore, si ce n’est déjà fait, pendant l’installation de votre application.
<manifest ...>

    <uses-permission android:name="android.permission.CAMERA" />
    <uses-feature android:name="android.hardware.camera.ar" />

    <application>
        ...
        <meta-data android:name="com.google.ar.core" android:value="required" />
        ...
    </application>

</manifest>

Modifiez Gradle Scripts\build.gradle (Module: app) pour y inclure l’entrée ci-dessous. Ce code garantit que votre application cible ARCore version 1.25. Après cette modification, il se peut que vous obteniez une notification de Gradle qui vous invite à effectuer une synchronisation : cliquez sur Sync now.

dependencies {
    ...
    implementation 'com.google.ar:core:1.25.0'
    ...
}

Intégration de Sceneform

Sceneform simplifie le rendu de scènes 3D réalistes dans les applications de réalité augmentée sans qu’il soit nécessaire d’apprendre OpenGL.

Modifiez Gradle Scripts\build.gradle (Module: app) pour y inclure les entrées ci-dessous. Ce code permet à votre application d’utiliser des constructions de langage à partir de Java 8, ce qui est une exigence de Sceneform. Cela permet également de garantir que votre application cible Sceneform version 1.15. Après cette modification, il se peut que vous obteniez une notification de Gradle qui vous invite à effectuer une synchronisation : cliquez sur Sync now.

android {
    ...

    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }
}

dependencies {
    ...
    implementation 'com.google.ar.sceneform.ux:sceneform-ux:1.15.0'
    ...
}

Ouvrez votre fichier app\res\layout\activity_main.xml et remplacez l’élément <TextView ... /> Hello World existant par l’ArFragment suivant. Ce code a pour effet d’afficher le flux de caméra sur votre écran, ce qui permet à ARCore de suivre la position de votre appareil au fur et à mesure de ses déplacements.

<fragment android:name="com.google.ar.sceneform.ux.ArFragment"
    android:id="@+id/ux_fragment"
    android:layout_width="match_parent"
    android:layout_height="match_parent" />

Notes

Pour afficher les données XML brutes de votre activité principale, cliquez sur le bouton « Code » ou « Fractionner » dans le coin supérieur droit d’Android Studio.

Redéployez votre application sur votre appareil pour la vérifier une nouvelle fois. Cette fois, vous êtes invité à fournir des autorisations pour la caméra. Après approbation, le rendu du flux de caméra doit s’afficher sur votre écran.

Placer un objet dans le monde réel

Vous allez maintenant créer et placer un objet à l’aide de votre application. Pour commencer, ajoutez les importations suivantes à app\java\<PackageName>\MainActivity :

import com.google.ar.core.HitResult;
import com.google.ar.core.Plane;
import com.google.ar.sceneform.AnchorNode;
import com.google.ar.sceneform.math.Vector3;
import com.google.ar.sceneform.rendering.Color;
import com.google.ar.sceneform.rendering.MaterialFactory;
import com.google.ar.sceneform.rendering.Renderable;
import com.google.ar.sceneform.rendering.ShapeFactory;
import com.google.ar.sceneform.ux.ArFragment;

import android.view.MotionEvent;

Ajoutez ensuite les variables de membre suivantes dans votre classe MainActivity :

private boolean tapExecuted = false;
private final Object syncTaps = new Object();
private ArFragment arFragment;
private AnchorNode anchorNode;
private Renderable nodeRenderable = null;
private float recommendedSessionProgress = 0f;

Ajoutez maintenant le code suivant à votre méthode app\java\<PackageName>\MainActivityonCreate(). Ce code raccorde un écouteur, appelé handleTap(), qui détecte les appuis de l’utilisateur sur l’écran de votre appareil. S’il appuie sur une surface du monde réel qui a déjà été reconnue par le suivi d’ARCore, l’écouteur s’exécute.

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);

    this.arFragment = (ArFragment) getSupportFragmentManager().findFragmentById(R.id.ux_fragment);
    this.arFragment.setOnTapArPlaneListener(this::handleTap);
}

Enfin, ajoutez la méthode handleTap() suivante pour tout lier ensemble. Une sphère est alors créée et placée là où l’utilisateur a appuyé. Au départ, la sphère est noire, car à cet instant, this.recommendedSessionProgress est défini sur zéro. Cette valeur sera ajustée par la suite.

protected void handleTap(HitResult hitResult, Plane plane, MotionEvent motionEvent) {
    synchronized (this.syncTaps) {
        if (this.tapExecuted) {
            return;
        }

        this.tapExecuted = true;
    }

    this.anchorNode = new AnchorNode();
    this.anchorNode.setAnchor(hitResult.createAnchor());

    MaterialFactory.makeOpaqueWithColor(this, new Color(
            this.recommendedSessionProgress,
            this.recommendedSessionProgress,
            this.recommendedSessionProgress))
            .thenAccept(material -> {
                this.nodeRenderable = ShapeFactory.makeSphere(0.1f, new Vector3(0.0f, 0.15f, 0.0f), material);
                this.anchorNode.setRenderable(nodeRenderable);
                this.anchorNode.setParent(arFragment.getArSceneView().getScene());
            });
}

Redéployez votre application sur votre appareil pour la vérifier une nouvelle fois. Cette fois, vous pouvez déplacer votre appareil pour permettre à ARCore de commencer à reconnaître votre environnement. Appuyez ensuite sur l’écran pour créer et placer votre sphère noire sur la surface de votre choix.

Attacher une ancre spatiale Azure locale

Modifiez Gradle Scripts\build.gradle (Module: app) pour y inclure l’entrée ci-dessous. Cet exemple d’extrait de code cible la version 2.10.2 du kit de développement logiciel (SDK) d’Azure Spatial Anchors. Notez que la version du kit de développement logiciel (SDK) 2.7.0 est affichage la version minimale prise en charge et que vous référencez une version plus récente d’Azure Spatial Anchors. Nous vous recommandons d’utiliser la dernière version du kit de développement logiciel (SDK) d’Azure Spatial Anchors. Vous trouverez les notes de publication du kit de développement logiciel (SDK) ici.

dependencies {
    ...
    implementation 'com.microsoft.azure.spatialanchors:spatialanchors_jni:[2.10.2]'
    implementation 'com.microsoft.azure.spatialanchors:spatialanchors_java:[2.10.2]'
    ...
}

Si vous ciblez le kit de développement logiciel (SDK) d’Azure Spatial Anchors 2.10.0 ou une version ultérieure, incluez l’entrée suivante dans la section des référentiels du fichier settings.gradle de votre projet. Cela inclura l’URL du flux de package Maven qui héberge les packages Android d’Azure Spatial Anchors pour le SDK 2.10.0 ou une version ultérieure :

dependencyResolutionManagement {
    ...
    repositories {
        ...
        maven {
            url 'https://pkgs.dev.azure.com/aipmr/MixedReality-Unity-Packages/_packaging/Maven-packages/maven/v1'
        }
        ...
    }
}

Cliquez avec le bouton droit sur app\java\<PackageName>->Nouveau->Classe Java. Affectez à Nom la valeur MyFirstApp, puis sélectionnez Classe. Un fichier appelé MyFirstApp.java est alors créé. Ajoutez-y l’importation suivante :

import com.microsoft.CloudServices;

Définissez android.app.Application comme sa superclasse.

public class MyFirstApp extends android.app.Application {...

Ajoutez ensuite le code suivant à l’intérieur de la nouvelle classe MyFirstApp, qui fait en sorte qu’Azure Spatial Anchors soit initialisé avec le contexte de votre application.

    @Override
    public void onCreate() {
        super.onCreate();
        CloudServices.initialize(this);
    }

À présent, modifiez app\manifests\AndroidManifest.xml pour inclure l’entrée suivante au sein du nœud <application> racine. Ce code raccorde la classe Application que vous avez créée dans votre application.

    <application
        android:name=".MyFirstApp"
        ...
    </application>

De retour dans app\java\<PackageName>\MainActivity, ajoutez les importations suivantes :

import android.view.MotionEvent;
import android.util.Log;

import com.google.ar.sceneform.ArSceneView;
import com.google.ar.sceneform.Scene;
import com.microsoft.azure.spatialanchors.CloudSpatialAnchor;
import com.microsoft.azure.spatialanchors.CloudSpatialAnchorSession;
import com.microsoft.azure.spatialanchors.SessionLogLevel;

Ajoutez ensuite les variables de membre suivantes dans votre classe MainActivity :

private float recommendedSessionProgress = 0f;

private ArSceneView sceneView;
private CloudSpatialAnchorSession cloudSession;
private boolean sessionInitialized = false;

Ajoutez maintenant la méthode initializeSession() suivante au sein de la classe mainActivity. Une fois appelée, elle assurera la création et l’initialisation d’une session Azure Spatial Anchors pendant le démarrage de votre application. Ce code permet de s’assurer que la session sceneview transmise à la session ASA via l’appel cloudSession.setSession n’est pas null en faisant un retour anticipé.

private void initializeSession() {
    if (sceneView.getSession() == null) {
        //Early return if the ARCore Session is still being set up
        return;
    }

    if (this.cloudSession != null) {
        this.cloudSession.close();
    }
    this.cloudSession = new CloudSpatialAnchorSession();
    this.cloudSession.setSession(sceneView.getSession());
    this.cloudSession.setLogLevel(SessionLogLevel.Information);
    this.cloudSession.addOnLogDebugListener(args -> Log.d("ASAInfo", args.getMessage()));
    this.cloudSession.addErrorListener(args -> Log.e("ASAError", String.format("%s: %s", args.getErrorCode().name(), args.getErrorMessage())));

    sessionInitialized = true;
}

Étant donné que initializeSession() peut effectuer un retour anticipé si la session sceneView n’est pas encore configurée (c’est-à-dire, si sceneView.getSession() a la valeur null), nous ajoutons un appel OnUpdate pour s’assurer que la session ASA est initialisée une fois la session sceneView créée.

private void scene_OnUpdate(FrameTime frameTime) {
    if (!sessionInitialized) {
        //retry if initializeSession did an early return due to ARCore Session not yet available (i.e. sceneView.getSession() == null)
        initializeSession();
    }
}

Maintenant, raccordons les méthodes initializeSession() et scene_OnUpdate(...) à la méthode onCreate(). De même, veillez à ce que les images issues de votre flux de caméra sont envoyées au kit SDK Azure Spatial Anchors à des fins de traitement.

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);

    this.arFragment = (ArFragment) getSupportFragmentManager().findFragmentById(R.id.ux_fragment);
    this.arFragment.setOnTapArPlaneListener(this::handleTap);

    this.sceneView = arFragment.getArSceneView();
    Scene scene = sceneView.getScene();
    scene.addOnUpdateListener(frameTime -> {
        if (this.cloudSession != null) {
            this.cloudSession.processFrame(sceneView.getArFrame());
        }
    });
    scene.addOnUpdateListener(this::scene_OnUpdate);
    initializeSession();
}

Enfin, ajoutez le code suivant à votre méthode handleTap(). Ce code a pour effet d’attache une ancre spatiale Azure locale à la sphère noire que vous placez dans le monde réel.

protected void handleTap(HitResult hitResult, Plane plane, MotionEvent motionEvent) {
    synchronized (this.syncTaps) {
        if (this.tapExecuted) {
            return;
        }

        this.tapExecuted = true;
    }

    this.anchorNode = new AnchorNode();
    this.anchorNode.setAnchor(hitResult.createAnchor());
    CloudSpatialAnchor cloudAnchor = new CloudSpatialAnchor();
    cloudAnchor.setLocalAnchor(this.anchorNode.getAnchor());

    MaterialFactory.makeOpaqueWithColor(this, new Color(
            this.recommendedSessionProgress,
            this.recommendedSessionProgress,
            this.recommendedSessionProgress))
            .thenAccept(material -> {
                this.nodeRenderable = ShapeFactory.makeSphere(0.1f, new Vector3(0.0f, 0.15f, 0.0f), material);
                this.anchorNode.setRenderable(nodeRenderable);
                this.anchorNode.setParent(arFragment.getArSceneView().getScene());
            });
}

Redéployez une nouvelle fois votre application. Déplacez votre appareil, appuyez sur l’écran, puis placez une sphère noire. Cette fois, votre code crée une ancre spatiale Azure locale et l’attache à votre sphère.

Avant de poursuivre, vous devez créer un compte Azure Spatial Anchors pour obtenir l’identificateur, la clé et le domaine du compte, si vous ne les avez pas. Pour vous en procurer, suivez les instructions dans la section ci-dessous.

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.

Charger votre ancre locale dans le cloud

Une fois que vous disposez de l’identificateur, de la clé et du domaine du compte Azure Spatial Anchors, revenez dans app\java\<PackageName>\MainActivity et ajoutez-y les importations suivantes :

import com.microsoft.azure.spatialanchors.SessionLogLevel;

import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

Ajoutez ensuite les variables de membre suivantes dans votre classe MainActivity :

private boolean sessionInitialized = false;

private String anchorId = null;
private boolean scanningForUpload = false;
private final Object syncSessionProgress = new Object();
private ExecutorService executorService = Executors.newSingleThreadExecutor();

À présent, ajoutez le code suivant à votre méthode initializeSession(). En premier lieu, ce code permet à votre application de surveiller la progression du kit SDK Azure Spatial Anchors à mesure qu’il collecte des images à partir de votre flux de caméra. Pendant ce temps, la couleur de votre sphère commence à passer de sa couleur noire initiale au gris. Ensuite, elle prend la couleur blanche une fois que le nombre d’images collectées est suffisant pour soumettre l’ancre au cloud. En second lieu, ce code fournit les informations d’identification nécessaires pour communiquer avec le back-end cloud. Voici où vous allez configurer votre application pour qu’elle utilise l’identificateur, la clé et le domaine de votre compte. Vous les avez copiés dans un éditeur de texte pendant la configuration de la ressource Spatial Anchors.

private void initializeSession() {
    if (sceneView.getSession() == null) {
        //Early return if the ARCore Session is still being set up
        return;
    }

    if (this.cloudSession != null) {
        this.cloudSession.close();
    }
    this.cloudSession = new CloudSpatialAnchorSession();
    this.cloudSession.setSession(sceneView.getSession());
    this.cloudSession.setLogLevel(SessionLogLevel.Information);
    this.cloudSession.addOnLogDebugListener(args -> Log.d("ASAInfo", args.getMessage()));
    this.cloudSession.addErrorListener(args -> Log.e("ASAError", String.format("%s: %s", args.getErrorCode().name(), args.getErrorMessage())));

    sessionInitialized = true;

    this.cloudSession.addSessionUpdatedListener(args -> {
        synchronized (this.syncSessionProgress) {
            this.recommendedSessionProgress = args.getStatus().getRecommendedForCreateProgress();
            Log.i("ASAInfo", String.format("Session progress: %f", this.recommendedSessionProgress));
            if (!this.scanningForUpload) {
                return;
            }
        }

        runOnUiThread(() -> {
            synchronized (this.syncSessionProgress) {
                MaterialFactory.makeOpaqueWithColor(this, new Color(
                        this.recommendedSessionProgress,
                        this.recommendedSessionProgress,
                        this.recommendedSessionProgress))
                        .thenAccept(material -> {
                            this.nodeRenderable.setMaterial(material);
                        });
            }
        });
    });

    this.cloudSession.getConfiguration().setAccountId(/* Copy your account Identifier in here */);
    this.cloudSession.getConfiguration().setAccountKey(/* Copy your account Key in here */);
    this.cloudSession.getConfiguration().setAccountDomain(/* Copy your account Domain in here */);
    this.cloudSession.start();
}

Ensuite, ajoutez la méthode uploadCloudAnchorAsync() suivante à l’intérieur de votre classe mainActivity. Une fois appelée, cette méthode attend de manière asynchrone que le nombre d’images collectées à partir de votre appareil soit suffisant. Dès lors, elle fait passer la couleur de votre sphère au jaune et commence à charger l’ancre spatiale Azure locale dans le cloud. À l’issue du chargement, le code retourne un identificateur d’ancre.

private CompletableFuture<String> uploadCloudAnchorAsync(CloudSpatialAnchor anchor) {
    synchronized (this.syncSessionProgress) {
        this.scanningForUpload = true;
    }


    return CompletableFuture.runAsync(() -> {
        try {
            float currentSessionProgress;
            do {
                synchronized (this.syncSessionProgress) {
                    currentSessionProgress = this.recommendedSessionProgress;
                }
                if (currentSessionProgress < 1.0) {
                    Thread.sleep(500);
                }
            }
            while (currentSessionProgress < 1.0);

            synchronized (this.syncSessionProgress) {
                this.scanningForUpload = false;
            }
            runOnUiThread(() -> {
                MaterialFactory.makeOpaqueWithColor(this, new Color(android.graphics.Color.YELLOW))
                        .thenAccept(yellowMaterial -> {
                            this.nodeRenderable.setMaterial(yellowMaterial);
                        });
            });

            this.cloudSession.createAnchorAsync(anchor).get();
        } catch (InterruptedException | ExecutionException e) {
            Log.e("ASAError", e.toString());
            throw new RuntimeException(e);
        }
    }, executorService).thenApply(ignore -> anchor.getIdentifier());
}

Enfin, raccordez tout ensemble. Dans votre méthode handleTap(), ajoutez le code suivant. Il appelle la méthode uploadCloudAnchorAsync() aussitôt que la sphère est créée. Une fois la méthode retournée, le code ci-dessous apporte une dernière mise à jour à la sphère en lui donnant la couleur bleue.

protected void handleTap(HitResult hitResult, Plane plane, MotionEvent motionEvent) {
    synchronized (this.syncTaps) {
        if (this.tapExecuted) {
            return;
        }

        this.tapExecuted = true;
    }

    this.anchorNode = new AnchorNode();
    this.anchorNode.setAnchor(hitResult.createAnchor());
    CloudSpatialAnchor cloudAnchor = new CloudSpatialAnchor();
    cloudAnchor.setLocalAnchor(this.anchorNode.getAnchor());

    MaterialFactory.makeOpaqueWithColor(this, new Color(
            this.recommendedSessionProgress,
            this.recommendedSessionProgress,
            this.recommendedSessionProgress))
            .thenAccept(material -> {
                this.nodeRenderable = ShapeFactory.makeSphere(0.1f, new Vector3(0.0f, 0.15f, 0.0f), material);
                this.anchorNode.setRenderable(nodeRenderable);
                this.anchorNode.setParent(arFragment.getArSceneView().getScene());
            });


    uploadCloudAnchorAsync(cloudAnchor)
            .thenAccept(id -> {
                this.anchorId = id;
                Log.i("ASAInfo", String.format("Cloud Anchor created: %s", this.anchorId));
                runOnUiThread(() -> {
                    MaterialFactory.makeOpaqueWithColor(this, new Color(android.graphics.Color.BLUE))
                            .thenAccept(blueMaterial -> {
                                this.nodeRenderable.setMaterial(blueMaterial);
                                synchronized (this.syncTaps) {
                                    this.tapExecuted = false;
                                }
                            });
                });
            });
}

Redéployez une nouvelle fois votre application. Déplacez votre appareil, appuyez sur l’écran, puis placez la sphère. Cette fois, la sphère change de couleur, passant du noir à blanc à mesure que les images de la caméra sont collectées. Une fois que le nombre d’images est suffisant, la sphère devient jaune et le chargement cloud débute. Assurez-vous que votre téléphone est connecté à Internet. À l’issue du chargement, la sphère passe au bleu. Si vous le souhaitez, vous pouvez aussi surveiller la fenêtre Logcat dans Android Studio pour voir les messages de journal envoyés par votre application. Des exemples de messages qui seraient enregistrés incluent la progression de la session pendant la capture de trames et l’identificateur d’ancre que le cloud renvoie une fois le téléchargement terminé.

Notes

Si vous ne voyez pas la valeur de recommendedSessionProgress (dans vos journaux de débogage comme Session progress) modification, assurez-vous que vous déplacez et faites pivoter votre téléphone autour de la sphère que vous avez placée.

Localiser l’ancre spatiale cloud

Une fois que votre ancre est chargée dans le cloud, vous pouvez essayer de la localiser à nouveau. Pour commencer, ajoutez les importations suivantes à votre code.

import java.util.concurrent.Executors;

import com.microsoft.azure.spatialanchors.AnchorLocateCriteria;
import com.microsoft.azure.spatialanchors.LocateAnchorStatus;

Ensuite, ajoutez le code suivant à votre méthode handleTap(). Ce code a pour effet de :

  • Supprimer la sphère bleue existante de l’écran.
  • Réinitialiser la session Azure Spatial Anchors. Cette action garantit que l’ancre à localiser provient du cloud et qu’il ne s’agit pas de l’ancre locale que vous avez créée.
  • Émettez une requête pour l’ancre que vous avez chargée dans le cloud.
protected void handleTap(HitResult hitResult, Plane plane, MotionEvent motionEvent) {
    synchronized (this.syncTaps) {
        if (this.tapExecuted) {
            return;
        }

        this.tapExecuted = true;
    }

    if (this.anchorId != null) {
        this.anchorNode.getAnchor().detach();
        this.anchorNode.setParent(null);
        this.anchorNode = null;
        initializeSession();
        AnchorLocateCriteria criteria = new AnchorLocateCriteria();
        criteria.setIdentifiers(new String[]{this.anchorId});
        cloudSession.createWatcher(criteria);
        return;
    }

    this.anchorNode = new AnchorNode();
    this.anchorNode.setAnchor(hitResult.createAnchor());
    CloudSpatialAnchor cloudAnchor = new CloudSpatialAnchor();
    cloudAnchor.setLocalAnchor(this.anchorNode.getAnchor());

    MaterialFactory.makeOpaqueWithColor(this, new Color(
            this.recommendedSessionProgress,
            this.recommendedSessionProgress,
            this.recommendedSessionProgress))
            .thenAccept(material -> {
                this.nodeRenderable = ShapeFactory.makeSphere(0.1f, new Vector3(0.0f, 0.15f, 0.0f), material);
                this.anchorNode.setRenderable(nodeRenderable);
                this.anchorNode.setParent(arFragment.getArSceneView().getScene());
            });


    uploadCloudAnchorAsync(cloudAnchor)
            .thenAccept(id -> {
                this.anchorId = id;
                Log.i("ASAInfo", String.format("Cloud Anchor created: %s", this.anchorId));
                runOnUiThread(() -> {
                    MaterialFactory.makeOpaqueWithColor(this, new Color(android.graphics.Color.BLUE))
                            .thenAccept(blueMaterial -> {
                                this.nodeRenderable.setMaterial(blueMaterial);
                                synchronized (this.syncTaps) {
                                    this.tapExecuted = false;
                                }
                            });
                });
            });
}

Maintenant, raccordez le code qui sera appelé dès que l’ancre recherchée sera localisée. À l’intérieur de votre méthode initializeSession(), ajoutez le code suivant. Cet extrait de compte crée et place une sphère verte une fois que l’ancre spatiale cloud est localisée. Il réactive également les appuis sur l’écran, ce qui vous permet de répéter une nouvelle fois le scénario dans son intégralité : création, chargement et relocalisation d’une nouvelle ancre.

private void initializeSession() {
    if (sceneView.getSession() == null) {
        //Early return if the ARCore Session is still being set up
        return;
    }

    if (this.cloudSession != null) {
        this.cloudSession.close();
    }
    this.cloudSession = new CloudSpatialAnchorSession();
    this.cloudSession.setSession(sceneView.getSession());
    this.cloudSession.setLogLevel(SessionLogLevel.Information);
    this.cloudSession.addOnLogDebugListener(args -> Log.d("ASAInfo", args.getMessage()));
    this.cloudSession.addErrorListener(args -> Log.e("ASAError", String.format("%s: %s", args.getErrorCode().name(), args.getErrorMessage())));

    sessionInitialized = true;

    this.cloudSession.addSessionUpdatedListener(args -> {
        synchronized (this.syncSessionProgress) {
            this.recommendedSessionProgress = args.getStatus().getRecommendedForCreateProgress();
            Log.i("ASAInfo", String.format("Session progress: %f", this.recommendedSessionProgress));
            if (!this.scanningForUpload) {
                return;
            }
        }

        runOnUiThread(() -> {
            synchronized (this.syncSessionProgress) {
                MaterialFactory.makeOpaqueWithColor(this, new Color(
                        this.recommendedSessionProgress,
                        this.recommendedSessionProgress,
                        this.recommendedSessionProgress))
                        .thenAccept(material -> {
                            this.nodeRenderable.setMaterial(material);
                        });
            }
        });
    });

    this.cloudSession.addAnchorLocatedListener(args -> {
        if (args.getStatus() == LocateAnchorStatus.Located) {
            runOnUiThread(() -> {
                this.anchorNode = new AnchorNode();
                this.anchorNode.setAnchor(args.getAnchor().getLocalAnchor());
                MaterialFactory.makeOpaqueWithColor(this, new Color(android.graphics.Color.GREEN))
                        .thenAccept(greenMaterial -> {
                            this.nodeRenderable = ShapeFactory.makeSphere(0.1f, new Vector3(0.0f, 0.15f, 0.0f), greenMaterial);
                            this.anchorNode.setRenderable(nodeRenderable);
                            this.anchorNode.setParent(arFragment.getArSceneView().getScene());

                            this.anchorId = null;
                            synchronized (this.syncTaps) {
                                this.tapExecuted = false;
                            }
                        });
            });
        }
    });

    this.cloudSession.getConfiguration().setAccountId(/* Copy your account Identifier in here */);
    this.cloudSession.getConfiguration().setAccountKey(/* Copy your account Key in here */);
    this.cloudSession.getConfiguration().setAccountDomain(/* Copy your account Domain in here */);
    this.cloudSession.start();
}

Et voilà ! Redéployez votre application une dernière fois pour essayer l’ensemble du scénario de bout en bout. Déplacez votre appareil, puis placez votre sphère noire. Continuez de déplacer votre appareil pour capturer les images de la caméra jusqu’à ce que la sphère devienne jaune. Votre ancre locale est alors chargée et la sphère devient bleue. Enfin, appuyez une nouvelle fois sur l’écran de façon à supprimer l’ancre locale, puis recherchez son équivalent cloud. Continuez de déplacer votre appareil jusqu’à ce que l’ancre spatiale cloud soit localisée. Une sphère verte doit alors apparaître à l’emplacement approprié, et vous pouvez effacer et répéter une nouvelle fois l’ensemble du scénario.

Regroupement

Voici comment le fichier de classe MainActivity 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.

package com.example.myfirstapp;

import android.os.Bundle;
import android.util.Log;
import android.view.MotionEvent;

import androidx.appcompat.app.AppCompatActivity;

import com.google.ar.core.HitResult;
import com.google.ar.core.Plane;
import com.google.ar.sceneform.AnchorNode;
import com.google.ar.sceneform.ArSceneView;
import com.google.ar.sceneform.FrameTime;
import com.google.ar.sceneform.Scene;
import com.google.ar.sceneform.math.Vector3;
import com.google.ar.sceneform.rendering.Color;
import com.google.ar.sceneform.rendering.MaterialFactory;
import com.google.ar.sceneform.rendering.Renderable;
import com.google.ar.sceneform.rendering.ShapeFactory;
import com.google.ar.sceneform.ux.ArFragment;

import com.microsoft.azure.spatialanchors.AnchorLocateCriteria;
import com.microsoft.azure.spatialanchors.CloudSpatialAnchor;
import com.microsoft.azure.spatialanchors.CloudSpatialAnchorSession;
import com.microsoft.azure.spatialanchors.LocateAnchorStatus;
import com.microsoft.azure.spatialanchors.SessionLogLevel;

import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class MainActivity extends AppCompatActivity {

    private boolean tapExecuted = false;
    private final Object syncTaps = new Object();
    private ArFragment arFragment;
    private AnchorNode anchorNode;
    private Renderable nodeRenderable = null;
    private float recommendedSessionProgress = 0f;

    private ArSceneView sceneView;
    private CloudSpatialAnchorSession cloudSession;
    private boolean sessionInitialized = false;

    private String anchorId = null;
    private boolean scanningForUpload = false;
    private final Object syncSessionProgress = new Object();
    private ExecutorService executorService = Executors.newSingleThreadExecutor();

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        this.arFragment = (ArFragment) getSupportFragmentManager().findFragmentById(R.id.ux_fragment);
        this.arFragment.setOnTapArPlaneListener(this::handleTap);

        this.sceneView = arFragment.getArSceneView();
        Scene scene = sceneView.getScene();
        scene.addOnUpdateListener(frameTime -> {
            if (this.cloudSession != null) {
                this.cloudSession.processFrame(sceneView.getArFrame());
            }
        });
        scene.addOnUpdateListener(this::scene_OnUpdate);
        initializeSession();
    }

    // <scene_OnUpdate>
    private void scene_OnUpdate(FrameTime frameTime) {
        if (!sessionInitialized) {
            //retry if initializeSession did an early return due to ARCore Session not yet available (i.e. sceneView.getSession() == null)
            initializeSession();
        }
    }
    // </scene_OnUpdate>

    // <initializeSession>
    private void initializeSession() {
        if (sceneView.getSession() == null) {
            //Early return if the ARCore Session is still being set up
            return;
        }

        if (this.cloudSession != null) {
            this.cloudSession.close();
        }
        this.cloudSession = new CloudSpatialAnchorSession();
        this.cloudSession.setSession(sceneView.getSession());
        this.cloudSession.setLogLevel(SessionLogLevel.Information);
        this.cloudSession.addOnLogDebugListener(args -> Log.d("ASAInfo", args.getMessage()));
        this.cloudSession.addErrorListener(args -> Log.e("ASAError", String.format("%s: %s", args.getErrorCode().name(), args.getErrorMessage())));

        sessionInitialized = true;

        this.cloudSession.addSessionUpdatedListener(args -> {
            synchronized (this.syncSessionProgress) {
                this.recommendedSessionProgress = args.getStatus().getRecommendedForCreateProgress();
                Log.i("ASAInfo", String.format("Session progress: %f", this.recommendedSessionProgress));
                if (!this.scanningForUpload) {
                    return;
                }
            }

            runOnUiThread(() -> {
                synchronized (this.syncSessionProgress) {
                    MaterialFactory.makeOpaqueWithColor(this, new Color(
                            this.recommendedSessionProgress,
                            this.recommendedSessionProgress,
                            this.recommendedSessionProgress))
                            .thenAccept(material -> {
                                this.nodeRenderable.setMaterial(material);
                            });
                }
            });
        });

        this.cloudSession.addAnchorLocatedListener(args -> {
            if (args.getStatus() == LocateAnchorStatus.Located) {
                runOnUiThread(() -> {
                    this.anchorNode = new AnchorNode();
                    this.anchorNode.setAnchor(args.getAnchor().getLocalAnchor());
                    MaterialFactory.makeOpaqueWithColor(this, new Color(android.graphics.Color.GREEN))
                            .thenAccept(greenMaterial -> {
                                this.nodeRenderable = ShapeFactory.makeSphere(0.1f, new Vector3(0.0f, 0.15f, 0.0f), greenMaterial);
                                this.anchorNode.setRenderable(nodeRenderable);
                                this.anchorNode.setParent(arFragment.getArSceneView().getScene());

                                this.anchorId = null;
                                synchronized (this.syncTaps) {
                                    this.tapExecuted = false;
                                }
                            });
                });
            }
        });

        this.cloudSession.getConfiguration().setAccountId(/* Copy your account Identifier in here */);
        this.cloudSession.getConfiguration().setAccountKey(/* Copy your account Key in here */);
        this.cloudSession.getConfiguration().setAccountDomain(/* Copy your account Domain in here */);
        this.cloudSession.start();
    }
    // </initializeSession>

    // <handleTap>
    protected void handleTap(HitResult hitResult, Plane plane, MotionEvent motionEvent) {
        synchronized (this.syncTaps) {
            if (this.tapExecuted) {
                return;
            }

            this.tapExecuted = true;
        }

        if (this.anchorId != null) {
            this.anchorNode.getAnchor().detach();
            this.anchorNode.setParent(null);
            this.anchorNode = null;
            initializeSession();
            AnchorLocateCriteria criteria = new AnchorLocateCriteria();
            criteria.setIdentifiers(new String[]{this.anchorId});
            cloudSession.createWatcher(criteria);
            return;
        }

        this.anchorNode = new AnchorNode();
        this.anchorNode.setAnchor(hitResult.createAnchor());
        CloudSpatialAnchor cloudAnchor = new CloudSpatialAnchor();
        cloudAnchor.setLocalAnchor(this.anchorNode.getAnchor());

        MaterialFactory.makeOpaqueWithColor(this, new Color(
                this.recommendedSessionProgress,
                this.recommendedSessionProgress,
                this.recommendedSessionProgress))
                .thenAccept(material -> {
                    this.nodeRenderable = ShapeFactory.makeSphere(0.1f, new Vector3(0.0f, 0.15f, 0.0f), material);
                    this.anchorNode.setRenderable(nodeRenderable);
                    this.anchorNode.setParent(arFragment.getArSceneView().getScene());
                });


        uploadCloudAnchorAsync(cloudAnchor)
                .thenAccept(id -> {
                    this.anchorId = id;
                    Log.i("ASAInfo", String.format("Cloud Anchor created: %s", this.anchorId));
                    runOnUiThread(() -> {
                        MaterialFactory.makeOpaqueWithColor(this, new Color(android.graphics.Color.BLUE))
                                .thenAccept(blueMaterial -> {
                                    this.nodeRenderable.setMaterial(blueMaterial);
                                    synchronized (this.syncTaps) {
                                        this.tapExecuted = false;
                                    }
                                });
                    });
                });
    }
    // </handleTap>

    // <uploadCloudAnchorAsync>
    private CompletableFuture<String> uploadCloudAnchorAsync(CloudSpatialAnchor anchor) {
        synchronized (this.syncSessionProgress) {
            this.scanningForUpload = true;
        }


        return CompletableFuture.runAsync(() -> {
            try {
                float currentSessionProgress;
                do {
                    synchronized (this.syncSessionProgress) {
                        currentSessionProgress = this.recommendedSessionProgress;
                    }
                    if (currentSessionProgress < 1.0) {
                        Thread.sleep(500);
                    }
                }
                while (currentSessionProgress < 1.0);

                synchronized (this.syncSessionProgress) {
                    this.scanningForUpload = false;
                }
                runOnUiThread(() -> {
                    MaterialFactory.makeOpaqueWithColor(this, new Color(android.graphics.Color.YELLOW))
                            .thenAccept(yellowMaterial -> {
                                this.nodeRenderable.setMaterial(yellowMaterial);
                            });
                });

                this.cloudSession.createAnchorAsync(anchor).get();
            } catch (InterruptedException | ExecutionException e) {
                Log.e("ASAError", e.toString());
                throw new RuntimeException(e);
            }
        }, executorService).thenApply(ignore -> anchor.getIdentifier());
    }
    // </uploadCloudAnchorAsync>

}

Étapes suivantes

Dans ce tutoriel, vous avez appris à créer une application Android qui intègre la fonctionnalité ARCore avec Azure Spatial Anchors. Pour en savoir plus sur la bibliothèque Azure Spatial Anchors, passez à notre guide sur la création et la localisation des ancres.