Отображение ближайших мест в дополненной реальности на Android (Kotlin)

1. Прежде чем начать

Абстрактный

В этой лабораторной работе вы узнаете, как использовать данные платформы Google Maps для отображения ближайших мест в дополненной реальности (AR) на Android.

2344909dd9a52c60.png

Предпосылки

  • Базовое понимание разработки под Android с использованием Android Studio.
  • Знакомство с Котлином

Что вы узнаете

  • Запросить у пользователя разрешение на доступ к камере и местоположению устройства.
  • Интегрируйте с Places API , чтобы получать близлежащие места рядом с местоположением устройства.
  • Интеграция с ARCore для поиска горизонтальных плоских поверхностей, чтобы виртуальные объекты можно было закрепить и разместить в трехмерном пространстве с помощью Sceneform .
  • Собирайте информацию о положении устройства в пространстве с помощью SensorManager и используйте Maps SDK для библиотеки служебных программ Android , чтобы размещать виртуальные объекты в правильном направлении.

Что вам понадобится

  • Android Studio 2020.3.1 или выше
  • Компьютер для разработки, поддерживающий OpenGL ES 3.0 или выше.
  • Устройство с поддержкой ARCore или эмулятор Android с поддержкой ARCore (инструкции приведены на следующем шаге).

2. Настройте

Android-студия

В этой лаборатории кода используется Android 10.0 (уровень API 29) и требуется, чтобы в Android Studio были установлены службы Google Play. Чтобы установить обе эти зависимости, выполните следующие шаги:

  1. Перейдите в диспетчер SDK, доступ к которому можно получить, щелкнув Инструменты > Диспетчер SDK .

6c44a9cb9cf6c236.png

  1. Проверьте, установлен ли Android 10.0. Если нет, установите его, установив флажок рядом с Android 10.0 (Q) , затем нажмите « ОК » и, наконец, еще раз нажмите « ОК » в появившемся диалоговом окне.

368f17a974c75c73.png

  1. Наконец, установите сервисы Google Play, перейдя на вкладку Инструменты SDK , установите флажок рядом с сервисами Google Play , нажмите ОК , затем снова выберите ОК в появившемся диалоговом окне**.**

497a954b82242f4b.png

Требуемые API

На шаге 3 следующего раздела включите Maps SDK для Android и Places API для этой лаборатории кода.

Начало работы с платформой Google Карт

Если вы еще не использовали платформу Google Maps, следуйте руководству по началу работы с платформой Google Maps или просмотрите список воспроизведения Начало работы с платформой Google Maps , чтобы выполнить следующие шаги:

  1. Создайте платежный аккаунт.
  2. Создайте проект.
  3. Включите API и SDK платформы Google Карт (перечислены в предыдущем разделе).
  4. Сгенерируйте API-ключ.

Дополнительно: эмулятор Android

Если у вас нет устройства с поддержкой ARCore, вы также можете использовать эмулятор Android для имитации сцены дополненной реальности, а также для имитации местоположения вашего устройства. Учитывая, что в этом упражнении вы также будете использовать Sceneform , вам также необходимо выполнить действия, описанные в разделе «Настройка эмулятора для поддержки Sceneform».

3. Быстрый старт

Чтобы вы начали как можно быстрее, вот некоторый начальный код, который поможет вам следовать этой кодовой лаборатории. Вы можете перейти к решению, но если вы хотите увидеть все шаги, продолжайте читать.

Вы можете клонировать репозиторий, если у вас установлен git .

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

Кроме того, вы можете нажать кнопку ниже, чтобы загрузить исходный код.

Получив код, откройте проект, найденный в starter каталоге.

4. Обзор проекта

Изучите код, который вы скачали на предыдущем шаге. Внутри этого репозитория вы должны найти один модуль с именем app , который содержит пакет com.google.codelabs.findnearbyplacesar .

AndroidManifest.xml

Следующие атрибуты объявлены в файле 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>

Первая запись метаданных должна указать, что ARCore является требованием для запуска этого приложения, а вторая — как вы предоставляете свой ключ API платформы Google Maps SDK Maps для Android.

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

Вот краткое описание каждой зависимости:

  • Библиотеки с идентификатором группы com.google.android.gms , а именно play-services-location и play-services-maps , используются для доступа к информации о местоположении устройства и доступа к функциям, связанным с Google Maps.
  • com.google.maps.android:maps-utils-ktx — это библиотека расширений Kotlin (KTX) для Maps SDK for Android Utility Library. Функциональность этой библиотеки будет использоваться для последующего позиционирования виртуальных объектов в реальном пространстве.
  • com.google.ar.sceneform.ux:sceneform-ux — это библиотека Sceneform , которая позволит вам визуализировать реалистичные 3D-сцены без необходимости изучения OpenGL.
  • Зависимости внутри идентификатора группы com.squareup.retrofit2 — это зависимости Retrofit , которые позволяют быстро написать HTTP-клиент для взаимодействия с Places API.

Структура проекта

Здесь вы найдете следующие пакеты и файлы:

  • **api — **этот пакет содержит классы, которые используются для взаимодействия с Places API с помощью Retrofit.
  • **ar—**этот пакет содержит все файлы, относящиеся к ARCore.
  • **model — ** этот пакет содержит один класс данных Place , который используется для инкапсуляции одного места, возвращаемого API Places.
  • MainActivity.kt — это единственное Activity , содержащееся в вашем приложении, которое будет отображать карту и вид с камеры.

5. Настройка сцены

Погрузитесь в основные компоненты приложения, начиная с элементов дополненной реальности.

MainActivity содержит SupportMapFragment , который будет обрабатывать отображение объекта карты, и подкласс ArFragmentPlacesArFragment — который обрабатывает отображение сцены дополненной реальности.

Настройка дополненной реальности

Помимо отображения сцены дополненной реальности, PlacesArFragment также будет обрабатывать запрос разрешения камеры от пользователя, если он еще не предоставлен. Дополнительные разрешения также можно запросить, переопределив метод getAdditionalPermissions . Учитывая, что вам также необходимо предоставить разрешение на определение местоположения, укажите это и переопределите метод getAdditionalPermissions :

class PlacesArFragment : ArFragment() {

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

Запустить его

Идите вперед и откройте код скелета в starter каталоге в Android Studio. Если вы нажмете « Выполнить » > «Запустить приложение» на панели инструментов и развернете приложение на своем устройстве или в эмуляторе, вам сначала будет предложено включить разрешение на определение местоположения и камеры. Нажмите « Разрешить », и после этого вы должны увидеть вид с камеры и вид карты рядом, как показано ниже:

e3e3073d5c86f427.png

Обнаружение самолетов

Осмотрев с помощью камеры окружение, в котором вы находитесь, вы можете заметить пару белых точек, наложенных на горизонтальные поверхности, вроде белых точек на ковре на этом изображении.

2a9b6ea7dcb2e249.png

Эти белые точки — рекомендации, предоставленные ARCore, чтобы указать, что горизонтальная плоскость была обнаружена. Эти обнаруженные плоскости позволяют вам создать то, что называется «якорем», чтобы вы могли размещать виртуальные объекты в реальном пространстве.

Для получения дополнительной информации об ARCore и о том, как он понимает окружающую вас среду, прочитайте о его основных концепциях .

6. Найдите близлежащие места

Затем вам нужно получить доступ и отобразить текущее местоположение устройства, а затем выбрать близлежащие места с помощью Places API .

Настройка карт

Ключ API платформы Google Карт

Ранее вы создали ключ API платформы Google Maps, чтобы разрешить запросы к API Places и использовать Maps SDK для Android. Откройте файл gradle.properties и замените строку "YOUR API KEY HERE" на созданный вами ключ API.

Отображение местоположения устройства на карте

После того как вы добавите свой ключ API, добавьте помощника на карту, чтобы помочь пользователям сориентироваться, где они находятся относительно карты. Для этого перейдите к методу setUpMaps и внутри вызова mapFragment.getMapAsync установите для googleMap.isMyLocationEnabled значение true. При этом на карте появится синяя точка.

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

Получить текущее местоположение

Чтобы получить местоположение устройства, вам нужно использовать класс FusedLocationProviderClient . Получение экземпляра этого уже было сделано в методе onCreate MainActivity . Чтобы использовать этот объект, заполните метод getCurrentLocation , который принимает лямбда-аргумент, чтобы местоположение могло быть передано вызывающей стороне этого метода.

Чтобы завершить этот метод, вы можете получить доступ к свойству lastLocation объекта FusedLocationProviderClient , а затем добавить addOnSuccessListener как таковой:

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

Метод getCurrentLocation вызывается из лямбды, предоставленной в getMapAsync в методе setUpMaps , из которого извлекаются близлежащие места.

Инициировать сетевой вызов

Обратите внимание, что при вызове метода getNearbyPlaces методу placesServices.nearbyPlaces передаются следующие параметры: ключ API, местоположение устройства, радиус в метрах (который равен 2 км) и тип места (в настоящее время установлен park ).

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

Чтобы завершить сетевой вызов, передайте ключ API, который вы определили в файле gradle.properties . Следующий фрагмент кода определен в файле build.gradle в конфигурации android > defaultConfig :

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

Это сделает строковое значение ресурса google_maps_key доступным во время сборки.

Чтобы завершить сетевой вызов, вы можете просто прочитать этот строковый ресурс через getString в объекте Context .

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

7. Места в дополненной реальности

До сих пор вы сделали следующее:

  1. Запрашиваемые разрешения на доступ к камере и местоположению у пользователя при первом запуске приложения
  2. Настройте ARCore, чтобы начать отслеживать горизонтальные плоскости
  3. Настройте Maps SDK с помощью своего ключа API
  4. Получил текущее местоположение устройства
  5. Получение близлежащих мест (в частности, парков) с помощью Places API.

Оставшийся шаг для выполнения этого упражнения — разместить места, которые вы выбираете, в дополненной реальности.

Понимание сцены

ARCore может понимать реальную сцену через камеру устройства, обнаруживая интересные и четкие точки, называемые характерными точками, в каждом кадре изображения. Когда эти характерные точки сгруппированы и кажутся лежащими на общей горизонтальной плоскости, например, на столах и полах, ARCore может сделать эту функцию доступной для приложения в виде горизонтальной плоскости.

Как вы видели ранее, ARCore помогает пользователю ориентироваться при обнаружении самолета, отображая белые точки.

2a9b6ea7dcb2e249.png

Добавление якорей

Как только плоскость обнаружена, вы можете прикрепить к ней объект, называемый якорем . С помощью якоря вы можете размещать виртуальные объекты и гарантировать, что эти объекты останутся в одном и том же положении в пространстве. Идите вперед и измените код, чтобы прикрепить его после обнаружения самолета.

В setUpAr OnTapArPlaneListener присоединяется к PlacesArFragment . Этот прослушиватель вызывается всякий раз, когда в сцене дополненной реальности касаются самолета. В этом вызове вы можете создать Anchor и AnchorNode из предоставленного HitResult в прослушивателе как таковом:

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

AnchorNode — это то место, куда вы будете прикреплять объекты дочерних узлов — экземпляры PlaceNode — в сцене, которая обрабатывается в вызове метода addPlaces .

Запустить его

Если вы запустите приложение с указанными выше модификациями, осмотритесь вокруг, пока не будет обнаружен самолет. Идите вперед и нажмите на белые точки, которые указывают на самолет. После этого вы должны увидеть маркеры на карте для всех ближайших парков вокруг вас. Однако, если вы заметили, виртуальные объекты застряли на якоре, который был создан, а не размещен относительно того места, где эти парки находятся в пространстве.

f93eb87c98a0098d.png

На последнем шаге вы исправите это, используя Maps SDK для библиотеки служебных программ Android и SensorManager на устройстве.

8. Расположение мест

Чтобы иметь возможность расположить значок виртуального места в дополненной реальности с точным заголовком, вам понадобятся две части информации:

  • Где истинный север
  • Угол между севером и каждым местом

Определение севера

Север можно определить с помощью имеющихся на устройстве датчиков положения (геомагнитного и акселерометра). С помощью этих двух датчиков можно в режиме реального времени собирать информацию о положении устройства в пространстве. Дополнительные сведения о датчиках положения см. в статье Вычисление ориентации устройства .

Чтобы получить доступ к этим датчикам, вам потребуется получить 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 Utility Library, которая содержит несколько вспомогательных функций для вычисления расстояний и направлений с помощью сферической геометрии. Для получения дополнительной информации прочитайте этот обзор библиотеки .

Далее вы воспользуетесь методом sphericalHeading заголовка в служебной библиотеке, который вычисляет направление/азимут между двумя объектами LatLng . Эта информация необходима внутри метода getPositionVector , определенного в Place.kt . Этот метод в конечном итоге вернет объект Vector3 , который затем будет использоваться каждым PlaceNode как его локальная позиция в пространстве AR.

Идите вперед и замените определение заголовка в этом методе следующим:

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

Местное положение

Последним шагом для правильной ориентации мест в AR является использование результата getPositionVector при добавлении объектов PlaceNode в сцену. Идите вперед и перейдите к addPlaces в MainActivity , прямо под строкой, где родительский элемент установлен для каждого placeNode (прямо под placeNode.setParent(anchorNode) ). Установите для localPosition placeNode результат вызова getPositionVector следующим образом:

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

По умолчанию метод getPositionVector устанавливает расстояние по y узла до 1 метра, как указано значением y в методе getPositionVector . Если вы хотите изменить это расстояние, скажем, на 2 метра, измените это значение по мере необходимости.

С этим изменением добавленные объекты PlaceNode теперь должны быть ориентированы по правильному заголовку. Теперь запустите приложение, чтобы увидеть результат!

9. Поздравления

Поздравляем!

Учить больше