Kurz: Podrobné pokyny k vytvoření nové aplikace pro Android pomocí Azure Spatial Anchors

V tomto kurzu se dozvíte, jak vytvořit novou aplikaci pro Android, která integruje funkce ARCore se službou Azure Spatial Anchors.

Požadavky

Abyste mohli absolvovat tento kurz, ujistěte se, že máte následující:

Začínáme

Spusťte Android Studio. V okně Vítá vás Android Studio klikněte na Spustit nový projekt Android Studio.

  1. Vyberte Soubor nový> projekt.
  2. V okně Vytvořit nový projekt v části Telefon a Tablet zvolte Prázdnou aktivitu a klikněte na Další.
  3. V okně Nový projekt – Prázdná aktivita změňte následující hodnoty:
    • Změňte název, název balíčku a umístění Uložit na požadované hodnoty.
    • Nastavit jazyk je Java
    • Nastavení minimální úrovně rozhraní API na API 26: Android 8.0 (Oreo)
    • Ostatní možnosti ponechte tak, jak jsou.
    • Klikněte na Finish (Dokončit).
  4. Instalační program součásti se spustí. Po nějakém zpracování android Studio otevře integrované vývojové prostředí (IDE).

Android Studio - New Project

Vyzkoušení

Pokud chcete otestovat novou aplikaci, připojte zařízení s podporou vývojáře k vývojovému počítači pomocí kabelu USB. V pravém horním rohu Android Studia vyberte připojené zařízení a klikněte na ikonu Spustit aplikaci . Android Studio nainstaluje aplikaci na připojené zařízení a spustí ji. V aplikaci spuštěné na vašem zařízení by se teď měla zobrazit zpráva "Hello World!". Klikněte na Spustit a> zastavit aplikaci. Android Studio - Run

Integrace ARCore

ARCore je platforma Googlu pro vytváření prostředí rozšířené reality, která umožňuje vašemu zařízení sledovat svou pozici při pohybu a vytváří své vlastní porozumění skutečnému světu.

Upravte app\manifests\AndroidManifest.xml zahrnutí následujících položek do kořenového <manifest> uzlu. Tento fragment kódu dělá několik věcí:

  • Umožní vaší aplikaci přístup k fotoaparátu zařízení.
  • Zajistí také, aby vaše aplikace byla viditelná jenom v Obchodě Google Play pro zařízení, která podporují ARCore.
  • Nakonfiguruje Obchod Google Play tak, aby při instalaci aplikace stáhl a nainstaloval ARCore, pokud ještě není nainstalovaný.
<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>

Upravte Gradle Scripts\build.gradle (Module: app) tak, aby zahrnoval následující položku. Tento kód zajistí, že vaše aplikace cílí na ARCore verze 1.25. Po této změně se může od Gradle zobrazit oznámení s žádostí o synchronizaci: Klikněte na Synchronizovat.

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

Integrace formátu scény

Funkce Sceneform usnadňuje vykreslení realistických 3D scén v aplikacích Rozšířené reality, aniž byste se museli učit OpenGL.

Upravte Gradle Scripts\build.gradle (Module: app) tak, aby zahrnoval následující položky. Tento kód umožní vaší aplikaci používat jazykové konstrukty z Javy 8, což Sceneform vyžaduje. Zajistí také, že vaše aplikace cílí na Sceneform verzi 1.15. Po této změně se může od Gradle zobrazit oznámení s žádostí o synchronizaci: Klikněte na Synchronizovat.

android {
    ...

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

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

Otevřete soubor a nahraďte app\res\layout\activity_main.xmlstávající prvek Hello Wolrd <TextView ... /> následujícím prvkem ArFragment. Tento kód způsobí, že se na obrazovce zobrazí kanál fotoaparátu, který umožňuje ARCore sledovat polohu vašeho zařízení při pohybu.

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

Poznámka:

Pokud chcete zobrazit nezpracovaný xml hlavní aktivity, klikněte na tlačítko Kód nebo Rozdělit v pravém horním rohu Android Studia.

Znovu nasaďte aplikaci do zařízení, abyste ji ověřili ještě jednou. Tentokrát byste měli být požádáni o oprávnění kamery. Po schválení byste měli vidět vykreslení kanálu fotoaparátu na obrazovce.

Umístění objektu ve skutečném světě

Pojďme vytvořit a umístit objekt pomocí aplikace. Nejprve do své app\java\<PackageName>\MainActivitypoložky přidejte následující importy:

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;

Pak do MainActivity třídy přidejte následující členské proměnné:

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

Dále do své app\java\<PackageName>\MainActivityonCreate() metody přidejte následující kód. Tento kód připojí naslouchací proces s názvem handleTap(), který rozpozná, když uživatel klepne na obrazovku na vašem zařízení. Pokud se klepnutí stane na skutečném povrchu, který už sledování ARCore rozpoznal, spustí se naslouchací proces.

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

Nakonec přidejte následující handleTap() metodu, která spojí všechno dohromady. Vytvoří kouli a umístí ji na klepané místo. Sphere bude zpočátku černá, protože this.recommendedSessionProgress je nyní nastavena na nulu. Tato hodnota se později upraví.

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

Znovu nasaďte aplikaci do zařízení, abyste ji ověřili ještě jednou. Tentokrát se můžete pohybovat po zařízení a získat ARCore, abyste mohli začít rozpoznávat vaše prostředí. Potom klepnutím na obrazovku vytvořte a umístěte černou kouli na povrch podle svého výběru.

Připojení místního Azure Spatial Anchoru

Upravte Gradle Scripts\build.gradle (Module: app) tak, aby zahrnoval následující položku. Tento ukázkový fragment kódu cílí na sadu Azure Spatial Anchors SDK verze 2.10.2. Mějte na paměti, že sada SDK verze 2.7.0 je aktuálně minimální podporovaná verze a odkazování na novější verzi Azure Spatial Anchors by také mělo fungovat. Doporučujeme použít nejnovější verzi sady Azure Spatial Anchors SDK. Poznámky k verzi sady SDK najdete tady.

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

Pokud cílíte na sadu Azure Spatial Anchors SDK 2.10.0 nebo novější, uveďte následující položku v části úložiště souboru projektu settings.gradle . Bude to zahrnovat adresu URL kanálu balíčku Maven, který hostuje balíčky Azure Spatial Anchors pro Android pro SADU SDK 2.10.0 nebo novější:

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

Klikněte pravým tlačítkem myši na app\java\<PackageName>třídu ->New-Java>. Nastavte název na MyFirstApp a vyberte Třída. Vytvoří se soubor s názvem MyFirstApp.java . Přidejte do něj následující import:

import com.microsoft.CloudServices;

Definujte android.app.Application ji jako její nadtřídu.

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

Pak do nové MyFirstApp třídy přidejte následující kód, který zajistí inicializaci Azure Spatial Anchors s kontextem vaší aplikace.

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

Nyní upravte app\manifests\AndroidManifest.xml , aby zahrnovala následující položku do kořenového <application> uzlu. Tento kód připojí třídu aplikace, kterou jste vytvořili do aplikace.

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

Zpátky do app\java\<PackageName>\MainActivityněj přidejte následující 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;

Pak do MainActivity třídy přidejte následující členské proměnné:

private float recommendedSessionProgress = 0f;

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

V dalším kroku přidáme do třídy mainActivity následující initializeSession() metodu. Po zavolání se zajistí, že se během spuštění aplikace vytvoří a správně inicializuje relace Azure Spatial Anchors. Tento kód zajistí, že relace sceneview předaná do relace ASA prostřednictvím cloudSession.setSession volání nemá hodnotu null tím, že se brzy vrátí.

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

Vzhledem k tomu initializeSession() , že by mohlo dojít k předčasnému návratu, pokud relace sceneView ještě není nastavena (tj. pokud sceneView.getSession() je null), přidáme volání onUpdate, abychom se ujistili, že se relace ASA inicializuje po vytvoření relace 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();
    }
}

Teď pojďme připojit metodu initializeSession() a scene_OnUpdate(...) metodu k vaší onCreate() metodě. Také zajistíme, aby se snímky z vašeho kanálu fotoaparátu odesílaly do sady Azure Spatial Anchors SDK ke zpracování.

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

Nakonec do své handleTap() metody přidejte následující kód. Připojí místní Azure Spatial Anchor k černé kouli, kterou umísťujeme do reálného světa.

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

Znovu nasaďte aplikaci znovu. Pohybujte se po zařízení, klepněte na obrazovku a umístěte černou kouli. Tentokrát ale váš kód vytvoří a připojí místní Azure Spatial Anchor k vaší sphere.

Než budete pokračovat, budete muset vytvořit účet Azure Spatial Anchors, abyste získali identifikátor účtu, klíč a doménu, pokud ho ještě nemáte. Pokud je chcete získat, postupujte podle následující části.

Vytvoření prostředku Spatial Anchors

Přejděte na Azure Portal.

V levém podokně vyberte Vytvořit prostředek.

Pomocí vyhledávacího pole vyhledejte Spatial Anchors.

Screenshot showing the results of a search for Spatial Anchors.

Vyberte Spatial Anchors a pak vyberte Vytvořit.

V podokně Účet prostorových ukotvení postupujte takto:

  • Zadejte jedinečný název prostředku pomocí běžných alfanumerických znaků.

  • Vyberte předplatné, ke kterému chcete prostředek připojit.

  • Výběrem možnosti Vytvořit novou vytvořte skupinu prostředků. Pojmenujte ji myResourceGroup a pak vyberte OK.

    Skupina prostředků je logický kontejner, do kterého se nasazují a spravují prostředky Azure, jako jsou webové aplikace, databáze a účty úložiště. Později se například můžete rozhodnout odstranit celou skupinu prostředků v jednom jednoduchém kroku.

  • Vyberte umístění (oblast), do kterého chcete prostředek umístit.

  • Výběrem možnosti Vytvořit zahájíte vytváření prostředku.

Screenshot of the Spatial Anchors pane for creating a resource.

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

Screenshot showing that the resource deployment is complete.

Vyberte Přejít k prostředku. Teď můžete zobrazit vlastnosti prostředku.

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

Screenshot of the resource properties pane.

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

Screenshot showing the resource's account domain value.

V části Nastavení vyberte Přístupový klíč. Zkopírujte hodnotu primárního klíče Account Key (Klíč účtu) do textového editoru pro pozdější použití.

Screenshot of the Keys pane for the account.

Nahrání místního ukotvení do cloudu

Jakmile budete mít identifikátor účtu Azure Spatial Anchors, klíč a doménu, můžeme se k němu vrátit app\java\<PackageName>\MainActivitya přidat do něj následující 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;

Pak do MainActivity třídy přidejte následující členské proměnné:

private boolean sessionInitialized = false;

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

Nyní do své initializeSession() metody přidejte následující kód. Nejprve tento kód umožní vaší aplikaci sledovat průběh, který sada Azure Spatial Anchors SDK dělá, když shromažďuje snímky z kanálu fotoaparátu. Jak to dělá, barva koule se začne měnit z původní černé, na šedou. Jakmile se pak shromáždí dostatek snímků, aby se odeslalo ukotvení do cloudu, změní se na bílou. Za druhé tento kód poskytne přihlašovací údaje potřebné ke komunikaci s cloudovým back-endem. Tady nakonfigurujete aplikaci tak, aby používala identifikátor účtu, klíč a doménu. Zkopírovali jste je do textového editoru při nastavování prostředku 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();
}

Dále do třídy mainActivity přidejte následující uploadCloudAnchorAsync() metodu. Po zavolání bude tato metoda asynchronně čekat, dokud se z vašeho zařízení neshromáždí dostatek snímků. Jakmile k tomu dojde, přepne barva koule na žlutou a pak začne nahrávat místní Azure Spatial Anchor do cloudu. Po dokončení nahrávání vrátí kód identifikátor ukotvení.

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

Nakonec pojďme všechno spojit dohromady. Do metody handleTap() přidejte následující kód. Vyvolá vaši uploadCloudAnchorAsync() metodu hned po vytvoření koule. Jakmile metoda vrátí, kód níže provede jednu konečnou aktualizaci vaší koule a změní její barvu na modrou.

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

Znovu nasaďte aplikaci znovu. Pohybujte se po zařízení, klepněte na obrazovku a umístěte kouli. Tentokrát ale vaše koule změní barvu z černé na bílou, protože jsou shromažďovány snímky fotoaparátu. Jakmile budeme mít dostatek snímků, koule se změní na žlutou a nahrávání do cloudu se spustí. Zkontrolujte, jestli je telefon připojený k internetu. Po dokončení nahrávání se vaše koule změní na modrou. Volitelně můžete okno v Android Studiu monitorovat Logcat a zobrazit zprávy protokolu, které aplikace odesílá. Mezi příklady zpráv, které by se protokolovaly, patří průběh relace během zachytávání snímků a identifikátor ukotvení, který cloud vrátí po dokončení nahrávání.

Poznámka:

Pokud se vám nezobrazuje hodnota recommendedSessionProgress změny (v protokolech ladění označovaných jako Session progress) a ujistěte se, že telefon přesouváte i rotujete kolem koule, kterou jste umístili.

Vyhledání cloudové prostorové ukotvení

Jakmile se ukotvení nahraje do cloudu, můžeme se pokusit ho znovu vyhledat. Nejprve do kódu přidáme následující importy.

import java.util.concurrent.Executors;

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

Pak do metody handleTap() přidáme následující kód. Tento kód:

  • Odeberte z obrazovky naši existující modrou kouli.
  • Znovu inicializujeme naši relaci Azure Spatial Anchors. Tato akce zajistí, že ukotvení, které vyhledáme, pochází z cloudu místo místního ukotvení, které jsme vytvořili.
  • Zadejte dotaz na ukotvení, které jsme nahráli do cloudu.
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;
                                }
                            });
                });
            });
}

Teď pojďme zavolat kód, který se vyvolá, když se nachází ukotvení, na které se dotazujeme. Do metody initializeSession() přidejte následující kód. Tento fragment kódu vytvoří a umístí zelenou kouli po umístění cloudové prostorové kotvy. Povolí také opětovné klepnutí na obrazovku, takže můžete celý scénář zopakovat ještě jednou: vytvořte další místní ukotvení, nahrajte ho a znovu ho vyhledejte.

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

A je to! Jednou aplikaci znovu nasaďte , abyste si vyzkoušeli celý scénář až do konce. Pohybujte se po zařízení a umístěte černou kouli. Pak pokračujte v pohybu zařízení a zachytávejte snímky fotoaparátu, dokud se kula nezbarví žlutě. Vaše místní kotva se nahraje a vaše koule se změní na modrou. Nakonec klepněte na obrazovku ještě jednou, aby se vaše místní ukotvení odebralo, a pak provedeme dotaz na jeho cloudový protějšek. Pokračujte v přesouvání zařízení, dokud se nenachází cloudové prostorové ukotvení. Zelená koule by se měla objevit ve správném umístění a můžete proplachovat a opakovat celý scénář znovu.

Spojení všeho dohromady

Tady je postup, jak by měl soubor kompletní MainActivity třídy vypadat po vytvoření všech různých prvků. Můžete ho použít jako referenci k porovnání s vlastním souborem a zjistit, jestli vám můžou zůstat nějaké rozdíly.

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>

}

Další kroky

V tomto kurzu jste viděli, jak vytvořit novou aplikaci pro Android, která integruje funkce ARCore se službou Azure Spatial Anchors. Další informace o knihovně Azure Spatial Anchors najdete v našem průvodci vytvořením a vyhledáním ukotvení.