
Технологии дополненной реальности (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 упрощает написание и обновление визуального интерфейса приложения, предоставляя декларативный подход.
ссылка на оригинал статьи https://habr.com/ru/company/otus/blog/720962/
Добавить комментарий