Как Яндекс создавал дополненную реальность в Картах для iOS. Опыт использования ARKit

Остается всё меньше людей, которых можно удивить дополненной реальностью (AR). Для кого-то эта технология ассоциируется с игрушкой на пару часов. Другие находят ей более практичное применение.

Меня зовут Дмитрий, и я разрабатываю Яндекс.Карты для iOS. Сегодня я расскажу читателям Хабра о том, как мы создавали маршрутизацию с использованием дополненной реальности. Вы также узнаете об особенностях применения фреймворка ARKit, благодаря которому внедрение дополненной реальности перестало быть уделом лишь специалистов в области компьютерного зрения.

ar-in-maps-1

В 2009 году журнал Esquire первым среди изданий масс-медиа добавил поддержку дополненной реальности в свой продукт. На обложке журнала разместили код, с помощью которого можно было увидеть Роберта Дауни младшего «вживую».


0v-a585aysdjqwyfrmtrbxgv05k.jpeg

Применение AR в сфере развлечений этим не ограничилось. Ярким примером стала игра Pokemon Go, вышедшая в 2016 году. Уже к июлю того же года её скачали свыше 16 млн раз. Успех игры привёл к появлению многочисленных клонов с AR.

Значимыми событиями в индустрии AR за последние годы можно считать анонсы Google Glass и Microsoft Hololens. Появление подобного рода устройств показывает вектор, в котором движутся крупные компании.

Не стала исключением и Apple. В 2017 году компания представила фреймворк ARKit, значение которого для индустрии трудно переоценить. И о нём мы расскажем подробнее.


ARKit

Особенности ARKit, благодаря которым использовать AR стало просто:


  • отсутствие необходимости в специальных метках (маркерах),
  • интеграция с существующими фреймворками для 2D/3D графики от Apple — SceneKit, SpriteKit, Metal,
  • высокая точность определения позиции и ориентации устройства в пространстве,
  • отсутствие необходимости в калибровке камеры или датчиков.

Под капотом ARKit находится система визуально-инерциальной одометрии, которая объединяет данные с визуальной (камера) и инерциальной (акселлирометр, гироскоп) подсистем устройства для определения положения и смещения на сцене. Связующим элементом этой системы является фильтр Калмана — алгоритм, которые в каждый момент времени выбирает лучшее из показаний двух подсистем и предоставляем его нам в виде нашей позиции и ориентации на сцене. ARKit также обладает «пониманием» сцены — мы можем определять горизонтальные и вертикальные поверхности, а также условия освещенности сцены. Таким образом при добавлении на сцену объекта, мы можем добавить ему дефолтное освещение, благодаря которому объект будет выглядеть более реалистично.


Кстати

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

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


Маршрутизация с AR в Яндекс.Картах

Обычно, после анонса новой версии iOS многие команды в Яндексе собираются для обсуждения возможности внедрения новых фич в свои приложения. Команда Яндекс.Карт поступила так же. В течение месяца с момента анонса ARKit мы нередко обсуждали способы его внедрения в Карты. Каких только идей мы не наслушались друг от друга! Достаточно быстро мы пришли к выводу, что одним из самых полезных и лежащих на поверхности решений является использование дополненной реальности в маршрутизации.

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

Для начала хочу сказать пару слов о маршрутизации. Что я закладываю в это понятие? С точки зрения реализации в мобильном приложении, это достаточно стандартный набор шагов, позволяющих пользователю добраться из точки A в точку B:


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

Мы не будем останавливаться на первых двух пунктах. Скажу только, что мы получаем маршрут через нашу кроссплатформенную библиотеку Яндекс.Mapkit, которая доступна и вам в виде pod«а. Чем же маршрутизация с дополненной реальностью отличается от стандартной маршрутизации в картах? В первую очередь основным отличием является почти полностью скрытая карта. Основной упор делается на область экрана с изображением видеопотока с камеры, на которую накладываются дополнительные визуальные элементы (метка финиша, вспомогательная метка и изображение линии маршрута). Каждый из этих визуальных элементов обладает своей смысловой нагрузкой и своей логикой (когда и как он должен быть отображен). Мы рассмотрим роль каждого из этих элементов более подробно позднее, а пока предлагаю рассмотреть задачи, которые стояли перед нами изначально:


  • научиться позиционировать объекты на сцене ARKit, зная их географические координаты,
  • научиться отрисовывать необходимый UI на 3D сцене с достаточной производительностью.

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

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

Началось всё с изучение инструментов. На тот момент только один человек в команде имел опыт в работе с 3D графикой. Давайте кратко рассмотрим те инструменты, с которыми придется столкнуться любому, кто задумается о реализации подобных идей с помощью ARKit.


Инструменты и API

Основная работа по рендерингу объектов заключается в создании и менеджменте объектов сцены фреймворка SceneKit. С появлением ARKit разработчику стал доступен класс ARSCNView (наследник класса SCNView — базового класса для работы со сценой в SceneKit), который решает большинство трудоемких задач по интеграции ARKit и SceneKit, а именно:


  • синхронизация положения телефона в пространстве с положением камеры на сцене,
  • система координат сцены совпадает с системой координат ARKit,
  • в качестве background’а сцены используется видеопоток с камеры устройства.

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

Для добавления на сцену объектов используется наследники или непосредственно объекты SCNNode. Этот класс представляет позицию (трехмерный вектор) в системе координат своего родительского объекта. Таким образом мы получаем дерево объектов на сцене с корнем в специальном объекте — rootNode нашей сцены. Здесь все очень похоже на иерархию объектов UIView в UIKit. Объекты SCNNode могут быть отображены на сцене при добавлении им материала и освещения.

Для того, чтобы добавить дополненную реальность в мобильное приложение, необходимо также знать об основных объектах API ARKit. Главным из них является объект сессии дополненной реальности — ARSession. Этот объект осуществляет процессинг данных и отвечает за жизненный цикл сессии дополненной реальности. Целью данной статьи не является пересказ документации ARKit и SceneKit, поэтому я не буду писать обо всех доступных параметрах конфигурации сессии дополненной реальности, а остановлюсь на одном из важнейших для навигационных приложений параметре конфигурации сессии дополненной реальности — worldAlignment. Этот параметр определяет направление координат осей сцены в момент инициализации сессии. Вообще, при инициализации сессии дополненной реальности, ARKit создает систему координат с началом в точке, совпадающей с текущим положением телефона в пространстве, и направляет оси этой системы в зависимости от значения свойства woldAlignment. В нашей реализации используется значение gravityAndHeading, которое подразумевает, что оси будут направлены следующим образом: ось Y — в направлении противоположном гравитации, ось Z — на юг, а ось X — на восток.

world-alignment-gravity-and-heading

При удачном стечении обстоятельств оси X/Z действительно будут сонаправлены с направлениями на Юг/Восток, но, из-за погрешностей в показаниях компаса, оси могут быть направлены под некоторым углом к направлению, описанному в документации. Это одна из проблем, с которой нам предстояло бороться, но об этом чуть позже.

Теперь, когда мы рассмотрели основные инструменты, подведем краткий итог: отображение маршрута с использованием SceneKit — это добавление объектов SCNNode на сцену в позиции, полученные путем конвертации из географических координат в координаты сцены. Перед тем как поговорить о конвертации координат и в целом о размещении объектов на сцене давайте поговорим о проблемах отрисовки элементов UI, предполагая, что мы знаем позиции объектов на сцене.

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

finish-placemark-overview


Размер

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

finish-placemark-size-demands

Нам предстояло понять, каким способом можно вклиниться в работающий по определенным правилам механизм проецирования SceneKit. Сразу хочу отметить, что на всё про всё у нас было около двух недель, поэтому времени производить глубокий анализ различных подходов к решению поставленных задач просто не было. Теперь, анализируя наши решения, оценивать их намного проще, и можно сделать вывод, что большинство принятых решений оказались верными. Требование к размеру, по сути, стало первым камнем преткновения. Все изложенные далее проблемы могут быть решены как с помощью SceneKit, так и UIKit. Я постарался подробно изложить способы решения каждой из проблем с использованием обоих подходов. Какой подход использовать, решать только вам.

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


  1. определим положение объекта на сцене — центр сферы,
  2. найдем проекцию этой точки на плоскость экрана (используя API SceneKit),
  3. для определения необходимого размера метки на экране, найдем расстояние от камеры до центра сферы на сцене,
  4. определим необходимый размер на экране по расстоянию до объекта, используя правила описанные в дизайне,
  5. зная размер метки на экране (диаметр окружности), выберем любую точку на этой окружности,
  6. сделаем обратное проецирование (unprojectPoint) выбранной точки,
  7. найдем длину вектора от полученной точки на сцене до центра сферы.

Полученное значение длины вектора и будет искомым радиусом сферы.

finish-placemark-size-solution-scenekit

На момент реализации нам не удалось найти способ определения размера объекта на сцене, и мы решили сделать отрисовку метки финиша с помощью UIKit. Алгоритм в этом случае повторяет шаги 1–5, после чего на экране рисуется окружность нужного размера с центром в точке, полученной в шаге 2 средствами UIKit. Пример реализации метки с использованием UIKit можно найти здесь.


Пару слов о коде

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

Приведенный код не претендует на оптимальность, полноту и production quality =)

Различие использования SceneKit и UIKit в данном случае заключается еще и в том, что при реализации на SceneKit объект SCNNode для конечной точки маршрута (метки финиша) будет создан с материалом и геометрией, так как он должeн быть видимым, в то время как при использовании UIKit объект нода нам понадобится исключительно для поиска проекции на плоскость экрана (для определения цетра метки на экране). Геометрию и материал в этом случае добавлять не нужно. Заметим, что расстояние от камеры до объекта SCNNode конечной точки маршрута можно найти двумя способами — используя географические координаты точек, либо как длину вектора между точками на сцене. Это возможно благодаря тому, что объект камеры является свойством SCNNode. Для получения нода камеры необходимо обратиться к свойству pointOfView нашей сцены.

Мы научились определять радиус нода метки финиша в произвольный момент времени при реализации на SceneKit и положение вьюхи метки финиша в случае реализации на UIKit. Остается понять, когда необходимо обновлять эти значения? Таким местом является метод объекта SCNSceneRendererDelegate:

renderer(_ renderer: SCNSceneRenderer, didRenderScene scene: SCNScene, atTime time: TimeInterval)

Этот метод вызывается после каждого отрендеренного кадра сцены. Обновляя значения свойств в теле этого метода, мы получим корректно отображенную метку финиша.


Анимация

После того, как метка финиша появилась в dev«е, мы приступили к добавлению анимации пульсации к этой метке. Думаю, для большинства iOS разработчиков создание анимаций не представляет особых проблем. Но при обдумывании способа реализации мы столкнулись с проблемой постоянного обновления frame’а нашей вьюхи. Отмечу, что в большинстве случаев анимации добавляются к статическим объектам UIView. Аналогичная проблема — постоянное обновление радиуса геометрии нода возникает и при реализации с помощью SceneKit. Дело в том, что пульсирующая анимация сводится к анимации размера окружности (для UIKit) и радиуса сферы (для SceneKit). Да-да, мы знаем что в UIKit такую анимацию можно сделать, используя CALayer, но для простоты повествования я решил рассмотреть этот вопрос симметрично для двух фреймворков. Рассмотрим реализацию на UIKit. Если добавить к существующему коду, обновляющему фрейм вьюхи код, анимирующий этот же фрейм, то анимация будет сбиваться явной установкой фрейма. Поэтому в качестве решения данной проблемы мы решили использовать анимацию свойства transform.scale.xy объекта UIView. При реализации с использование SceneKit придется добавить анимацию свойства scale для объекта SCNNode. Приятным моментом использования SceneKit в данном случае является тот факт, что он полностью поддерживает CoreAnimation, поэтому учить новое API не обязательно. Код, реализующий анимацию схожую с анимацией метки в Яндекс.Картах, выглядит примерно так:

let animationGroup = CAAnimationGroup.init()
animationGroup.duration = 1.0
animationGroup.repeatCount = .infinity

let opacityAnimation = CABasicAnimation(keyPath: "opacity")
opacityAnimation.fromValue = NSNumber(value: 1.0)
opacityAnimation.toValue = NSNumber(value: 0.1)

let scaleAnimation = CABasicAnimation(keyPath: "scale")
scaleAnimation.fromValue = NSValue(scnVector3: SCNVector3(1.0, 1.0, 1.0))
scaleAnimation.toValue = NSValue(scnVector3: SCNVector3(1.2, 1.2, 1.2))

animationGroup.animations = [opacityAnimation, scaleAnimation]
finishNode.addAnimation(animationGroup, forKey: "animations")


Билборд

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

Начнем с UIKit. В данном случае билборд представляет собой обычный UILabel, в котором постоянно обновляется текст, показывающий расстояние до конечной точки маршрута. Посмотрим на проблему, с которой мы столкнулись.

finish-placemark-billboard-problem-uikit

Если задать лейблу какой-нибудь фрейм, а затем повернуть телефон, мы увидим, что фрейм не изменится (было бы странно, если бы это было не так). В то же время нам хотелось бы, чтобы лейбл оставался параллельным плоскости земли.

finish-placemark-billboard-desired-uikit

Думаю, всем понятно, что при изменении ориентации девайса нам необходимо повернуть лейбл, но на какой угол? Если включить воображение и представить мысленно все оси систем координат и вектора, участвующие в данном процессе, можно прийти к выводу, что угол поворота равен углу между осью x системы координат UIKit и проекцией оси X системы координат SceneKit на плоскость экрана.

finish-placemark-billboard-solution-uikit

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

При реализации метки финиша с использованием SceneKit рендерить билборд с расстоянием вам, скорее всего, придется так же средствами SceneKit, а значит, перед вами определенно возникнет задача заставить объект SCNNode всегда быть ориентированным к камере. Думаю, проблема станет более понятной, если взглянуть на картинку:

finish-placemark-billboard-problem-scenekit

Решается эта задача путем использования SCNBillboardConstraint API. Добавив констрейнт со свободной осью Y в коллекцию констрейнтов нашего нода, мы получим нод, который вращается вокруг оси Y своей системы координат, таким образом, чтобы всегда быть ориентированным к камере. Единственной задачей разработчика становится разместить этот нод на правильной высоте, чтобы билборд с расстоянием всегда был виден пользователю.

let billboardConstraint = SCNBillboardConstraint()
billboardConstraint.freeAxes = SCNBillboardAxis.Y
finishNode.constraints = [billboardConstraint]

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

finish-placemark-hint-overview

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


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

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

Алгоритм поиска центра вспомогательной метки:


  1. создать для конечной точки маршрута объект SCNNode с позицией на сцене, полученной из географической координаты точки,
  2. найти проекцию точки на плоскость экрана,
  3. найти пересечение отрезка из центра экрана до точки найденной проекции с отрезками границ экрана в системе координат экрана.

finish-placemark-hint-solution

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

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

Прежде чем приступить к описанию реализации, важно отметить, что по дизайну, стрелки дожны были находиться на расстоянии 3 м друг от друга. Если оценить количество объектов (стрелок) которое необходимо отрендерить при маршруте длиной около 1 км то оно составит примерно 330 шт. При этом, каждому объекту добавляется анимация движения вдоль своего участка маршрута. Отметим, что стрелки, удаленные от положения камеры на сцене на расстояние порядка 100–150 метров практически не видны из за небольшого размера. Рассмотрев эти факторы было решено не отображать все объекты, а отображать лишь те из них, которые удалены от пользователя не более чем на 100 метров вдоль линии маршрута, периодически обновляя отображаемый набор объектов. Мы отображаем достаточный объем визуальной информации, исключая ненужные вычисления SceneKit и экономя батарейку пользователя.

route-polyline-overview

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


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


Выбор участка для отображения

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

route-polyline-route-part-selection


Создание 3D модели

Рассмотрим более подробно процесс создания 3D модели. В большинстве случаев всё, что вам необходимо сделать для создания простой 3D модели (как наша стрелка), это открыть любой 3D редактор, потратить некоторое время на его освоение и сделать в нем эту модель. В случае, если ребята из вашей команды обладают опытом 3D моделирования, или у них есть время учить, к примеру, 3DMax (и он должен быть куплен), то вам несказанно повезло. К сожалению, на момент реализации этой фичи, особым опытом никто из нас не обладал, свободного времени на обучение не было, поэтому нам пришлось делать модель, так сказать, подручными средствами. Я имею ввиду описание модели в коде. Началось все с представления 3D-модели в виде треугольников. Затем нам пришлось вручную найти координаты вершин этих треугольников в системе координат модели, после чего создать массив индексов вершин треугольников. Имея в распоряжении эти данные, мы можем создать необходимую геометрию прямо в SceneKit. Создать модель, подобную нашей можно, например, так:

class ARSCNArrowGeometry: SCNGeometry {
    convenience init(material: SCNMaterial) {
        let vertices: [SCNVector3] = [
            SCNVector3Make(-0.02,  0.00,  0.00), // 0
            SCNVector3Make(-0.02,  0.50, -0.33), // 1
            SCNVector3Make(-0.10,  0.44, -0.50), // 2
            SCNVector3Make(-0.22,  0.00, -0.39), // 3
            SCNVector3Make(-0.10, -0.44, -0.50), // 4
            SCNVector3Make(-0.02, -0.50, -0.33), // 5
            SCNVector3Make( 0.02,  0.00,  0.00), // 6
            SCNVector3Make( 0.02,  0.50, -0.33), // 7
            SCNVector3Make( 0.10,  0.44, -0.50), // 8
            SCNVector3Make( 0.22,  0.00, -0.39), // 9
            SCNVector3Make( 0.10, -0.44, -0.50), // 10
            SCNVector3Make( 0.02, -0.50, -0.33), // 11
        ]
        let sources: [SCNGeometrySource] = [SCNGeometrySource(vertices: vertices)]
        let indices: [Int32] = [0,3,5, 3,4,5, 1,2,3, 0,1,3, 10,9,11, 6,11,9, 6,9,7, 9,8,7,
                                6,5,11, 6,0,5, 6,1,0, 6,7,1, 11,5,4, 11,4,10, 9,4,3, 9,10,4, 9,3,2, 9,2,8, 8,2,1, 8,1,7]
        let geometryElements = [SCNGeometryElement(indices: indices, primitiveType: .triangles)]
        self.init(sources: sources, elements: geometryElements)
        self.materials = [material]
    }
}

static func arrowBlue() -> SCNGeometry {
    let material = SCNMaterial()
    material.diffuse.contents = UIColor.blue
    material.lightingModel = .constant
    return ARSCNArrowGeometry(material: material)
}

Итоговый результат выглядит так:

route-polyline-arrow-model


Анимация линии маршрута

Следующим этапом на пути к отображению анимированной линии маршрута стал этап создания непосредственно анимации. Но каким способ реализовать анимацию, которая в конечном виде выглядит так, будто стрелка начинает свое движение в начальной точке выбранного участка маршрута и «плывет» вдоль маршрута до конца этого участка?


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

route-polyline-route-part-partitioning

В начале каждого участка мы создаем объект SCNNode стрелки, анимация которого заключается в движении вдоль своего участка.

route-polyline-arrows-initial-position

Как видно, участок анимации иногда состоит из одного сегмента, иногда из двух и более. Всё зависит от шага (в нашем случае — 3 метра) между стрелками и координатами точек, которые составляют маршрут.

Анимация стрелки представляет собой последовательность из двух шагов:


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

Схематично это выглядит так:

route-polyline-arrow-anitaion-steps

Реализовывать подобную анимацию нам показалось проще всего с помощью SCNAction API — декларативное API, позволяющее удобно создавать последовательные, групповые и повторяющиеся анимации. Подробнее посмотреть на реализацию можно тут. Благодаря тому, что каждая стрелка заканчивает свою анимацию в стартовой точке участка анимации следующей стрелки, создается впечатление непрерывного движения стрелки вдоль всего выбранного участка маршрута.

На этом предлагаю закончить рассмотрение различных аспектов рендеринга и перейти к основной части — определение позиций объектов на сцене по географическим координатам объектов.

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


  • геодезические (или географические для простоты) координаты — положение объектов (точек маршрута) в реальном мире,
  • декартовы координаты — положение объектов на сцене (в ARKit). Вспомним, что система координат сцены совпадает с системой координат ARKit (в случае использования ARSCNView).

Перевод из одной системы координат в другую и обратно возможен благодаря тому, что координаты в ARKit измеряются в метрах, а смещение между двумя геодезическими координатами можно с большой точностью перевести в смещение в метрах по осям X и Z системы координат ARKit при небольших смещениях. Напомню, что геодезические координаты — это точки с определенной долготой и широтой.

Давайте вспомним такие важные понятия из курса географии, как параллели и меридианы, и их основные свойства:


  • Параллель — линия с градусным значением широты. Длины различных параллелей различны.
  • Меридиан — линия с градусным значением долготы. Длины всех меридианов одинаковы.

Теперь посмотрим, как можно рассчитать смещение в метрах, между двумя геодезическими координатами с координатами \inline (lat_1,lon_1) и \inline (lat_2, lon_2):

\Delta x = \Delta lon \times metersInLonDegree(lat_{0}), \Delta z = \Delta lat \times metersInLatDegree

metersInLonDegree(\alpha) = \frac{2 \pi R_\text{земли} \cos \left ( \alpha \right ) }{ 360^{°} }, metersInLatDegree = \frac{2 \pi R_\text{земли} }{ 360^{°} }


Пояснение

Cмещение в геодезических координатах линейно мапится в метры только при небольших смещениях. При больших смещениях необходимо честно брать интеграл.

Теперь, когда мы умеем переводить смещение из одной системы координат в другую, нужно определиться с точкой начала отсчета — точка, для которой одновременно известны географическая координата и координата в ARKit (координата на сцене). Найдя такую точку, мы сможем определить координату любого объекта на сцене, зная его географическую координату и используя вышеприведенные формулы.

Для большей ясности рассмотрим пример:
В момент начала сессии дополненной реальности мы запросили у CoreLocation нашу географическую координату и получили её моментально — \inline (lat_0,lon_0). Вспомнив тот факт, что начало системы координат ARKit находится в момент старта сессии в точке, где расположено устройство, мы получили точку начала отсчета, так как нам известна географическая координата и координата на сцене \inline (x_0,y_0,z_0)=(0,0,0). Пусть нам необходимо найти координату на сцене объекта с географической координатой \inline (lat_1,lon_1). Для этого найдем смещение в метрах между географической координатой объекта и географической координатой нашей точки начала отсчета, а затем найденное смещение прибавим к координате на сцене точки начала отсчета. Полученная координата на сцене и будем являться искомой.

coordinates-conversion-object-position-on-scene

Отмечу, что найденная таким способом позиция на сцене будет соответствовать положению объекта в реальном мире только в том случае, если оси X/Z системы координат сцены сонаправлены с направлениям на Юг/Восток. Сонаправленность осей, по идее, должна достигаться за счет установки флага worldAlignment в значение gravitiAndHeading. Но как я уже говорил в начале поста, это далеко не всегда так.

Рассмотрим более подробно способ определения точки начала отсчета. Для этого введем понятие эстимейт — совокупность географической координаты и координаты на сцене.

coordinates-conversion-estimate-definition

Предложенный выше спо

© Habrahabr.ru