1. 始める前に
概要
この Codelab では、Google Maps Platform のデータを使用して、Android の拡張現実(AR)で付近の場所を表示する方法について説明します。
前提条件
- Android Studio を使用して Android 開発を行うのに必要な基礎知識がある
- Kotlin の使用経験がある
演習内容
- デバイスのカメラと位置情報にアクセスする権限をユーザーにリクエストする。
- Places API と統合し、デバイスの位置情報に基づいて付近の場所を取得する。
- ARCore と統合して水平面を検出し、Sceneform を使用して仮想オブジェクトを 3D 空間に固定および配置する。
- SensorManager を使用して空間内のデバイスの向きに関する情報を収集し、Maps SDK for Android ユーティリティ ライブラリを使用して仮想オブジェクトを正しい方角に配置する。
必要なもの
- Android Studio 2020.3.1 以降
- OpenGL ES 3.0 以降をサポートする開発マシン
- ARCore 対応デバイス、または ARCore 対応の Android Emulator(設定手順については、次のステップを参照)
2. 準備
Android Studio
この Codelab では Android 10.0(API レベル 29)を使用します。Android Studio には Google Play 開発者サービスがインストールされている必要があります。これらの依存関係を両方ともインストールするには、次の手順を行います。
- SDK Manager に移動します([Tools] > [SDK Manager] の順にクリック)。
- Android 10.0 がインストールされているかどうかを確認します。インストールされていない場合は、[Android 10.0 (Q)] の横にあるチェックボックスをオンにしてから [OK] をクリックし、ダイアログで再度 [OK] をクリックしてインストールします。
- 最後に、Google Play 開発者サービスをインストールします。まず [SDK Tools] タブに移動し、[Google Play services] の横にあるチェックボックスをオンにして、[OK] をクリックします。ダイアログが表示されたら、再度 [OK] を選択します**。**
必要な API
次のセクションの手順 3 で、この Codelab で使用する Maps SDK for Android と Places API を有効にします。
Google Maps Platform の利用を始める
Google Maps Platform を初めて使用する場合は、Google Maps Platform スタートガイドを参照するか、再生リスト「Getting Started with Google Maps Platform」を視聴して、以下の手順を行ってください。
- 請求先アカウントを作成します。
- プロジェクトを作成します。
- Google Maps Platform の API と SDK(前セクションに記載のもの)を有効化します。
- 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-location
とplay-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] をクリックすると、次のようにカメラビューと地図表示が並べて表示されます。
平面の検出
周囲の環境をカメラで確認すると、水平方向の平面上(下の画像ではカーペットの上)に白い点の集合が表示されます。
これらの白い点は、水平面が検出されたことを示す 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.isMyLocationEnabled
を true.
に設定します。これにより、地図上に青い点が表示されます。
private fun setUpMaps() {
mapFragment.getMapAsync { googleMap ->
googleMap.isMyLocationEnabled = true
// ...
}
}
現在地を取得する
デバイスの位置情報を取得するには、FusedLocationProviderClient
クラスを使用します。このクラスのインスタンスは、MainActivity
の onCreate
メソッドですでに取得されています。このオブジェクトを使用するには、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. 拡張現実に場所を配置する
ここまでの演習で、以下の手順が完了しています。
- アプリを初めて実行する際に、カメラへのアクセス権限と位置情報の利用許可をユーザーにリクエストする
- ARCore を設定して、水平面のトラッキングを開始する
- API キーを使って Maps SDK を設定する
- デバイスの現在地を取得する
- Places API を使用して付近の場所(公園など)を取得する
この演習の残りのステップでは、取得した場所を拡張現実に配置する方法を学びます。
シーンを認識する
ARCore は、デバイスのカメラを通して現実世界のシーンを認識し、各画像フレームでなんらかの明確な特徴を持つポイント(特徴点)を検出します。テーブルや床などの水平面上に一群の特徴点が検出されると、アプリでその平面を利用することが可能になります。
すでに説明したように、ARCore では、平面が検出されるとユーザーのガイドとなる白い点が表示されます。
アンカーを追加する
平面が検出されると、アンカーと呼ばれるオブジェクトを追加できます。アンカーを使用することで、仮想オブジェクトを配置し、それぞれのオブジェクトを空間内の同じ位置に固定して表示することができます。平面が検出されたら、コードに変更を加えて、アンカーを 1 つ追加してみましょう。
setUpAr
では、OnTapArPlaneListener
が PlacesArFragment
に追加されています。このリスナーは、拡張現実シーンで平面がタップされると呼び出されます。リスナーの HitResult
に基づいて、次のようにこの呼び出し内に Anchor
と AnchorNode
を作成します。
arFragment.setOnTapArPlaneListener { hitResult, _, _ ->
val anchor = hitResult.createAnchor()
anchorNode = AnchorNode(anchor)
anchorNode?.setParent(arFragment.arSceneView.scene)
addPlaces(anchorNode!!)
}
addPlaces
メソッド呼び出しで処理されるシーンで、AnchorNode
に子ノード オブジェクト(PlaceNode
インスタンス)を追加します。
実行
上記の変更を適用したら、アプリを実行し、カメラをかざして平面が検出されるのを待ちます。平面を表す白い点をタップしてください。これで、付近にあるすべての公園のマーカーが地図上に表示されます。ただし、仮想オブジェクトは作成したアンカーに固定されており、空間内の公園の位置関係に基づいて配置されてはいません。
最後のステップでは、デバイスで 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
の結果を使用します。まずは、MainActivity
の addPlaces
に移動しましょう。placeNode
ごとに親が設定されている行(placeNode.setParent(anchorNode)
)のすぐ下で、placeNode
の localPosition
を getPositionVector
の呼び出しの結果に設定します。
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 は完了です!