チュートリアル:Azure Spatial Anchors を使用して新しい Android アプリを作成する手順Tutorial: Step-by-step instructions to create a new Android app using Azure Spatial Anchors

このチュートリアルでは、ARCore 機能を Azure Spatial Anchors と統合する新しい Android アプリの作成方法を示します。This tutorial will show you how to create a new Android app that integrates ARCore functionality with Azure Spatial Anchors.

前提条件Prerequisites

このチュートリアルを完了するには、以下のものが必要です。To complete this tutorial, make sure you have:

使用の開始Getting started

Android Studio を起動します。Start Android Studio. [Android Studio へようこそ] ウィンドウで、 [新規 Android Studio プロジェクトの開始] を選択します。In the Welcome to Android Studio window, click Start a new Android Studio project. または、既に開かれているプロジェクトがある場合は、 [ファイル] -> [新規プロジェクト] を選択します。Or, if you have a project already opened, select File->New Project.

[新規プロジェクトの作成] ウィンドウの [スマホおよびタブレット] セクションで、 [空のアクティビティ] を選択し、 [次へ] をクリックします。In the Create New Project window, under the Phone and Tablet section, choose Empty Activity, and click Next. 次に、 [Minimum API level](最小 API レベル)API 26: Android 8.0 (Oreo) を選択し、 [Language](言語)Java に設定されていることを確認します。Then, under Minimum API level, choose API 26: Android 8.0 (Oreo), and ensure the Language is set to Java. プロジェクトの名前と場所、およびパッケージ名の変更が必要な場合があります。You may want to change the Project Name & Location, and the Package name. 他のオプションはそのままにします。Leave the other options as they are. [完了] をクリックします。Click Finish. コンポーネント インストーラーが実行されます。The Component Installer will run. 完了したら、 [完了] をクリックします。Once it's done, click Finish. いくつかの処理の後、Android Studio によって IDE が開かれます。After some processing, Android Studio will open the IDE.

試してみるTrying it out

新しいアプリをテストするには、USB ケーブルを使用して開発用マシンに開発者向けのデバイスを接続します。To test out your new app, connect your developer-enabled device to your development machine with a USB cable. [実行] -> [Run 'app']('アプリ' を実行) をクリックします。Click Run->Run 'app'. [Select Deployment Target](配置ターゲットの選択) ウィンドウで、お使いのデバイスを選択し、 [OK] をクリックします。In the Select Deployment Target window, select your device, and click OK. Android Studio によって、接続されているデバイスにアプリがインストールされて起動されます。Android Studio installs the app on your connected device and starts it. "Hello World!" が、You should now see "Hello World!" お使いのデバイスで実行されているアプリに表示されます。displayed in the app running on your device. [実行] -> [Stop 'app']('app' を停止) をクリックします。Click Run->Stop 'app'.

ARCore との統合Integrating ARCore

ARCore は、Augmented Reality エクスペリエンスを構築するための Google のプラットフォームであり、お使いのデバイスが移動するときにその位置を追跡し、現実世界の独自の認識を構築できるようにします。ARCore is Google's platform for building Augmented Reality experiences, enabling your device to track its position as it moves and builds its own understanding of the real world.

ルート <manifest> ノード内に次のエントリを含むように app\manifests\AndroidManifest.xml を変更します。Modify app\manifests\AndroidManifest.xml to include the following entries inside the root <manifest> node. このコード スニペットではいくつかのことを行います。This code snippet does a few things:

  • アプリにデバイスのカメラへのアクセスを許可します。It will allow your app to access your device camera.
  • Google Play ストア内で、ARCore をサポートするデバイスに対してのみアプリが表示されるようにもします。It will also ensure your app is only visible in the Google Play Store to devices that support ARCore.
  • アプリのインストール時に ARCore がまだインストールされていない場合は ARCore をダウンロードしてインストールするように Google Play ストアを構成します。It will configure the Google Play Store to download and install ARCore, if it isn't installed already, when your app is installed.
<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>

次のエントリを含むように Gradle Scripts\build.gradle (Module: app) を変更します。Modify Gradle Scripts\build.gradle (Module: app) to include the following entry. このコードにより、ご自分のアプリは確実に ARCore バージョン 1.8 を対象とするようになります。This code will ensure that your app targets ARCore version 1.8. この変更の後に、Gradle から同期を求める通知を受け取ることがあります。 [Sync now](今すぐ同期) をクリックします。After this change, you might get a notification from Gradle asking you to sync: click Sync now.

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

Sceneform の統合Integrating Sceneform

Sceneform により、OpenGL を習得しなくても、Augmented Reality アプリ内でリアルな 3D シーンを簡単にレンダリングできます。Sceneform makes it simple to render realistic 3D scenes in Augmented Reality apps, without having to learn OpenGL.

次のエントリを含むように Gradle Scripts\build.gradle (Module: app) を変更します。Modify Gradle Scripts\build.gradle (Module: app) to include the following entries. このコードにより、アプリは Sceneform に必要な Java 8 の言語コンストラクトの使用を許可されます。This code will allow your app to use language constructs from Java 8, which Sceneform requires. また、ご自分のアプリは確実に Sceneform バージョン 1.8 を対象とするようになります。これはご自分のアプリで使用している ARCore のバージョンと一致する必要があるためです。It will also ensure your app targets Sceneform version 1.8, since it should match the version of ARCore your app is using. この変更の後に、Gradle から同期を求める通知を受け取ることがあります。 [Sync now](今すぐ同期) をクリックします。After this change, you might get a notification from Gradle asking you to sync: click Sync now.

android {
    ...

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

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

app\res\layout\activity_main.xml を開き、既存の Hello World <TextView> 要素を次の ArFragment に置き換えます。Open your app\res\layout\activity_main.xml, and replace the existing Hello Wolrd <TextView> element with the following ArFragment. このコードにより、カメラ フィードが画面に表示され、お使いのデバイスが移動するときにその位置を ARCore で追跡できます。This code will cause the camera feed to be displayed on your screen enabling ARCore to track your device position as it moves.

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

お使いのデバイスにアプリを再デプロイして、もう一度検証します。Redeploy your app to your device to validate it once more. 今回は、カメラのアクセス許可を求められます。This time, you should be asked for camera permissions. 承認されると、画面上にカメラ フィードのレンダリングが表示されます。Once approved, you should see your camera feed rendering on your screen.

現実世界でのオブジェクトの配置Place an object in the real world

アプリを使用してオブジェクトを作成し、配置しましょう。Let's create & place an object using your app. まず、以下の import を app\java\<PackageName>\MainActivity に追加します。First, add the following imports into your app\java\<PackageName>\MainActivity:

import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;

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;

続いて、以下のメンバー変数を MainActivity クラスに追加します。Then, add the following member variables into your MainActivity class:


public class MainActivity extends AppCompatActivity {

    private boolean tapExecuted = false;
    private final Object syncTaps = new Object();
    private ArFragment arFragment;

次に、以下のコードを app\java\<PackageName>\MainActivity onCreate() メソッドに追加します。Next, add the following code into your app\java\<PackageName>\MainActivity onCreate() method. このコードでは、ユーザーがデバイスの画面をタップしたときにそれを検出する handleTap() というリスナーをフックします。This code will hook up a listener, called handleTap(), that will detect when the user taps the screen on your device. ARCore の追跡によって既に認識されている現実世界のサーフェス上でタップが行われると、リスナーが実行されます。If the tap happens to be on a real world surface that has already been recognized by ARCore's tracking, the listener will run.

private ExecutorService executorService = Executors.newSingleThreadExecutor();

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

最後に、すべてをまとめる次の handleTap() メソッド追加します。Finally, add the following handleTap() method, that will tie everything together. これにより球体が作成されて、タップされた位置に配置されます。It will create a sphere, and place it on the tapped location. this.recommendedSessionProgress は現在 0 に設定されているので、最初は球体が黒くなります。The sphere will initially be black, since this.recommendedSessionProgress is set to zero right now. この値は後で調整されます。This value will be adjusted later on.

// </initializeSession>

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

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

お使いのデバイスにアプリを再デプロイして、もう一度検証します。Redeploy your app to your device to validate it once more. 今回は、ARCore が環境の認識を開始するようにお使いのデバイスを移動できます。This time, you can move around your device to get ARCore to start recognizing your environment. 次に、画面をタップして黒い球体を作成し、任意のサーフェス上に配置します。Then, tap the screen to create & place your black sphere over the surface of your choice.

ローカル Azure Spatial Anchor のアタッチAttach a local Azure Spatial Anchor

次のエントリを含むように Gradle Scripts\build.gradle (Module: app) を変更します。Modify Gradle Scripts\build.gradle (Module: app) to include the following entry. このコードにより、ご自分のアプリは確実に Azure Spatial Anchors バージョン 1.3.0 を対象とするようになります。This code will ensure that your app targets Azure Spatial Anchors version 1.3.0. ただし、Azure Spatial Anchors の任意の最新バージョンの参照が機能します。That said, referencing any recent version of Azure Spatial Anchors should work.

dependencies {
    ...
    implementation "com.microsoft.azure.spatialanchors:spatialanchors_jni:[1.3.0]"
    implementation "com.microsoft.azure.spatialanchors:spatialanchors_java:[1.3.0]"
    ...
}

app\java\<PackageName>-> [新規] -> [Java Class](Java クラス) を右クリックします。Right-click app\java\<PackageName>->New->Java Class. [名前]MyFirstApp に設定し、 [スーパークラス]android.app.Application に設定します。Set Name to MyFirstApp, and Superclass to android.app.Application. 他のオプションはそのままにします。Leave the other options as they are. Click OK.Click OK. MyFirstApp.java というファイルが作成されます。A file called MyFirstApp.java will be created. そこに次の import を追加します。Add the following import to it:

import com.microsoft.CloudServices;

次に、新しい MyFirstApp クラス内に次のコードを追加します。このコードにより、Azure Spatial Anchors がアプリケーションのコンテキストで初期化されます。Then, add the following code inside the new MyFirstApp class, which will ensure Azure Spatial Anchors is initialized with your application's context.

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

ここで、ルート <application> ノード内に次のエントリを含むように app\manifests\AndroidManifest.xml を変更します。Now, modify app\manifests\AndroidManifest.xml to include the following entry inside the root <application> node. このコードでは、作成したアプリケーション クラスがアプリにフックされます。This code will hook up the Application class you created into your app.

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

app\java\<PackageName>\MainActivity に戻り、そこに以下の import を追加します。Back in app\java\<PackageName>\MainActivity, add the following imports into it:

import com.google.ar.sceneform.rendering.ShapeFactory;
import com.google.ar.sceneform.ux.ArFragment;

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

import com.google.ar.sceneform.ArSceneView;
import com.google.ar.sceneform.Scene;

続いて、以下のメンバー変数を MainActivity クラスに追加します。Then, add the following member variables into your MainActivity class:

private ArFragment arFragment;
private AnchorNode anchorNode;
private Renderable nodeRenderable = null;
private float recommendedSessionProgress = 0f;

次に、以下の initializeSession() メソッドを mainActivity クラス内に追加しましょう。Next, let's add the following initializeSession() method inside your mainActivity class. これが呼び出されると、Azure Spatial Anchors セッションが作成され、アプリの起動時に適切に初期化されます。Once called, it will ensure an Azure Spatial Anchors session is created and properly initialized during the startup of your app.

// </onCreate>

// <initializeSession>
private void initializeSession() {
    if (this.cloudSession != null){
        this.cloudSession.close();
    }
    this.cloudSession = new CloudSpatialAnchorSession();
    this.cloudSession.setSession(sceneView.getSession());
    this.cloudSession.getConfiguration().setAccountId(/* Copy your account Identifier in here */);

次に、initializeSession() メソッドを onCreate() メソッドにフックしましょう。Now, let's hook your initializeSession() method into your onCreate() method. また、カメラ フィードからのフレームが処理のために Azure Spatial Anchors SDK に送信されるようにします。Also, we'll ensure that frames from your camera feed are sent to Azure Spatial Anchors SDK for processing.

private ExecutorService executorService = Executors.newSingleThreadExecutor();

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

最後に、以下のコードを handleTap() メソッドに追加します。Finally, add the following code into your handleTap() method. これにより、現実世界に配置している黒い球体にローカルの Azure 空間アンカーがアタッチされます。It will attach a local Azure Spatial Anchor to the black sphere that we're placing in the real world.

// </initializeSession>

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

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

アプリをもう一度再デプロイします。Redeploy your app once more. お使いのデバイスを移動し、画面をタップして黒い球体を配置します。Move around your device, tap the screen, and place a black sphere. ただし今回は、コードによりローカルの Azure 空間アンカーが作成されて球体にアタッチされます。This time, though, your code will be creating and attaching a local Azure Spatial Anchor to your sphere.

先に進む前に、Azure Spatial Anchors アカウント識別子とキーがまだない場合はこれらを作成します。Before proceeding any further, you'll need to create an Azure Spatial Anchors account Identifier and Key, if you don't already have them. 次のセクションに従ってこれらを取得します。Follow the following section to obtain them.

Spatial Anchors リソースを作成するCreate a Spatial Anchors resource

Azure ポータルにアクセスします。Go to the Azure portal.

Azure portal の左側のナビゲーション ウィンドウで、 [リソースの作成] を選択します。In the left navigation pane in the Azure portal, select Create a resource.

検索ボックスを使用して、「Spatial Anchors」を検索します。Use the search box to search for Spatial Anchors.

Spatial Anchors の検索

[Spatial Anchors] を選択します。Select Spatial Anchors. ダイアログ ボックスで [作成] を選択します。In the dialog box, select Create.

[Spatial Anchors アカウント] ダイアログ ボックスで以下を行います。In the Spatial Anchors Account dialog box:

  • 通常の英数字を使用して、一意のリソース名を入力します。Enter a unique resource name, using regular alphanumeric characters.

  • リソースをアタッチするサブスクリプションを選択します。Select the subscription that you want to attach the resource to.

  • [新規作成] を選択して、リソース グループを作成します。Create a resource group by selecting Create new. myResourceGroup」と名前を付け、 [OK] を選択します。Name it myResourceGroup and select OK. リソース グループとは、Web アプリ、データベース、ストレージ アカウントなどの Azure リソースのデプロイと管理に使用する論理コンテナーです。A resource group is a logical container into which Azure resources like web apps, databases, and storage accounts are deployed and managed. たとえば、後から簡単な手順で一度にリソース グループ全体を削除することもできます。For example, you can choose to delete the entire resource group in one simple step later.

  • リソースを配置する場所 (リージョン) を選択します。Select a location (region) in which to place the resource.

  • [新規] を選択して、リソースの作成を開始します。Select New to begin creating the resource.

    リソースの作成

リソースが作成されると、Azure portal に、デプロイが完了したことが表示されます。After the resource is created, Azure Portal will show that your deployment is complete. [リソースに移動] をクリックします。Click Go to resource.

デプロイ完了

これで、リソースのプロパティを確認できます。Then, you can view the resource properties. リソースの [アカウント ID] 値は、後で必要になるため、テキスト エディターにコピーしておきます。Copy the resource's Account ID value into a text editor because you'll need it later.

リソースのプロパティ

[設定][キー] を選択します。Under Settings, select Key. [主キー] の値をテキスト エディターにコピーします。Copy the Primary key value into a text editor. この値は Account Key ですThis value is the Account Key. この情報は後で必要になります。You'll need it later.

アカウント キー

クラウドへのローカル アンカーのアップロードUpload your local anchor into the cloud

自分の Azure Spatial Anchors アカウント識別子とキーを作成したら、app\java\<PackageName>\MainActivity に戻り、そこに以下の import を追加できます。Once you have your Azure Spatial Anchors account Identifier and Key, we can go back in app\java\<PackageName>\MainActivity, add the following imports into it:

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

import java.util.concurrent.CompletableFuture;

続いて、以下のメンバー変数を MainActivity クラスに追加します。Then, add the following member variables into your MainActivity class:

private float recommendedSessionProgress = 0f;

private ArSceneView sceneView;
private CloudSpatialAnchorSession cloudSession;

private String anchorId = null;

次に、以下のコードを initializeSession() メソッドに追加します。Now, add the following code into your initializeSession() method. まず、このコードにより、Azure Spatial Anchors SDK によってカメラ フィードからフレームが収集される際の進行状況をアプリで監視できます。First, this code will allow your app to monitor the progress that the Azure Spatial Anchors SDK makes as it collects frames from your camera feed. その際、球体の色が当初の黒から灰色に変化し始めます。As it does, the color of your sphere will start changing from its original black, into grey. さらに、アンカーをクラウドに送信するための十分なフレームが収集されると、白に変わります。Then, it will turn white once enough frames are collected to submit your anchor to the cloud. 次に、このコードでは、クラウド バックエンドとの通信に必要な資格情報が提供されます。Second, this code will provide the credentials needed to communicate with the cloud back-end. ここでは、アカウント識別子とキーを使用するようにアプリを構成します。Here is where you'll configure your app to use your account Identifier and Key. これらの情報は、Spatial Anchors リソースを設定するときにテキスト エディターにコピーしました。You copied them into a text editor when setting up the Spatial Anchors resource.

// </onCreate>

// <initializeSession>
private void initializeSession() {
    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())));

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

次に、以下の uploadCloudAnchorAsync() メソッドを mainActivity クラスに追加します。Next, add the following uploadCloudAnchorAsync() method inside your mainActivity class. 呼び出されると、このメソッドは、お使いのデバイスから十分なフレームが収集されるまで非同期的に待機します。Once called, this method will asynchronously wait until enough frames are collected from your device. この状況が発生するとすぐに、球体の色が黄色に切り替わってから、クラウドへのローカル Azure 空間アンカーのアップロードが開始されます。As soon as that happens, it will switch the color of your sphere to yellow, and then it will start uploading your local Azure Spatial Anchor into the cloud. アップロードが完了すると、コードによってアンカー識別子が返されます。Once the upload finishes, the code will return an anchor identifier.

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

最後に、すべてをつなげましょう。Finally, let's hook everything together. handleTap() メソッドに次のコードを追加します。In your handleTap() method, add the following code. これにより、球体が作成されるとすぐに uploadCloudAnchorAsync() メソッドが呼び出されます。It will invoke your uploadCloudAnchorAsync() method as soon as your sphere is created. メソッドから戻ると、次のコードによって球体への最終的な更新が実行され、色が青色に変わります。Once the method returns, the code below will perform one final update to your sphere, changing its color to blue.

// </initializeSession>

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

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

アプリをもう一度再デプロイします。Redeploy your app once more. お使いのデバイスを移動し、画面をタップして球体を配置します。Move around your device, tap the screen, and place your sphere. ただし今回は、カメラ フレームの収集時に球体の色が黒から白に向かって変化します。This time, though, your sphere will change its color from black towards white, as camera frames are collected. 十分なフレームを取得すると、球体が黄色に変わり、クラウドのアップロードが開始されます。Once we have enough frames, the sphere will turn into yellow, and the cloud upload will start. アップロードの完了後に、球体は青色になります。Once the upload finishes, your sphere will turn blue. 必要に応じて、Android Studio 内の Logcat ウィンドウを使用して、アプリによって送信されているログ メッセージを監視することもできます。Optionally, you could also use the Logcat window inside Android Studio to monitor the log messages your app is sending. たとえば、フレーム キャプチャ中のセッションの進行状況や、アップロードが完了した後にクラウドから返されるアンカー識別子などです。For example, the session progress during frame captures, and the anchor identifier that the cloud returns once the upload is completed.

クラウド空間アンカーの配置Locate your cloud spatial anchor

アンカーがクラウドにアップロードされると、それを配置する準備ができています。One your anchor is uploaded to the cloud, we're ready to attempt locating it again. まず、以下の import をコードに追加しましょう。First, let's add the following imports into your code.

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

次に、以下のコードを handleTap() メソッドに追加しましょう。Then, let's add the following code into your handleTap() method. このコードでは、次のことが行われます。This code will:

  • 既存の青い球体を画面から削除します。Remove our existing blue sphere from the screen.
  • Azure Spatial Anchors セッションをもう一度初期化します。Initialize our Azure Spatial Anchors session again. このアクションにより、配置しようとしているアンカーは、作成したローカル アンカーではなくクラウドから取得されます。This action will ensure that the anchor we're going to locate comes from the cloud instead of the local anchor we created.
  • クラウドにアップロードしたアンカーに対するクエリを発行します。Issue a query for the anchor we uploaded to the 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;
                                }
                            });
                    });
                });
        });
}

ここで、クエリの実行対象のアンカーが配置されたときに呼び出されるコードをフックしましょう。Now, let's hook the code that will be invoked when the anchor we're querying for is located. initializeSession() メソッド内に次のコードを追加します。Inside your initializeSession() method, add the following code. このスニペットでは、クラウド空間アンカーが配置された後に緑色の球体が作成され、配置されます。This snippet will create & place a green sphere once the cloud spatial anchor is located. これにより、画面をもう一度タップできるようになるので、シナリオ全体をもう一度繰り返すことができます。別のローカル アンカーを作成し、アップロードして、もう一度配置します。It will also enable screen tapping again, so you can repeat the whole scenario once more: create another local anchor, upload it, and locate it again.

private void initializeSession() {
    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())));

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

これで完了です。That's it! 最後にアプリを再デプロイし、シナリオ全体をエンド ツー エンドで試します。Redeploy your app one last time to try out the whole scenario end to end. お使いのデバイスを移動し、黒い球体を配置します。Move around your device, and place your black sphere. 次に、球体が黄色に変わるまでデバイスを移動し続けてカメラ フレームをキャプチャします。Then, keep moving your device to capture camera frames until the sphere turns yellow. ローカル アンカーがアップロードされ、球体が青色に変わります。Your local anchor will be uploaded, and your sphere will turn blue. 最後に、ローカル アンカーが削除されるように画面をもう一度タップしてから、対応するクラウド アンカーに対してクエリを実行します。Finally, tap your screen once more, so that your local anchor is removed, and then we'll query for its cloud counterpart. クラウド空間アンカーが配置されるまで、デバイスの移動を続けます。Continue moving your device around until your cloud spatial anchor is located. 正しい場所に緑色の球体が表示され、シナリオ全体をもう一度繰り返すことができます。A green sphere should appear in the correct location, and you can rinse & repeat the whole scenario again.

すべてをまとめるPutting everything together

ここでは、さまざまな要素がすべてまとめられた後に完全な MainActivity クラス ファイルがどのようになるかを示します。Here is how the complete MainActivity class file should look like, after all the different elements have been put together. これを参照用に使用してご自身のファイルと比較し、違いが残っているかどうかを特定できます。You can use it as a reference to compare against your own file, and spot if you may have any differences left.

package com.example.myfirstapp;

import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;

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

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

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

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 String anchorId = null;
    private boolean scanningForUpload = false;
    private final Object syncSessionProgress = new Object();
    private ExecutorService executorService = Executors.newSingleThreadExecutor();

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

        initializeSession();
    }
    // </onCreate>

    // <initializeSession>
    private void initializeSession() {
        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())));

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

次の手順Next steps

このチュートリアルでは、ARCore 機能を Azure Spatial Anchors と統合する新しい Android アプリの作成方法を説明しました。In this tutorial, you've seen how to create a new Android app that integrates ARCore functionality with Azure Spatial Anchors. Azure Spatial Anchors ライブラリの詳細については、アンカーの作成と配置方法に関するガイドを引き続き参照してください。To learn more about the Azure Spatial Anchors library, continue to our guide on how to create and locate anchors.