Samouczek: instrukcje krok po kroku dotyczące tworzenia nowej aplikacji dla systemu Android przy użyciu usługi Azure Spatial Anchors

W tym samouczku pokazano, jak utworzyć nową aplikację dla systemu Android, która integruje funkcje ARCore z usługą Azure Spatial Anchors.

Wymagania wstępne

Aby ukończyć kroki tego samouczka, upewnij się, że dysponujesz następującymi elementami:

Wprowadzenie

Uruchom program Android Studio. W oknie Witamy w programie Android Studio kliknij pozycję Start a new Android Studio project (Rozpocznij nowy projekt programu Android Studio).

  1. Wybierz pozycję Plik nowy> projekt.
  2. W oknie Tworzenie nowego projektu w sekcji Telefon i tabletu wybierz pozycję Puste działanie, a następnie kliknij przycisk Dalej.
  3. W oknie Nowy projekt — puste działanie zmień następujące wartości:
    • Zmień nazwę, nazwę pakietu i lokalizację zapisz na żądane wartości
    • Ustaw język na wartość Java
    • Ustaw minimalny poziom interfejsu API na API 26: Android 8.0 (Oreo)
    • Pozostaw inne opcje, ponieważ są
    • Kliknij przycisk Zakończ.
  4. Instalator składników zostanie uruchomiony. Po zakończeniu przetwarzania program Android Studio otworzy środowisko IDE.

Android Studio - New Project

Wypróbowywanie

Aby przetestować nową aplikację, połącz urządzenie z obsługą deweloperów z maszyną deweloperów za pomocą kabla USB. W prawym górnym rogu programu Android Studio wybierz połączone urządzenie i kliknij ikonę Uruchom aplikację . Program Android Studio instaluje aplikację na połączonym urządzeniu i uruchamia ją. W aplikacji uruchomionej na urządzeniu powinna zostać wyświetlona wartość "Hello World!". Kliknij pozycję Uruchom zatrzymaj> aplikację. Android Studio - Run

Integrowanie aplikacji ARCore

ARCore to platforma Firmy Google do tworzenia środowisk rzeczywistości rozszerzonej, umożliwiając urządzeniu śledzenie jego pozycji w miarę poruszania się i budowania własnego zrozumienia świata rzeczywistego.

Zmodyfikuj app\manifests\AndroidManifest.xml , aby uwzględnić następujące wpisy w węźle głównym <manifest> . Ten fragment kodu wykonuje kilka czynności:

  • Umożliwi to aplikacji dostęp do aparatu urządzenia.
  • Zapewni to również, że Aplikacja będzie widoczna tylko w Sklepie Google Play na urządzeniach obsługujących arcore.
  • Skonfiguruje sklep Google Play, aby pobrać i zainstalować aplikację ARCore, jeśli nie została jeszcze zainstalowana, po zainstalowaniu aplikacji.
<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>

Zmodyfikuj Gradle Scripts\build.gradle (Module: app) , aby uwzględnić następujący wpis. Ten kod zapewni, że aplikacja będzie przeznaczona dla arcore w wersji 1.25. Po tej zmianie możesz otrzymać powiadomienie z aplikacji Gradle z prośbą o zsynchronizowanie: kliknij pozycję Synchronizuj teraz.

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

Integrowanie programu Sceneform

Funkcja Sceneform ułatwia renderowanie realistycznych scen 3D w aplikacjach rzeczywistości rozszerzonej bez konieczności nauki języka OpenGL.

Zmodyfikuj Gradle Scripts\build.gradle (Module: app) , aby uwzględnić następujące wpisy. Ten kod umożliwi aplikacji używanie konstrukcji językowych z języka Java 8, co Sceneform wymaga. Zapewni to również, że aplikacja będzie przeznaczona Sceneform dla wersji 1.15. Po tej zmianie możesz otrzymać powiadomienie z aplikacji Gradle z prośbą o zsynchronizowanie: kliknij pozycję Synchronizuj teraz.

android {
    ...

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

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

Otwórz plik app\res\layout\activity_main.xmli zastąp istniejący element Hello Wolrd <TextView ... /> następującym elementem ArFragment. Ten kod spowoduje wyświetlenie kanału informacyjnego aparatu na ekranie, dzięki czemu ARCore będzie śledzić położenie urządzenia podczas jego przesuwania.

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

Uwaga

Aby wyświetlić nieprzetworzone dane XML głównego działania, kliknij przycisk "Kod" lub "Podziel" w prawym górnym rogu programu Android Studio.

Ponownie wdróż aplikację na urządzeniu, aby ją ponownie zweryfikować. Tym razem należy poprosić o uprawnienia aparatu. Po zatwierdzeniu na ekranie powinno zostać wyświetlone renderowanie kanału informacyjnego aparatu.

Umieszczanie obiektu w świecie rzeczywistym

Utwórzmy i umieśćmy obiekt przy użyciu aplikacji. Najpierw dodaj następujące importy do elementu 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;

Następnie dodaj następujące zmienne składowe do klasy 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;

Następnie dodaj następujący kod do metody app\java\<PackageName>\MainActivityonCreate() . Ten kod spowoduje podłączenie odbiornika o nazwie handleTap(), który wykryje, kiedy użytkownik naciągnie ekran na urządzeniu. Jeśli naciśnięcie stanie się na rzeczywistej powierzchni, która została już rozpoznana przez śledzenie ARCore, odbiornik zostanie uruchomiony.

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

Na koniec dodaj następującą handleTap() metodę, która będzie wiązać wszystko razem. Spowoduje to utworzenie sfery i umieszczenie jej w lokalizacji naciśniętej. Sfera początkowo będzie czarna, ponieważ this.recommendedSessionProgress jest ustawiona na zero w tej chwili. Ta wartość zostanie później skorygowana.

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

Ponownie wdróż aplikację na urządzeniu, aby ją ponownie zweryfikować. Tym razem możesz poruszać się po urządzeniu, aby uzyskać arcore, aby rozpocząć rozpoznawanie środowiska. Następnie naciśnij ekran, aby utworzyć i umieścić czarną kulę na wybranej powierzchni.

Dołączanie lokalnej usługi Azure Spatial Anchor

Zmodyfikuj Gradle Scripts\build.gradle (Module: app) , aby uwzględnić następujący wpis. Ten przykładowy fragment kodu jest przeznaczony dla zestawu Azure Spatial Anchors SDK w wersji 2.10.2. Należy pamiętać, że zestaw SDK w wersji 2.7.0 jest obecnie minimalną obsługiwaną wersją, a odwołanie się do najnowszej wersji usługi Azure Spatial Anchors również powinno działać. Zalecamy korzystanie z najnowszej wersji zestawu SDK usługi Azure Spatial Anchors. Informacje o wersji zestawu SDK można znaleźć tutaj.

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

Jeśli używasz zestawu Azure Spatial Anchors SDK 2.10.0 lub nowszego, dołącz następujący wpis w sekcji repozytoriów pliku projektu settings.gradle . Będzie to adres URL kanału informacyjnego pakietu Maven, który hostuje pakiety systemu Android usługi Azure Spatial Anchors dla zestawu SDK 2.10.0 lub nowszego:

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

Kliknij prawym przyciskiem myszy pozycję app\java\<PackageName>->Nowa klasa Java>. Ustaw wartość Nazwa na MyFirstApp, a następnie wybierz pozycję Klasa. Zostanie utworzony plik o nazwie MyFirstApp.java . Dodaj do niego następujący import:

import com.microsoft.CloudServices;

Zdefiniuj android.app.Application jako swoją superklasę.

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

Następnie dodaj następujący kod wewnątrz nowej MyFirstApp klasy, co zapewni, że usługa Azure Spatial Anchors zostanie zainicjowana z kontekstem aplikacji.

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

Teraz zmodyfikuj app\manifests\AndroidManifest.xml , aby uwzględnić następujący wpis w węźle głównym <application> . Ten kod spowoduje podłączenie klasy Aplikacji utworzonej w aplikacji.

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

app\java\<PackageName>\MainActivityWróć do pliku , dodaj do niego następujące importy:

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;

Następnie dodaj następujące zmienne składowe do klasy MainActivity :

private float recommendedSessionProgress = 0f;

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

Następnie dodajmy następującą initializeSession() metodę wewnątrz mainActivity klasy. Po wywołaniu upewni się, że sesja usługi Azure Spatial Anchors zostanie utworzona i prawidłowo zainicjowana podczas uruchamiania aplikacji. Ten kod zapewnia, że sesja widoku sceny przekazana do sesji usługi ASA za pośrednictwem cloudSession.setSession wywołania nie ma wartości null, mając wczesny zwrot.

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

Ponieważ initializeSession() może wykonać wczesny zwrot, jeśli sesja sceneView nie jest jeszcze skonfigurowana (tj. jeśli sceneView.getSession() ma wartość null), dodamy wywołanie onUpdate, aby upewnić się, że sesja usługi ASA zostanie zainicjowana po utworzeniu sesji sceneView.

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

Teraz podłączmy metodę initializeSession() i scene_OnUpdate(...) do metody onCreate() . Ponadto upewnimy się, że ramki z kanału informacyjnego aparatu są wysyłane do zestawu SDK usługi Azure Spatial Anchors do przetwarzania.

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

Na koniec dodaj następujący kod do metody handleTap() . Dołączy on lokalną usługę Azure Spatial Anchor do czarnej kuli, którą umieszczamy w świecie rzeczywistym.

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

Ponownie wdróż aplikację. Poruszaj się po urządzeniu, naciśnij ekran i umieść czarną sferę. Tym razem jednak kod będzie tworzyć i dołączać lokalną usługę Azure Spatial Anchor do swojej sfery.

Przed kontynuowaniem należy utworzyć konto usługi Azure Spatial Anchors, aby uzyskać identyfikator konta, klucz i domenę, jeśli jeszcze ich nie masz. Postępuj zgodnie z poniższą sekcją, aby je uzyskać.

Tworzenie zasobu usługi Spatial Anchors

Przejdź do portalu Azure Portal.

W okienku po lewej stronie wybierz pozycję Utwórz zasób.

Użyj pola wyszukiwania, aby wyszukać usługi Spatial Anchors.

Screenshot showing the results of a search for Spatial Anchors.

Wybierz pozycję Zakotwiczenia przestrzenne, a następnie wybierz pozycję Utwórz.

W okienku Konto usługi Spatial Anchors wykonaj następujące czynności:

  • Wprowadź unikatową nazwę zasobu przy użyciu zwykłych znaków alfanumerycznych.

  • Wybierz subskrypcję, do której chcesz dołączyć zasób.

  • Utwórz grupę zasobów, wybierając pozycję Utwórz nową. Nadaj jej nazwę myResourceGroup, a następnie wybierz przycisk OK.

    Grupa zasobów to logiczny kontener, w którym są wdrażane i zarządzane zasoby platformy Azure, takie jak aplikacje internetowe, bazy danych i konta magazynu. Na przykład można później usunąć całą grupę zasobów w jednym prostym kroku.

  • Wybierz lokalizację (region), w której ma być umieszczany zasób.

  • Wybierz pozycję Utwórz , aby rozpocząć tworzenie zasobu.

Screenshot of the Spatial Anchors pane for creating a resource.

Po utworzeniu zasobu witryna Azure Portal pokazuje, że wdrożenie zostało ukończone.

Screenshot showing that the resource deployment is complete.

Wybierz pozycję Przejdź do zasobu. Teraz możesz wyświetlić właściwości zasobu.

Skopiuj wartość identyfikatora konta zasobu do edytora tekstów do późniejszego użycia.

Screenshot of the resource properties pane.

Skopiuj również wartość domeny konta zasobu do edytora tekstów do późniejszego użycia.

Screenshot showing the resource's account domain value.

W obszarze Ustawienia wybierz pozycję Klucz dostępu. Skopiuj wartość Klucza podstawowego, Klucz konta, do edytora tekstów do późniejszego użycia.

Screenshot of the Keys pane for the account.

Przekazywanie kotwicy lokalnej do chmury

Po utworzeniu identyfikatora konta usługi Azure Spatial Anchors, klucza i domeny możemy wrócić app\java\<PackageName>\MainActivitydo elementu , dodać do niego następujące importy:

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;

Następnie dodaj następujące zmienne składowe do klasy MainActivity :

private boolean sessionInitialized = false;

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

Teraz dodaj następujący kod do metody initializeSession() . Najpierw ten kod umożliwi aplikacji monitorowanie postępu działania zestawu SDK usługi Azure Spatial Anchors podczas zbierania ramek z kanału informacyjnego aparatu. Tak jak to robi, kolor sfery zacznie się zmieniać z oryginalnego czarnego, na szary. Następnie zmieni kolor na biały po zebraniu wystarczającej liczby ramek w celu przesłania kotwicy do chmury. Po drugie ten kod dostarczy poświadczenia potrzebne do komunikowania się z zapleczem chmury. W tym miejscu skonfigurujesz aplikację tak, aby korzystała z identyfikatora konta, klucza i domeny. Skopiowano je do edytora tekstów podczas konfigurowania zasobu usługi 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();
}

Następnie dodaj następującą uploadCloudAnchorAsync() metodę wewnątrz mainActivity klasy. Po wywołaniu ta metoda będzie asynchronicznie czekać, aż z urządzenia zostanie zebrana wystarczająca liczba ramek. Gdy tylko tak się stanie, zmieni kolor sfery na żółty, a następnie rozpocznie przekazywanie lokalnej usługi Azure Spatial Anchor do chmury. Po zakończeniu przekazywania kod zwróci identyfikator kotwicy.

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

Na koniec połączmy wszystko razem. W metodzie handleTap() dodaj następujący kod. Spowoduje to wywołanie uploadCloudAnchorAsync() metody zaraz po utworzeniu sfery. Po powrocie metody poniższy kod wykona jedną ostateczną aktualizację do sfery, zmieniając kolor na niebieski.

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

Ponownie wdróż aplikację. Poruszaj się po urządzeniu, naciśnij ekran i umieść sferę. Tym razem jednak twoja sfera zmieni kolor z czarnego na biały, ponieważ zbierane są klatki aparatu. Po uzyskaniu wystarczającej liczby ramek sfera zmieni się na żółtą, a przekazywanie do chmury rozpocznie się. Upewnij się, że telefon jest połączony z Internetem. Po zakończeniu przekazywania sfera zmieni kolor na niebieski. Opcjonalnie możesz monitorować Logcat okno w programie Android Studio, aby wyświetlić komunikaty dziennika wysyłane przez aplikację. Przykłady komunikatów, które zostaną zarejestrowane, obejmują postęp sesji podczas przechwytywania ramek i identyfikator kotwicy zwracany przez chmurę po zakończeniu przekazywania.

Uwaga

Jeśli nie widzisz wartości recommendedSessionProgress (w dziennikach debugowania określanych jako Session progress) zmień, upewnij się, że przenosisz i obracasz telefon wokół umieszczonej sfery.

Lokalizowanie kotwicy przestrzennej chmury

Po przekazaniu kotwicy do chmury możemy spróbować go ponownie zlokalizować. Najpierw dodajmy następujące importy do kodu.

import java.util.concurrent.Executors;

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

Następnie dodajmy następujący kod do metody handleTap() . Ten kod:

  • Usuń naszą istniejącą niebieską sferę z ekranu.
  • Ponownie zainicjuj sesję usługi Azure Spatial Anchors. Ta akcja zapewni, że kotwica, którą znajdziemy, pochodzi z chmury zamiast utworzonej przez nas kotwicy lokalnej.
  • Wydaj zapytanie dotyczące kotwicy przekazanej do chmury.
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;
                                }
                            });
                });
            });
}

Teraz podłączmy kod, który zostanie wywołany, gdy znajduje się kotwica, dla którego wykonujemy zapytanie. initializeSession() Wewnątrz metody dodaj następujący kod. Ten fragment kodu utworzy i umieści zieloną sferę po zlokalizowaniu kotwicy przestrzennej chmury. Spowoduje to również ponowne naciśnięcie ekranu, dzięki czemu można powtórzyć cały scenariusz jeszcze raz: utworzyć inną kotwicę lokalną, przekazać ją i zlokalizować ponownie.

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

I już! Ponownie wdróż aplikację po raz ostatni, aby wypróbować cały scenariusz. Poruszaj się po urządzeniu i umieść czarną sferę. Następnie przenieś urządzenie, aby przechwycić klatki aparatu, aż sfera zmieni kolor na żółty. Twoja kotwica lokalna zostanie przekazana, a twoja sfera zmieni kolor na niebieski. Na koniec naciśnij ponownie ekran, aby lokalna kotwica została usunięta, a następnie wykonamy zapytanie dotyczące jego odpowiednika w chmurze. Kontynuuj przenoszenie urządzenia do momentu zlokalizowaniu kotwicy przestrzennej chmury. Zielona kula powinna pojawić się w prawidłowej lokalizacji i można spłukać i powtórzyć cały scenariusz ponownie.

Łączenie wszystkich elementów

Oto jak powinien wyglądać kompletny MainActivity plik klasy, po zebraniu wszystkich różnych elementów. Możesz użyć go jako odwołania do porównania z własnym plikiem i dostrzec, jeśli mogą istnieć jakiekolwiek różnice w lewo.

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>

}

Następne kroki

W tym samouczku pokazano, jak utworzyć nową aplikację dla systemu Android, która integruje funkcje ARCore z usługą Azure Spatial Anchors. Aby dowiedzieć się więcej o bibliotece usługi Azure Spatial Anchors, kontynuuj pracę z naszym przewodnikiem w sekcji dotyczącej sposobu tworzenia i znajdowania kotwic.