Технологии дополненной реальности (Augmented Reality, AR) развиваются с первых экспериментов с шлемами в 1968 году и прогнозируются как один из быстрорастущих сегментов развития интерфейсов (особенно при появлении специализированных устройств, таких как Hololens, Xiaomi Smart Glasses и проекта с непонятной судьбой Google Glass). Не могли не заметить этот тренд и разработчики операционных систем для мобильных устройств, Apple выпустила свой набор инструментов ARKit, также как и Google создала набор библиотек ARCore. Особенно важно, что поддержка этих библиотек доступна на большом количестве устройств (для Android нужна версия 7.0 или новее, а это более 94% доступных устройств, при этом почти 90% из них поддерживают Depth API, необходимый для корректной работы алгоритмов размещения объектов виртуального мира в сложном окружении). В этой статье мы рассмотрим основные вопросы использования ARCore и размещения объектов виртуального мира над поверхностями реального.
Прежде всего нужно убедиться, что ваш телефон поддерживает ARCore, для этого необходимо установить Google Play Services for AR по ссылке. В любом случае для разработки можно использовать эмулятор. Для корректной работы на эмуляторе нужно установить этот пакет (версия для эмулятора).
При создании приложения необходимо указать минимальную версию SDK 24, а также необходимость использования AR и, если использование ARCore критично для основной функциональности приложения, также отметить его как Required, например так (в AndroidManifest.xml):
<uses-feature android:name="android.hardware.camera.ar" />
<application>
<meta-data android:name="com.google.ar.core" android:value="required" />
</application>
Также в коде должны быть выполнены вызовы ArCoreApk.checkAvailability()
для проверки доступности библиотеки и ArCoreApk.requestInstall()
для перехода к установке библиотеки. Также в зависимости проекта нужно добавить библиотеку:
dependencies {
//...
implementation("com.google.ar:core:1.36.0")
}
Добавим в onCreate в MainActivity проверку доступности AR:
val availability = ArCoreApk.getInstance().checkAvailability(this)
println("AR is supported: ${availability.isSupported}")
Если возвращается значение true, то мы можем использовать функциональность библиотеки иначе можно запросить установку ArCore:
if (!availability.isSupported) {
val result = ArCoreApk.getInstance().requestInstall(this, false)
when (result) {
ArCoreApk.InstallStatus.INSTALLED -> println("Do some work")
ArCoreApk.InstallStatus.INSTALL_REQUESTED -> println("Just wait for user actions")
}
}
Поскольку дополненная реальность использует камеру телефона, то нужно дополнительно объявить и запросить разрешение для камеры, добавим в AndroidManifest.xml
<uses-feature
android:name="android.hardware.camera"
android:required="false" />
<uses-permission android:name="android.permission.CAMERA"/>
И запросим runtime-разрешение для доступа к камере (CAMERA_REQUEST_CODE=1000):
when (PackageManager.PERMISSION_GRANTED) {
ContextCompat.checkSelfPermission(
this,
Manifest.permission.CAMERA
) -> {
println("Do some job")
}
else -> {
requestPermissions(
arrayOf(Manifest.permission.CAMERA),
CAMERA_REQUEST_CODE
)
}
}
и также обработаем результат действия выдачи разрешения (если было запрошено):
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<out String>,
grantResults: IntArray
) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
if (requestCode== CAMERA_REQUEST_CODE) {
if (grantResults.first() == PackageManager.PERMISSION_GRANTED) {
println("Do some work with camera")
}
}
}
Все взаимодействие с библиотекой выполняется через сессию:
import com.google.ar.core.Config
import com.google.ar.core.Session
lateinit var session: Session
fun onCreate() {
//...инициализация...
session = Session(config)
val config = Config(session)
//здесь можно изменить конфигурацию
//например setFocusMode устанавливает режим фокусировки
//также можно управлять алгоритмами поиска поверхностей (setPlaneFindingMode),
//включать поддержку режима глубины (setDepthMode), управлять способами обнаружения
//источников освещения (setLightEstimationMode) и т.д.
session.configure(config)
}
fun onDestroy() {
session.close()
}
Для создания объектов 3D-сцены будут использоваться возможности OpenGL ES, для этого на сцену необходимо поместить View с типом GLSurfaceView (activity_main.xml):
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<android.opengl.GLSurfaceView
android:id="@+id/surface"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>
Выполним инициализацию контекста и присоединим класс для отрисовки сцены:
val surface = findViewById<GLSurfaceView>(R.id.surface)
surface.setRenderer(MainRenderer())
surface.requestRender()
Сам класс MainRenderer будет наследоваться от android.opengl.GLSurfaceView.Renderer и определять следующие методы:
override fun onSurfaceCreated(gl: GL10?, config: EGLConfig?) {
println("Surface created")
//здесь нужно выполнить инициализацию контекста
}
override fun onSurfaceChanged(gl: GL10?, width: Int, height: Int) {
println("Surface changed")
//здесь можно обновить контекст (например, размеры сцены)
}
override fun onDrawFrame(gl: GL10?) {
println("Draw frame")
val frame = session.update()
//... логика отрисовки
}
Здесь наиболее важным является объект frame, который позволяет как получить доступ к актуальному изображению с камеры и получить результаты распознавания источников света, ключевых точек, поверхностей или поз, например:
acquireCameraImage()
извлекает текущее изображение с камеры;acquireRawDepthImage16Bits()
получить карту глубины для изображения;getLightEstimate()
обнаруживает фоновый источник освещения (если это разрешено в конфигурации);getUpdatedAnchors()
получает список обнаруженных точек привязки (используются для позиционирования объектов в дополненной реальности, могут быть созданы над любым обнаруженным объектом или точкой на поверхности.
Из сессии можно получить информацию об отслеживаемых объектах (включая поверхности) следующим образом:
session.getAllTrackables<Plane>(Plane::class.java)
Также сейчас поддерживается GeoSpatial API для позиционирования объектов с привязкой к географическим координатам (через session.earth), что может быть полезно для отображения подсказок при навигации, визуализации исторических изображений при посещении достопримечательностей и т.д.
Поскольку для отображения на сцене требуется работать с GL-текстурами, удобно использовать готовый рендерер, который умеет работать с фоновым изображением, отображением 3D-моделей и собственных шейдеров. Например можно взять пример отсюда, где поддерживается работа с моделями в формате WaveFront (.obj) и текстурами в PNG. Также можно использовать любую библиотеку для работы поверх EGL.
Наиболее просто начать разработку AR приложения с базового шаблона hello_ar_kotlin, в котором реализовано отображение фоновой текстуры (изображение с камеры), карты глубины, возможность загрузки 3D-моделей и примеры для добавления объектов к якорным точкам (с отслеживанием положения точки при изменении изображения на камере). В этом проекте есть дополнительные helper-классы, упрощающие управление жизненным циклом сессии ARCore и запрос необходимых разрешений (ARCoreSessionLifecycleHelper). Также Sample Renderer представляет базовый класс, который дает возможность загружать 3D-модели и текстуры, отображать растровые изображения как текстуру (класс FrameBuffer), например, изображение с камер, через BackgroundRenderer, работать с 3D-объектами и трансформировать их в набор полигонов (а также загружать из WaveFront OBJ-файла) в классе Mesh, загружать текстуры (класс Texture). В примере можно увидеть как используется поиск поверхностей и добавления якорных точек для размещения 3D-объектов:
Для добавления виртуального объекта необходимо в onSurfaceCreated загрузить mesh (3D-сетку из obj-файла) и текстуры для поверхности и отражения:
virtualObjectAlbedoInstantPlacementTexture =
Texture.createFromAsset(
render,
"models/pawn_albedo_instant_placement.png",
Texture.WrapMode.CLAMP_TO_EDGE,
Texture.ColorFormat.SRGB
)
val virtualObjectPbrTexture =
Texture.createFromAsset(
render,
"models/pawn_roughness_metallic_ao.png",
Texture.WrapMode.CLAMP_TO_EDGE,
Texture.ColorFormat.LINEAR
)
virtualObjectMesh = Mesh.createFromAsset(render, "models/pawn.obj")
virtualObjectShader =
Shader.createFromAssets(
render,
"shaders/environmental_hdr.vert",
"shaders/environmental_hdr.frag",
mapOf("NUMBER_OF_MIPMAP_LEVELS" to cubemapFilter.numberOfMipmapLevels.toString())
)
.setTexture("u_AlbedoTexture", virtualObjectAlbedoTexture)
.setTexture("u_RoughnessMetallicAmbientOcclusionTexture", virtualObjectPbrTexture)
.setTexture("u_Cubemap", cubemapFilter.filteredCubemapTexture)
.setTexture("u_DfgTexture", dfgTexture)
Для отображения обнаруженных поверхностей используется класс PlaneRenderer (в onDraw):
val projectionMatrix = FloatArray(16)
camera.getProjectionMatrix(projectionMatrix, 0, Z_NEAR, Z_FAR)
planeRenderer.drawPlanes(
render,
session.getAllTrackables<Plane>(Plane::class.java),
camera.displayOrientedPose,
projectionMatrix
)
Визуализация присоединенных к якорным точкам объектов выполняется аналогично с использованием матрицы для преобразования экранных координат в мировую систему координат:
val viewMatrix = FloatArray(16)
val projectionMatrix = FloatArray(16)
val modelViewMatrix = FloatArray(16) // view x model
val modelViewProjectionMatrix = FloatArray(16) // projection x view x model
camera.getViewMatrix(viewMatrix, 0)
Matrix.multiplyMM(modelViewProjectionMatrix, 0, projectionMatrix, 0, viewMatrix, 0)
for ((anchor, trackable) in
wrappedAnchors.filter { it.anchor.trackingState == TrackingState.TRACKING }) {
// Get the current pose of an Anchor in world space. The Anchor pose is updated
// during calls to session.update() as ARCore refines its estimate of the world.
anchor.pose.toMatrix(modelMatrix, 0)
// Calculate model/view/projection matrices
Matrix.multiplyMM(modelViewMatrix, 0, viewMatrix, 0, modelMatrix, 0)
Matrix.multiplyMM(modelViewProjectionMatrix, 0, projectionMatrix, 0, modelViewMatrix, 0)
// Update shader properties and draw
virtualObjectShader.setMat4("u_ModelView", modelViewMatrix)
virtualObjectShader.setMat4("u_ModelViewProjection", modelViewProjectionMatrix)
val texture =
if ((trackable as? InstantPlacementPoint)?.trackingMethod ==
InstantPlacementPoint.TrackingMethod.SCREENSPACE_WITH_APPROXIMATE_DISTANCE
) {
virtualObjectAlbedoInstantPlacementTexture
} else {
virtualObjectAlbedoTexture
}
virtualObjectShader.setTexture("u_AlbedoTexture", texture)
render.draw(virtualObjectMesh, virtualObjectShader, virtualSceneFramebuffer)
}
Для добавления якорной точки координаты нажатия на экране преобразуются в координаты на обнаруженной поверхности:
val hitResultList = frame.hitTest(tap)
//находим ближайший к нам
val firstHitResult =
hitResultList.firstOrNull { hit ->
when (val trackable = hit.trackable!!) {
is Plane ->
trackable.isPoseInPolygon(hit.hitPose) &&
PlaneRenderer.calculateDistanceToPlane(hit.hitPose, camera.pose) > 0
is Point -> trackable.orientationMode == Point.OrientationMode.ESTIMATED_SURFACE_NORMAL
is InstantPlacementPoint -> true
is DepthPoint -> true
else -> false
}
}
if (firstHitResult != null) {
wrappedAnchors.add(WrappedAnchor(firstHitResult.createAnchor(), firstHitResult.trackable))
}
Поскольку ARCore принимает на себя основные задачи по обнаружению и отслеживанию объектов (может распознавать двумерные объекты на данный момент, а также есть эксперименты с обнаружение 3D-объектов с использование ARCore ML), поверхностей, точек (например, угловых точек 3D-объектов для измерения расстояний), то в большинстве случае остается только решить задачу позиционирования и ориентировки 3D-объектов по указанным координатам и размещения источников света в виртуальной сцене для корректной визуализации бликов и теней на создаваемых объектах и здесь уже начинается работа с низкоуровневыми методами библиотеки EGL и преобразованиями координат, которые могут быть реализованы через дополнительные библиотеки, например libgdx.
В завершение приглашаю вас на бесплатный урок, в рамках которого рассмотрим Jetpack Compose - современный тулкит от компании Google для создания приложений под ОС Android на языке программирования Kotlin. Jetpack Compose упрощает написание и обновление визуального интерфейса приложения, предоставляя декларативный подход.
dnazarov007
И не забываем про https://developers.google.com/ar/devices