Pull to refresh
0
Lodoss Team
Сложные системы, мобильные приложения, дизайн и AR

Как мы разрабатывали AR-приложение для обзора исторических мест

Reading time7 min
Views5.1K


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

Augmented Reality


Приложения с дополненной реальностью в качестве гидов по городам — тема хорошо известная и реализованная многими разработчиками. Это направление использования AR появилось одним из первых, так как позволяет использовать все очевидные возможности дополненной реальности: показывать пользователям информацию о зданиях, давать справку о работе учреждения и знакомить с достопримечательностями. На последнем хакатоне, который проводился внутри компании, было представлено несколько проектов с применением дополненной реальности, и нам пришла в голову идея создать AR-приложение, которое покажет, как выглядели достопримечательность или историческое место в прошлом. Для этого — объединить современные технологии дополненной реальности со старинными фотографиями. Например, оказавшись перед Исаакиевским собором, можно будет навести на него камеру смартфона и увидеть его первое деревянное здание, которое было разобрано в 1715 году.

Механика работы такая: приложение отображает заданные исторические места и достопримечательности города на карте, выводит на экран краткую информацию о них, при помощи нотификаций оповещает пользователя о том, что он находится неподалеку от интересной точки. Когда человек приближается к историческому памятнику на расстояние 40 метров, становится доступен AR-режим. При этом открывается камера, и краткая информация об объектах отображается прямо в окружающем пользователя пространстве. Последний имеет возможность взаимодействовать с виртуальными объектами: прикоснувшись к карточке исторического места, можно перейти к просмотру альбома с изображениями.

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

Проблема 1. Плавающие точки


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

Для начала решили воспользоваться уже готовой библиотекой для iOS: ARKit-CoreLocation. Проект лежит на GitHub в свободном доступе, содержит помимо кода основных классов примеры интеграции и позволяет выполнить интересующую нас задачу за пару часов. Необходимо только скормить библиотеке координаты точек и изображение, используемое в качестве маркера.

Неудивительно, что за эту легкость пришлось заплатить. Точки-маркеры постоянно плавали в пространстве: то забирались под потолок, то отрисовывались где-то под ногами. Далеко не каждый пользователь согласился бы несколько минут ловить AR-объект в фокус, чтобы ознакомиться с интересующей его информацией.

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

Попробовали в координатах вместо фиксированной высоты над уровнем моря использовать altitude, которую LocationManager возвращал для текущего положения пользователя. Однако полностью проблему это не устранило. Данные, поступающие от Location Manager, начинали прыгать с разбросом до 60 метров, стоило только покрутить устройство в руках. В результате картинка получалась нестабильной, что нас, конечно, снова не устроило.

В итоге решено было отказаться от библиотеки ARKit-CoreLocation и разместить точки в пространстве самостоятельно. Очень сильно в этом помогла статья ARKit and CoreLocation, написанная Кристофером Веб-Оренштейном. Пришлось потратить немного больше времени и освежить в памяти некоторые математические аспекты, но результат того стоил: AR-объекты наконец-то оказались на своих местах. После этого осталось только разбросать их по оси Y, чтобы надписи и точки было проще прочитать, да поставить соответствие между расстоянием от текущего положения до точки и координатой Z AR-объекта, чтобы информация о ближайших исторических местах оказалась на переднем плане.

Понадобилось рассчитать новую позицию SCNNode в пространстве, ориентируясь на координаты:

let place = PlaceNode()
let locationTransform = MatrixHelper.transformMatrix(for: matrix_identity_float4x4, originLocation: curUserLocation, location: nodeLocation, yPosition: pin.yPos, shouldScaleByDistance: false)
let nodeAnchor = ARAnchor(transform: locationTransform)
scene.session.add(anchor: nodeAnchor)
scene.scene.rootNode.addChildNode(place)

В класс MatrixHelper вынесли вспомогательные функции:

class MatrixHelper {

static func transformMatrix(for matrix: simd_float4x4, originLocation: CLLocation, location: CLLocation, yPosition: Float) -> simd_float4x4 {
    	let distanceToPoint = Float(location.distance(from: originLocation))
    	let distanceToNode = (10 + distanceToPoint/1000.0)
    	let bearing = GLKMathDegreesToRadians(Float(originLocation.coordinate.direction(to: location.coordinate)))
    	let position = vector_float4(0.0, yPosition, -distanceToNode, 0.0)
    	let translationMatrix = MatrixHelper.translationMatrix(with: matrix_identity_float4x4, for: position)
    	let rotationMatrix = MatrixHelper.rotateAroundY(with: matrix_identity_float4x4, for: bearing)
    	let transformMatrix = simd_mul(rotationMatrix, translationMatrix)
    	return simd_mul(matrix, transformMatrix)
	}

static func translationMatrix(with matrix: matrix_float4x4, for translation : vector_float4) -> matrix_float4x4 {
    	var matrix = matrix
    	matrix.columns.3 = translation
    	return matrix
	}

static func rotateAroundY(with matrix: matrix_float4x4, for degrees: Float) -> matrix_float4x4 {
    	var matrix : matrix_float4x4 = matrix
   	 
    	matrix.columns.0.x = cos(degrees)
    	matrix.columns.0.z = -sin(degrees)
   	 
    	matrix.columns.2.x = sin(degrees)
    	matrix.columns.2.z = cos(degrees)
    	return matrix.inverse
	}
}

Для расчета азимута добавили расширение CLLocationCoordinate2D

extension CLLocationCoordinate2D {    
	func calculateBearing(to coordinate: CLLocationCoordinate2D) -> Double {
    	let a = sin(coordinate.longitude.toRadians() - longitude.toRadians()) * cos(coordinate.latitude.toRadians())
    	let b = cos(latitude.toRadians()) * sin(coordinate.latitude.toRadians()) - sin(latitude.toRadians()) * cos(coordinate.latitude.toRadians()) * cos(coordinate.longitude.toRadians() - longitude.toRadians())
    	return atan2(a, b)
	}
    
	func direction(to coordinate: CLLocationCoordinate2D) -> CLLocationDirection {
    	return self.calculateBearing(to: coordinate).toDegrees()
	}
} 

Проблема 2. Избыток AR-объектов


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

Тем не менее, в некоторых районах концентрация точек все равно была слишком велика. Поэтому для увеличения наглядности решили использовать кластеризацию. На экране карты эта особенность доступна по умолчанию благодаря логике, заложенной в MapKit, а вот в AR-режиме реализовывать ее пришлось вручную.

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

image

Для обеспечения интерактивности AR-объектов на ARSCNView повесили UITapGestureRecognizer и в обработчике при помощи метода hitTest проверяли, на какой из объектов SCNNode нажал пользователь. Если это оказывалась фотография расположенной неподалеку достопримечательности, приложение открывало соответствующий альбом в полноэкранном режиме.

Проблема 3. Радар


В ходе реализации приложения необходимо было показать точки на небольшом радаре. По идее, недоразумений с этим быть не должно было, ведь азимут и расстояние до точки мы уже вычислили, даже успели преобразовать их в 3D координаты. Оставалось только разместить точки в двумерном пространстве на экране.



Чтобы не изобретать велосипед, обратились к библиотеке Radar, открытый код которой опубликован на GitHub. Яркое превью и гибкие настройки примера обнадеживали, однако на деле точки оказались смещены относительно истинного расположения в пространстве. Провозившись некоторое время в попытках исправить формулы, обратились к менее красивому, но более надежному варианту, описанному в приложении iPhone Augmented Reality Toolkit:

func place(dot: Dot) {
    	var y: CGFloat = 0.0
    	var x: CGFloat = 0.0
   		
	if degree < 0 {
                	degree += 360
            }
    	let bearing = dot.bearing.toRadians()
   	
	let radius: CGFloat = 60.0 // radius of the radar view

    	if (bearing > 0 && bearing < .pi / 2) {
        	//the 1 quadrant of the radar
        	x = radius + CGFloat(cosf(Float((.pi / 2) - bearing)) * Float(dot.distance))
        	y = radius - CGFloat(sinf(Float((.pi / 2) - bearing)) * Float(dot.distance))
    	} else if (bearing > .pi / 2.0 && bearing < .pi) {
        	//the 2 quadrant of the radar
        	x = radius + CGFloat(cosf(Float(bearing - (.pi / 2))) * Float(dot.distance))
        	y = radius + CGFloat(sinf(Float(bearing - (.pi / 2))) * Float(dot.distance))
    	} else if (bearing > .pi && bearing < (3 * .pi / 2)) {
        	//the 3 quadrant of the radar
        	x = radius - CGFloat(cosf(Float((3 * .pi / 2) - bearing)) * Float(dot.distance))
        	y = radius + CGFloat(sinf(Float((3 * .pi / 2) - bearing)) * Float(dot.distance))
    	} else if (bearing > (3 * .pi / 2.0) && bearing < (2 * .pi)) {
        	//the 4 quadrant of the radar
        	x = radius - CGFloat(cosf(Float(bearing - (3 * .pi / 2))) * Float(dot.distance))
        	y = radius - CGFloat(sinf(Float(bearing - (3 * .pi / 2))) * Float(dot.distance))
    	} else if (bearing == 0) {
        	x = radius
        	y = radius - CGFloat(dot.distance)
    	} else if (bearing == .pi / 2) {
        	x = radius + CGFloat(dot.distance)
        	y = radius
    	} else if (bearing == .pi) {
        	x = radius
        	y = radius + CGFloat(dot.distance)
    	} else if (bearing == 3 * .pi / 2) {
        	x = radius - CGFloat(dot.distance)
        	y = radius
    	} else {
        	x = radius
        	y = radius - CGFloat(dot.distance)
    	}
   	 
    	let newPosition = CGPoint(x: x, y: y)
   	 
    	dot.layer.position = newPosition

Backend


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


На момент разрботки мобильного приложения все бекендеры были заняты на коммерческих проектах, а contentful позволил предоставить в течение нескольких часов:

  • мобильному разработчику — удобный бекенд
  • контент-менеджеру — удобную админку для заполнения данных

Подобную реализацию бекенда изначально использовали и команды, которые участвовали в хакатоне (упоминался в начале статьи), что в очередной раз доказывает, что такие вещи, как хакатоны, позволяют отвлечься от решения своих насущных задач на проектах, дают возможность покреативить и попробовать что-то новенькое.

Заключение



Разрабатывать AR-приложение было очень интересно, в процессе мы испробовали несколько готовых библиотек, но также нам пришлось вспомнить математику и много чего написать самим.

Простой, на первый взгляд, проект потребовал немало рабочих часов на реализацию и доводку алгоритмов, несмотря на то, что мы использовали стандартный SDK от Apple.

Недавно мы выложили приложение в AppStore. Вот как оно выглядит в работе.


Пока что у нас в базе есть точки только для Таганрога, однако, все желающие могут поучаствовать в расширении «зоны покрытия».
Tags:
Hubs:
+6
Comments4

Articles

Change theme settings

Information

Website
lodossteam.ru
Registered
Employees
Unknown