Android の拡張現実で付近の場所を表示する(Kotlin)

1. 始める前に

概要

この Codelab では、Google Maps Platform のデータを使用して、Android の拡張現実(AR)で付近の場所を表示する方法について説明します。

2344909dd9a52c60.png

前提条件

  • Android Studio を使用して Android 開発を行うのに必要な基礎知識がある
  • Kotlin の使用経験がある

演習内容

  • デバイスのカメラと位置情報にアクセスする権限をユーザーにリクエストする。
  • Places API と統合し、デバイスの位置情報に基づいて付近の場所を取得する。
  • ARCore と統合して水平面を検出し、Sceneform を使用して仮想オブジェクトを 3D 空間に固定および配置する。
  • SensorManager を使用して空間内のデバイスの向きに関する情報を収集し、Maps SDK for Android ユーティリティ ライブラリを使用して仮想オブジェクトを正しい方角に配置する。

必要なもの

2. 準備

Android Studio

この Codelab では Android 10.0(API レベル 29)を使用します。Android Studio には Google Play 開発者サービスがインストールされている必要があります。これらの依存関係を両方ともインストールするには、次の手順を行います。

  1. SDK Manager に移動します([Tools] > [SDK Manager] の順にクリック)。

6c44a9cb9cf6c236.png

  1. Android 10.0 がインストールされているかどうかを確認します。インストールされていない場合は、[Android 10.0 (Q)] の横にあるチェックボックスをオンにしてから [OK] をクリックし、ダイアログで再度 [OK] をクリックしてインストールします。

368f17a974c75c73.png

  1. 最後に、Google Play 開発者サービスをインストールします。まず [SDK Tools] タブに移動し、[Google Play services] の横にあるチェックボックスをオンにして、[OK] をクリックします。ダイアログが表示されたら、再度 [OK] を選択します**。**

497a954b82242f4b.png

必要な API

次のセクションの手順 3 で、この Codelab で使用する Maps SDK for AndroidPlaces API を有効にします。

Google Maps Platform の利用を始める

Google Maps Platform を初めて使用する場合は、Google Maps Platform スタートガイドを参照するか、再生リスト「Getting Started with Google Maps Platform」を視聴して、以下の手順を行ってください。

  1. 請求先アカウントを作成します。
  2. プロジェクトを作成します。
  3. Google Maps Platform の API と SDK(前セクションに記載のもの)を有効化します。
  4. API キーを生成します。

(省略可)Android Emulator

ARCore 対応デバイスをお持ちでない場合は、Android Emulator を使用して拡張現実シーンをシミュレートしたり、デバイスの位置情報をエミュレートしたりすることができます。この演習では Sceneform を使用するため、下記のリンクから「Configure the emulator to support Sceneform(英語)」の手順も行ってください。

3. クイック スタート

できるだけ早く演習を開始できるように、この Codelab で使用できるスターター コードが用意されています。すぐに次のステップに進んでも問題ありませんが、すべての手順を確認したい場合は、最後までお読みください。

git がインストールされている場合は、リポジトリのクローンを作成できます。

git clone https://github.com/googlecodelabs/display-nearby-places-ar-android.git

あるいは、下のボタンをクリックしてソースコードをダウンロードすることもできます。

コードを入手したら、starter ディレクトリにあるプロジェクトを開きます。

4. プロジェクトの概要

前のステップでダウンロードしたコードを確認しましょう。このリポジトリには、app という名前のモジュールが 1 つあります。このモジュールには、パッケージ com.google.codelabs.findnearbyplacesar が含まれています。

AndroidManifest.xml

この Codelab で必要な機能を利用できるようにするため、AndroidManifest.xml ファイルでは次の属性が宣言されています。

<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />

<!-- Sceneform requires OpenGL ES 3.0 or later. -->
<uses-feature
   android:glEsVersion="0x00030000"
   android:required="true" />

<!-- Indicates that app requires ARCore ("AR Required"). Ensures the app is visible only in the Google Play Store on devices that support ARCore. For "AR Optional" apps remove this line. -->
<uses-feature android:name="android.hardware.camera.ar" />

uses-permission では、機能を利用するためにユーザーから許可を取得する必要のある権限が指定されます。以下のように宣言されます。

  • android.permission.INTERNET - アプリによるネットワーク操作とインターネット経由でのデータ取得(Places API を介して場所の情報を取得するなど)を可能にします。
  • android.permission.CAMERA - デバイスのカメラを使用して拡張現実にオブジェクトを表示するには、カメラへのアクセス権限が必要です。
  • android.permission.ACCESS_FINE_LOCATION - デバイスの位置情報に基づいて付近の場所を取得するには、位置情報へのアクセス権限が必要です。

uses-feature では、このアプリで必要なハードウェア機能が指定されます。以下のように宣言されます。

  • OpenGL ES バージョン 3.0 が必要
  • ARCore 対応デバイスが必要

さらに、アプリケーション オブジェクトに次のメタデータタグが追加されます。

<application
  android:allowBackup="true"
  android:icon="@mipmap/ic_launcher"
  android:label="@string/app_name"
  android:roundIcon="@mipmap/ic_launcher_round"
  android:supportsRtl="true"
  android:theme="@style/AppTheme">

  <!--
     Indicates that this app requires Google Play Services for AR ("AR Required") and causes
     the Google Play Store to download and install Google Play Services for AR along with
     the app. For an "AR Optional" app, specify "optional" instead of "required".
  -->

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

  <meta-data
     android:name="com.google.android.geo.API_KEY"
     android:value="@string/google_maps_key" />

  <!-- Additional elements here -->

</application>

1 つ目のメタデータ エントリはこのアプリを実行するのに ARCore が必要であることを示し、2 つ目のエントリは Maps SDK for Android に Google Maps Platform の API キーを提供する方法を示しています。

build.gradle

build.gradle では、次の追加の依存関係が指定されます。

dependencies {
    // Maps & Location
    implementation 'com.google.android.gms:play-services-location:17.0.0'
    implementation 'com.google.android.gms:play-services-maps:17.0.0'
    implementation 'com.google.maps.android:maps-utils-ktx:1.7.0'

    // ARCore
    implementation "com.google.ar.sceneform.ux:sceneform-ux:1.15.0"

    // Retrofit
    implementation "com.squareup.retrofit2:retrofit:2.7.1"
    implementation "com.squareup.retrofit2:converter-gson:2.7.1"
}

それぞれの依存関係について、以下に簡単に説明します。

  • グループ ID が com.google.android.gms であるライブラリ(play-services-locationplay-services-maps)は、デバイスの位置情報と Google マップの関連機能にアクセスするのに使用されます。
  • com.google.maps.android:maps-utils-ktx は、Maps SDK for Android ユーティリティ ライブラリ用の Kotlin 拡張機能(KTX)ライブラリです。このライブラリの機能は、後で仮想オブジェクトを実空間に配置する際に使用します。
  • com.google.ar.sceneform.ux:sceneform-ux は、OpenGL に精通していなくても、リアルな 3D シーンをレンダリングできる Sceneform ライブラリです。
  • グループ ID com.squareup.retrofit2 内の依存関係は Retrofit 依存関係です。この依存関係により、Places API と通信する HTTP クライアントを簡単に記述できるようになります。

プロジェクトの構造

以下のパッケージとファイルがあります。

  • **api - **このパッケージには、Retrofit を使用して Places API と通信する際に使用するクラスが含まれています。
  • **ar - **このパッケージには ARCore に関連するすべてのファイルが含まれています。
  • **model - **このパッケージには、単一のデータクラス Place が含まれています。このクラスは、Places API から返される単一の場所をカプセル化する際に使用されます。
  • MainActivity.kt - アプリに含まれる単一の Activity。地図とカメラビューを表示します。

5. シーンを設定する

それでは、アプリのコア コンポーネントに移りましょう。まずは、拡張現実の設定から始めます。

MainActivity には、地図オブジェクトの表示を処理する SupportMapFragment と、拡張現実シーンの表示を処理する ArFragment のサブクラス(PlacesArFragment)が含まれています。

拡張現実の設定

PlacesArFragment は、拡張現実シーンの表示のほか、カメラへのアクセス権限のリクエストも処理します(ユーザーからアクセス許可が付与されていない場合)。getAdditionalPermissions メソッドをオーバーライドして、追加の権限をリクエストすることもできます。たとえば、位置情報の利用許可を取得する必要がある場合は、次のように指定して getAdditionalPermissions メソッドをオーバーライドします。

class PlacesArFragment : ArFragment() {

   override fun getAdditionalPermissions(): Array<String> =
       listOf(Manifest.permission.ACCESS_FINE_LOCATION)
           .toTypedArray()
}

実行

Android Studio のディレクトリ starter に移動し、スケルトン コードを開きます。ツールバーで [Run] > [Run 'アプリ'] の順にクリックし、アプリをデバイスまたはエミュレータにデプロイすると、位置情報とカメラへのアクセス権限を有効にするよう求めるメッセージが表示されます。[Allow] をクリックすると、次のようにカメラビューと地図表示が並べて表示されます。

e3e3073d5c86f427.png

平面の検出

周囲の環境をカメラで確認すると、水平方向の平面上(下の画像ではカーペットの上)に白い点の集合が表示されます。

2a9b6ea7dcb2e249.png

これらの白い点は、水平面が検出されたことを示す ARCore のガイドです。この検出された平面上に「アンカー」を作成することで、実空間に仮想オブジェクトを配置することが可能になります。

ARCore の詳細と、ARCore が周囲の環境を認識する方法については、基本的な概念をご覧ください。

6 付近の場所を取得する

このステップでは、まずデバイスの現在地にアクセスして、地図に表示します。その後、Places API を使用して付近の場所を取得します。

マップの設定

Google Maps Platform の API キー

この Codelab で作成した Google Maps Platform の API キーを使用して、Places API のクエリを有効にし、Maps SDK for Android を使用します。まず gradle.properties ファイルを開き、作成した API キーで文字列 "YOUR API KEY HERE" を置き換えます。

地図上にデバイスの現在地を表示する

API キーを追加したら、ユーザーが現在、地図上のどこに位置しているのかを把握するため、地図にガイドを追加します。これを行うには、setUpMaps メソッドに移動して、mapFragment.getMapAsync 呼び出し内で googleMap.isMyLocationEnabledtrue. に設定します。これにより、地図上に青い点が表示されます。

private fun setUpMaps() {
   mapFragment.getMapAsync { googleMap ->
       googleMap.isMyLocationEnabled = true
       // ...
   }
}

現在地を取得する

デバイスの位置情報を取得するには、FusedLocationProviderClient クラスを使用します。このクラスのインスタンスは、MainActivityonCreate メソッドですでに取得されています。このオブジェクトを使用するには、getCurrentLocation メソッドを入力します。このメソッドはラムダ引数を受け入れるので、メソッドの呼び出し元に現在地のデータを渡すことが可能になります。

このメソッドを完了するには、次のように FusedLocationProviderClient オブジェクトの lastLocation プロパティにアクセスし、addOnSuccessListener を追加します。

fusedLocationClient.lastLocation.addOnSuccessListener { location ->
    currentLocation = location
    onSuccess(location)
}.addOnFailureListener {
    Log.e(TAG, "Could not get location")
}

getCurrentLocation メソッドは、setUpMaps メソッドの getMapAsync で指定されたラムダ内から呼び出されます。これにより、付近の場所の取得が可能になります。

場所のネットワーク呼び出しを開始する

getNearbyPlaces メソッドの呼び出しでは、API キー、デバイスの位置情報、メートル単位の半径(以下の例では 2 km に設定)、場所のタイプ(以下の例では park に設定)の各パラメータが placesServices.nearbyPlaces メソッドに渡されます。

val apiKey = "YOUR API KEY"
placesService.nearbyPlaces(
   apiKey = apiKey,
   location = "${location.latitude},${location.longitude}",
   radiusInMeters = 2000,
   placeType = "park"
)

ネットワーク呼び出しを完了するには、gradle.properties ファイルで定義した API キーを渡します。次のコード スニペットは、build.gradle ファイルの android > defaultConfig の設定で定義されています。

android {
   defaultConfig {
       resValue "string", "google_maps_key", (project.findProperty("GOOGLE_MAPS_API_KEY") ?: "")
   }
}

これにより、ビルド時に文字列リソース値 google_maps_key が使用できるようになります。

ネットワーク呼び出しを完了するには、Context オブジェクトの getString を介して、この文字列リソースを読み取ります。

val apiKey = this.getString(R.string.google_maps_key)

7. 拡張現実に場所を配置する

ここまでの演習で、以下の手順が完了しています。

  1. アプリを初めて実行する際に、カメラへのアクセス権限と位置情報の利用許可をユーザーにリクエストする
  2. ARCore を設定して、水平面のトラッキングを開始する
  3. API キーを使って Maps SDK を設定する
  4. デバイスの現在地を取得する
  5. Places API を使用して付近の場所(公園など)を取得する

この演習の残りのステップでは、取得した場所を拡張現実に配置する方法を学びます。

シーンを認識する

ARCore は、デバイスのカメラを通して現実世界のシーンを認識し、各画像フレームでなんらかの明確な特徴を持つポイント(特徴点)を検出します。テーブルや床などの水平面上に一群の特徴点が検出されると、アプリでその平面を利用することが可能になります。

すでに説明したように、ARCore では、平面が検出されるとユーザーのガイドとなる白い点が表示されます。

2a9b6ea7dcb2e249.png

アンカーを追加する

平面が検出されると、アンカーと呼ばれるオブジェクトを追加できます。アンカーを使用することで、仮想オブジェクトを配置し、それぞれのオブジェクトを空間内の同じ位置に固定して表示することができます。平面が検出されたら、コードに変更を加えて、アンカーを 1 つ追加してみましょう。

setUpAr では、OnTapArPlaneListenerPlacesArFragment に追加されています。このリスナーは、拡張現実シーンで平面がタップされると呼び出されます。リスナーの HitResult に基づいて、次のようにこの呼び出し内に AnchorAnchorNode を作成します。

arFragment.setOnTapArPlaneListener { hitResult, _, _ ->
   val anchor = hitResult.createAnchor()
   anchorNode = AnchorNode(anchor)
   anchorNode?.setParent(arFragment.arSceneView.scene)
   addPlaces(anchorNode!!)
}

addPlaces メソッド呼び出しで処理されるシーンで、AnchorNode に子ノード オブジェクト(PlaceNode インスタンス)を追加します。

実行

上記の変更を適用したら、アプリを実行し、カメラをかざして平面が検出されるのを待ちます。平面を表す白い点をタップしてください。これで、付近にあるすべての公園のマーカーが地図上に表示されます。ただし、仮想オブジェクトは作成したアンカーに固定されており、空間内の公園の位置関係に基づいて配置されてはいません。

f93eb87c98a0098d.png

最後のステップでは、デバイスで Maps SDK for Android ユーティリティ ライブラリSensorManager を使用してこの問題を修正します。

8. 場所を正しい方角に配置する

仮想の場所のアイコンを正確な方角に基づいて拡張現実に配置するには、次の 2 つの情報が必要です。

  • 真北の方角
  • 北と各場所のなす角度

真北を定める

真北は、デバイスの位置センサー(地磁気センサーと加速度計)で特定できます。この 2 つのセンサーを使用することで、空間におけるデバイスの向きに関する情報をリアルタイムに収集できます。位置センサーについて詳しくは、デバイスの画面の向きを計算するをご覧ください。

位置センサーにアクセスするには、SensorManager を取得してからセンサーに SensorEventListener を登録する必要がありますが、これらの手順は MainActivity のライフサイクル メソッドですでに完了しています(以下を参照)。

override fun onCreate(savedInstanceState: Bundle?) {
   super.onCreate(savedInstanceState)
   // ...
   sensorManager = getSystemService()!!
   // ...
}

override fun onResume() {
   super.onResume()
   sensorManager.getDefaultSensor(Sensor.TYPE_MAGNETIC_FIELD)?.also {
       sensorManager.registerListener(
           this,
           it,
           SensorManager.SENSOR_DELAY_NORMAL
       )
   }
   sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER)?.also {
       sensorManager.registerListener(
           this,
           it,
           SensorManager.SENSOR_DELAY_NORMAL
       )
   }
}

override fun onPause() {
   super.onPause()
   sensorManager.unregisterListener(this)
}

onSensorChanged メソッドでは、SensorEvent オブジェクトが指定されます。このオブジェクトには、時間の経過とともに変化する各センサーの詳細なデータが格納されます。このメソッドに次のコードを追加します。

override fun onSensorChanged(event: SensorEvent?) {
   if (event == null) {
       return
   }
   if (event.sensor.type == Sensor.TYPE_ACCELEROMETER) {
       System.arraycopy(event.values, 0, accelerometerReading, 0, accelerometerReading.size)
   } else if (event.sensor.type == Sensor.TYPE_MAGNETIC_FIELD) {
       System.arraycopy(event.values, 0, magnetometerReading, 0, magnetometerReading.size)
   }

   // Update rotation matrix, which is needed to update orientation angles.
   SensorManager.getRotationMatrix(
       rotationMatrix,
       null,
       accelerometerReading,
       magnetometerReading
   )
   SensorManager.getOrientation(rotationMatrix, orientationAngles)
}

上記のコードはセンサーのタイプを確認し、そのタイプに応じてセンサーの測定値(加速度計または磁力計の値)を更新します。これらのセンサーの測定値に基づいて、北とデバイスの間の角度(orientationAngles[0] の値)を求めることができます。

球面幾何学に基づく方角

北の方角が決まると、北と各場所のなす角度を求めることができます。その後、その情報に基づいて、拡張現実に正しい方角で場所を配置します。

方角を計算するには、Maps SDK for Android ユーティリティ ライブラリを使用します。このライブラリには、球面幾何学を使って距離や方角を計算するヘルパー関数が含まれています。詳しくは、ライブラリの概要をご覧ください。

次に、ユーティリティ ライブラリの sphericalHeading メソッドを使用して、2 つの LatLng オブジェクト間の方角(方位)を計算します。この情報は、Place.kt で定義される getPositionVector メソッドで必要となります。このメソッドは最終的に Vector3 オブジェクトを返し、このオブジェクトは拡張現実空間のローカルの位置として各 PlaceNode で使用されます。

このメソッドの方角の定義を次のように置き換えます。

val heading = latLng.sphericalHeading(placeLatLng)

これにより、メソッドの定義は次のようになります。

fun Place.getPositionVector(azimuth: Float, latLng: LatLng): Vector3 {
   val placeLatLng = this.geometry.location.latLng
   val heading = latLng.sphericalHeading(placeLatLng)
   val r = -2f
   val x = r * sin(azimuth + heading).toFloat()
   val y = 1f
   val z = r * cos(azimuth + heading).toFloat()
   return Vector3(x, y, z)
}

ローカルの位置

拡張現実で場所を正しい向きに配置する最後のステップでは、PlaceNode オブジェクトをシーンに追加する際に getPositionVector の結果を使用します。まずは、MainActivityaddPlaces に移動しましょう。placeNode ごとに親が設定されている行(placeNode.setParent(anchorNode))のすぐ下で、placeNodelocalPositiongetPositionVector の呼び出しの結果に設定します。

val placeNode = PlaceNode(this, place)
placeNode.setParent(anchorNode)
placeNode.localPosition = place.getPositionVector(orientationAngles[0], currentLocation.latLng)

デフォルトでは、getPositionVector メソッドは、getPositionVector メソッドの y 値で指定されている 1 m をノードの y 距離に設定します。この距離を、たとえば 2 m などに調整したい場合は、必要に応じて値を変更します。

この変更により、追加された PlaceNode オブジェクトが正しい方角に配置されます。アプリを実行して、結果を確認してみましょう。

9. 完了

これで、この Codelab は完了です!

詳細