Людей, которых можно удивить дополненной реальностью (AR), остаётся всё меньше.
У некоторых эта технология на пару часов ассоциируется с игрушкой.
Другие находят этому более практическое применение.
Меня зовут Дмитрий, я разрабатываю Яндекс.
Карты для iOS. Сегодня я расскажу читателям Хабра о том, как мы создавали маршрутизацию с помощью дополненной реальности.
Также вы узнаете об особенностях использования фреймворка ARKit, благодаря которому реализация дополненной реальности больше не является уделом только специалистов в области компьютерного зрения.
В 2009 году журнал Esquire первым среди СМИ добавил в свой продукт поддержку дополненной реальности.
На обложке журнала разместили код, с помощью которого можно было увидеть Роберта Дауни-младшего «вживую».
На этом использование AR в сфере развлечений не заканчивается.
Ярким примером стала игра Pokemon Go, вышедшая в 2016 году.
К июлю того же года ее скачали более 16 миллионов раз.
Успех игры привел к появлению многочисленных клонов с AR. К значимым событиям в индустрии AR последних лет относятся анонсы Google Glass и Microsoft Hololens. Появление такого рода устройств показывает вектор, по которому движутся крупные компании.
Apple не стала исключением.
В 2017 году компания представила фреймворк ARKit, важность которого для отрасли сложно переоценить.
И мы расскажем вам об этом подробнее.
ARKit
Особенности ARKit, которые упрощают использование AR:- нет необходимости в специальных пометках (маркерах),
- интеграция с существующими фреймворками для 2D/3D графики от Apple — SceneKit, SpriteKit, Metal,
- высокая точность определения положения и ориентации устройства в пространстве,
- нет необходимости калибровать камеру или датчики.
Связующим элементом этой системы является фильтр Калмана — алгоритм, который в каждый момент времени выбирает лучшее из показаний двух подсистем и предоставляет его нам в виде нашего положения и ориентации на сцене.
ARKit также обладает пониманием сцены — мы можем определять горизонтальные и вертикальные поверхности, а также условия освещения сцены.
Таким образом, при добавлении объекта в сцену мы можем добавить к нему освещение по умолчанию, что сделает объект более реалистичным.
Кстати Вскоре будет выпущена версия 2.0 платформы, которая добавит новые функции и значительно повысит точность позиционирования.
ARKit позволил разработчикам внедрять высококачественную дополненную реальность в свои приложения с гораздо меньшими усилиями.
Продемонстрируем это на примере Яндекс.
Карт.
Маршрутизация с AR в Яндекс.
Картах Обычно после анонса новой версии iOS многие команды Яндекса собираются, чтобы обсудить возможность внедрения новых функций в свои приложения.
То же самое сделала команда Яндекс.
Карт. За месяц, прошедший с момента анонса ARKit, мы часто обсуждали способы его реализации в Картах.
Мы услышали столько идей друг от друга! Довольно быстро мы пришли к выводу, что одним из самых полезных и очевидных решений является использование дополненной реальности в маршрутизации.
Выбор этой идеи был обусловлен тем, что многие пользователи карт часто сталкиваются с ситуацией, когда оказываешься в незнакомой местности и нужно быстро решить, куда идти.
Стандартный подход среднестатистического пользователя карты — открыть приложение, построить пешеходный маршрут и, повернувшись на месте, определить, куда идти.
Идея внедрения дополненной реальности в пешеходные маршруты — избавить пользователя от лишних действий, сразу показав, куда идти, прямо поверх изображения с камеры.
Сначала хочу сказать несколько слов о маршрутизации.
Что я подразумеваю под этим понятием? С точки зрения реализации в мобильном приложении это достаточно стандартный набор шагов, позволяющий пользователю добраться из точки А в точку Б:
- выбор пунктов отправления и прибытия,
- получение маршрута в виде набора точек в географических (широта, долгота) координатах,
- отображение линии маршрута на карте,
- предоставление пользователю дополнительной информации во время движения по маршруту.
Скажу лишь, что маршрут мы получаем через нашу кроссплатформенную библиотеку Яндекс.
Мапкит, которая также доступна вам в виде пода.
Чем маршрутизация в дополненной реальности отличается от стандартной маршрутизации по карте? Прежде всего, главное отличие — практически полностью скрытая карта.
Основное внимание уделяется области экрана с изображением видеопотока с камеры, на которую накладываются дополнительные визуальные элементы (финишная метка, вспомогательная метка и изображение линии маршрута).
Каждый из этих визуальных элементов имеет свою смысловую нагрузку и свою логику (когда и как его следует отображать).
Подробнее роль каждого из этих элементов мы рассмотрим позже, а пока предлагаю рассмотреть задачи, которые стояли перед нами изначально:
- научиться позиционировать объекты на сцене ARKit, зная их географические координаты,
- научитесь рисовать необходимый UI на 3D сцене с достаточной производительностью.
Но все оказалось немного сложнее, чем казалось на первый взгляд. Прежде чем приступить к реализации самой функции, одному из моих коллег была поставлена задача сделать прототип, показывающий возможность (или невозможность) реализации такого функционала имеющимся набором инструментов.
Две недели мы наблюдали, как Сан Саныч бороздит просторы и прилегающие территории нашего офиса с телефоном в руках и смотрит на окружающий мир через призму своей камеры.
В итоге мы получили рабочий прототип, который показывал каждую путевую точку в виде маркера на сцене с указанием расстояния до нее.
С помощью этого прототипа можно было при удачном стечении обстоятельств дойти с работы до метро пешком и практически ни разу не заблудиться.
А если серьезно, то он подтвердил возможность реализации задуманного функционала.
Но оставался ряд задач, которые нашей команде еще предстояло решить.
Все началось с изучения инструментов.
На тот момент только один человек в команде имел опыт работы с 3D-графикой.
Давайте кратко рассмотрим инструменты, с которыми столкнется каждый, кто задумывается о реализации подобных идей с помощью ARKit.
Инструменты и API
Основная работа по рендерингу объектов — создание объектов сцены и управление ими в среде SceneKit. С появлением ARKit у разработчика появился доступ к классу ARSCNView (потомок класса SCNView, базовому классу для работы со сценами в SceneKit), который решает большую часть трудоемких задач по интеграции ARKit и SceneKit, а именно:- синхронизация положения телефона в пространстве с положением камеры на сцене,
- система координат сцены соответствует системе координат ARKit,
- В качестве фона сцены используется видеопоток с камеры устройства.
Для добавления объектов на сцену используются преемники или сами объекты SCNNode. Этот класс представляет позицию (3D-вектор) в системе координат его родительского объекта.
Таким образом, мы получаем дерево объектов на сцене с корнем в специальном объекте — rootNode нашей сцены.
Это очень похоже на иерархию объектов UIView в UIKit. Объекты SCNNode можно визуализировать в сцене, добавляя к ним материал и освещение.
Чтобы добавить дополненную реальность в мобильное приложение, вам также необходимо знать об основных объектах API ARKit. Главным из них является объект сеанса дополненной реальности — ARSession. Этот объект обрабатывает данные и отвечает за жизненный цикл сеанса дополненной реальности.
Цель этой статьи — не пересказывать документацию по ARKit и SceneKit, поэтому я не буду писать обо всех доступных параметрах конфигурации AR-сессии, а остановлюсь на одном из важнейших параметров конфигурации AR-сессии для навигационных приложений — worldAlignment. Этот параметр определяет направление координат осей сцены в момент инициализации сессии.
В целом, при инициализации сеанса дополненной реальности ARKit создает систему координат, начало которой находится в точке, совпадающей с текущим положением телефона в пространстве, и направляет оси этой системы в зависимости от значения свойства woldAlignment. В нашей реализации используется значение GravityAndHeading, что означает, что оси будут направлены следующим образом: ось Y находится в направлении, противоположном гравитации, ось Z — на юг, а ось X — на восток.
При удачном стечении обстоятельств оси X/Z действительно будут совмещены с направлениями на Юг/Восток, но из-за ошибок в показаниях компаса оси могут быть направлены под некоторым углом к направлению, описанному в документации.
Это одна из проблем, с которой нам пришлось бороться, но об этом позже.
Теперь, когда мы рассмотрели основные инструменты, давайте кратко подведем итоги: картографирование маршрутов с помощью SceneKit заключается в добавлении SCNNodes к сцене в позициях, полученных путем преобразования географических координат в координаты сцены.
Прежде чем говорить о преобразовании координат и вообще о размещении объектов на сцене, давайте поговорим о проблемах отрисовки элементов пользовательского интерфейса, предполагая, что мы знаем положения объектов на сцене.
Финишная отметка Основным визуальным элементом пешеходного маршрута дополненной реальности является маркер финиша, отображающий конечную точку маршрута.
Мы также показываем пользователю расстояние до конечной точки маршрута над меткой.
Размер
Когда нам впервые показали дизайн этой бирки, первое, на что мы обратили внимание, это требования к размеру этой бирки.Они не подчинялись правилам перспективной проекции.
Поясню, что в 3D-движках, которые используются для создания, например, компьютерных игр, «взгляд» моделируется с помощью перспективной проекции.
По правилам перспективной проекции удаленные предметы изображаются в меньшем масштабе, а параллельные линии вообще не являются параллельными.
Таким образом, размер проекции объекта на плоскость экрана линейно изменяется (уменьшается) по мере удаления камеры от объекта на сцене.
Из описания раскладок следует, что размер отметки на экране имеет фиксированный (максимальный) размер на расстоянии менее 50 м, затем линейно уменьшается от 50 м до 2 км, после чего минимальный размер остается неизменным.
.
Такие требования, очевидно, обусловлены удобством пользователя.
Они позволяют пользователю никогда не терять из виду конечную точку маршрута, поэтому пользователь всегда будет иметь представление, куда идти.
Нам нужно было придумать, как вклиниться в основанный на правилах механизм проецирования SceneKit. Сразу хочу отметить, что на все про все у нас было около двух недель, поэтому времени на углубленный анализ различных подходов к решению поставленных задач просто не было.
Теперь, анализируя наши решения, их гораздо легче оценить, и мы можем сделать вывод, что большинство принятых решений оказались правильными.
Требование к размеру, по сути, стало первым камнем преткновения.
Все описанные ниже проблемы можно решить, используя как SceneKit, так и UIKit. Я постарался подробно описать, как решить каждую проблему, используя оба подхода.
Какой подход использовать – решать вам.
Давайте представим, что мы решили реализовать маркер финиша с помощью SceneKit. Если учесть, что метка по макетам должна была выглядеть на экране как круг, то становится очевидным, что в SceneKit объект метки должен быть сферой (поскольку проекция сферы на любую плоскость — это круг).
Чтобы проекция имела на экране определенный радиус, указанный в требованиях дизайнеров, необходимо знать радиус сферы в каждый момент времени.
Таким образом, разместив на сцене в определенной точке сферу определенного радиуса и постоянно обновляя ее радиус по мере ее приближения или удаления, мы получим проекцию на экран необходимого размера в каждый момент времени.
Алгоритм определения радиуса сферы в произвольный момент времени следующий:
- определяем положение объекта на сцене – центр сферы,
- найдем проекцию этой точки на плоскость экрана (с помощью API SceneKit),
- для определения необходимого размера отметки на экране находим расстояние от камеры до центра сферы на сцене,
- определим необходимый размер на экране исходя из расстояния до объекта, используя правила, описанные в проекте,
- зная размер метки на экране (диаметр круга), выберите любую точку на этом круге,
- давайте проецируем (unprojectPoint) выбранную точку,
- Найдем длину вектора от полученной точки на сцене до центра сферы.
На момент реализации нам не удалось найти способ определения размера объекта на сцене, и мы решили визуализировать финишную отметку с помощью UIKit. В этом случае алгоритм повторяет шаги 1-5, после чего на экране с помощью UIKit рисуется круг необходимого размера с центром в точке, полученной на шаге 2. Пример реализации метки с использованием UIKit можно найти Здесь .
Несколько слов о коде В конце статьи я привел несколько ссылок на полезные и просто интересные материалы, в том числе примеры, в которых можно подробно рассмотреть реальный код, решающий представленные в статье задачи и реализующий заданные алгоритмы.
На мой взгляд, основной интерес прототип пешеходной маршрутизации , объединяющий в себе весь функционал, за исключением механизма регулировки оси, который подробно описан ниже.
Данный код не претендует на оптимальность, полноту и качество продукции =) Разница между использованием SceneKit и UIKit в данном случае заключается в том, что при реализации на SceneKit объект SCNNode для конечной точки маршрута (маркера финиша) будет создан с материалом и геометрией, поскольку он должен быть видимым, а при использовании UIKit Объект node нам понадобится исключительно для нахождения проекции на плоскость экрана (для определения центра метки на экране).
В этом случае нет необходимости добавлять геометрию и материал.
Обратите внимание, что расстояние от камеры до объекта SCNNode конечной точки маршрута можно найти двумя способами — с помощью географических координат точек или как длину вектора между точками на сцене.
Это возможно, поскольку объект камеры является свойством SCNNode. Чтобы получить узел камеры, вам необходимо получить доступ к свойству pointOfView нашей сцены.
Мы научились определять радиус узла финишной метки в произвольный момент времени при реализации в SceneKit и положение вида финишной метки при реализации в UIKit. Осталось понять, когда необходимо обновить эти значения? Таким местом является метод объекта SCNSceneRendererDelegate:
Этот метод вызывается после рендеринга каждого кадра сцены.renderer(_ renderer: SCNSceneRenderer, didRenderScene scene: SCNScene, atTime time: TimeInterval)
Обновив значения свойств в теле этого метода, мы получим корректное отображение метки завершения.
Анимация
Как только в dev появился маркер финиша, мы приступили к добавлению к маркеру пульсирующей анимации.Думаю, для большинства iOS-разработчиков создание анимации не представляет особых проблем.
Но, думая о методе реализации, мы столкнулись с проблемой постоянного обновления рамки нашего представления.
Обратите внимание, что в большинстве случаев анимация добавляется к статическим объектам UIView. Аналогичная проблема — постоянное обновление радиуса геометрии узла происходит при реализации с помощью SceneKit. Дело в том, что пульсирующая анимация сводится к анимации размера круга (для UIKit) и радиуса сферы (для SceneKit).
Да-да, мы знаем, что в UIKit такую анимацию можно сделать с помощью CALayer, но для простоты рассказа я решил рассмотреть этот вопрос симметрично для двух фреймворков.
Давайте посмотрим на реализацию на UIKit. Если к существующему коду, обновляющему кадр просмотра, добавить код, анимирующий тот же кадр, то анимация будет сбиваться из-за явной настройки кадра.
Поэтому в качестве решения этой проблемы мы решили использовать анимацию свойства Transform.scale.xy объекта UIView. При реализации с использованием SceneKit вам придется добавить анимацию свойства масштаба для объекта 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, в котором постоянно обновляется текст, показывающий расстояние до конечной точки маршрута.
Давайте посмотрим на проблему, с которой мы столкнулись.
Если дать метке рамку, а затем повернуть телефон, то мы увидим, что рамка не меняется (было бы странно, если бы это было не так).
В то же время нам бы хотелось, чтобы метка оставалась параллельной плоскости заземления.
Я думаю все понимают, что при изменении ориентации устройства нам нужно повернуть метку, но на какой угол? Если включить воображение и мысленно представить себе все оси систем координат и векторы, участвующие в этом процессе, то можно прийти к выводу, что угол поворота равен углу между осью x системы координат UIKit и проекция оси X системы координат SceneKit на плоскость экрана.
Простая задача, которая еще раз доказала полезность школьного курса геометрии.
При реализации маркера финиша с помощью SceneKit вам, скорее всего, придется визуализировать рекламный щит с расстоянием с помощью SceneKit, а значит перед вами обязательно встанет задача заставить объект SCNNode всегда быть ориентирован на камеру.
Думаю, проблема станет понятнее, если посмотреть на картинку:
Эта проблема решается с помощью API SCNBillboardConstraint. Добавив в коллекцию ограничений нашего узла ограничение со свободной осью Y, мы получим узел, который вращается вокруг оси Y своей системы координат, чтобы всегда быть ориентированным на камеру.
Единственная задача разработчика — разместить этот узел на правильной высоте, чтобы дистанционный билборд всегда был виден пользователю.
let billboardConstraint = SCNBillboardConstraint()
billboardConstraint.freeAxes = SCNBillboardAxis.Y
finishNode.constraints = [billboardConstraint]
Вспомогательная этикетка Одной из основных особенностей пешеходных маршрутов с дополненной реальностью внутри команды мы считаем вспомогательный маркер — специальный визуальный элемент, который появляется на экране в тот момент, когда конечная точка маршрута исчезает из поля зрения и показывает пользователю куда повернуть телефон, чтобы на экране появился маркер финиша
Уверен, многие наши читатели сталкивались с подобным функционалом в некоторых играх, чаще всего шутерах.
Каково же было удивление нашей команды, когда мы увидели этот элемент пользовательского интерфейса в макетах.
Скажу сразу, правильная реализация такой возможности может потребовать не один час экспериментов, но конечный результат стоит потраченного времени.
Начали мы с определения требований, а именно:
- при любой ориентации устройства метка перемещается по границам экрана,
- если пользователь повернулся на 180 градусов к конечной точке маршрута, внизу экрана отображается метка,
- в каждый момент времени поворот на знак должен быть кратчайшим поворотом до конечной точки маршрута.
Практически сразу мы пришли к выводу, что рендеринг будет осуществляться с помощью UIKit. Основной проблемой при реализации было определение центра этой отметки в каждый момент времени.
После рассмотрения финишного знака эта задача не должна представлять никаких затруднений, поэтому я не буду подробно останавливаться на ее решении.
В статье я приведу только описание алгоритма выбора центра вспомогательной метки, а исходный код можно посмотреть здесь .
Алгоритм нахождения центра вспомогательной метки:
- создать объект SCNNode для конечной точки маршрута с позицией на сцене, полученной из географических координат точки,
- найти проекцию точки на плоскость экрана,
- найти пересечение отрезка от центра экрана до точки найденной проекции с отрезками границ экрана в экранной системе координат.
Найденная точка пересечения является искомым центром вспомогательной метки.
По аналогии с кодом, обновляющим параметры финишной метки, мы поместили код, рисующий вспомогательную метку, в уже упомянутый выше метод делегата.
Полилиния маршрута Построив маршрут и увидев на экране отметку финиша, пользователь может дойти до нее, ориентируясь только по направлению к отметке, но маршрутизация называется так, потому что она показывает маршрут пользователю.
Мы подумали, что было бы очень странно урезать функционал маршрутизации пешеходов, исключив отображение маршрута из AR-версии.
Для визуализации линии маршрута было решено отобразить набор движущихся по ней стрелок.
В данном случае дизайнеров удовлетворило то, что по мере удаления стрелки практически исчезнут (размер будет определяться правилами перспективной проекции), и для реализации было решено использовать SceneKit. Прежде чем описывать реализацию, важно отметить, что по задумке стрелки должны были находиться на расстоянии 3 м друг от друга.
Если прикинуть количество объектов (стрелок), которые необходимо отрисовать для маршрута длиной около 1 км, то оно составит примерно 330 штук.
При этом к каждому объекту добавляется анимация движения по своему участку маршрута.
Обратите внимание, что стрелки, расположенные на расстоянии около 100-150 метров от положения камеры на сцене, практически незаметны из-за своих небольших размеров.
Учитывая эти факторы, было принято решение не отображать все объекты, а отображать только те, которые находятся на расстоянии не более 100 метров от пользователя по линии маршрута, периодически обновляя отображаемый набор объектов.
Мы показываем достаточно визуальной информации, исключая ненужные вычисления SceneKit и экономя заряд батареи пользователя.
Давайте рассмотрим основные шаги, которые нам пришлось реализовать для получения конечного результата:
- выбираем участок маршрута, для которого будем отображать примитивы,
- создание 3D моделей,
- создание анимации,
- обновление при движении по маршруту.
Выбор области для отображения
Как я уже отмечал выше, мы не отображаем стрелки на весь маршрут, а выбираем для отображения оптимальный участок.Выбор участка в произвольный момент времени заключается в поиске ближайшего к текущему положению пользователя сегмента маршрута (маршрут — последовательность сегментов/участков) и выборе сегментов от ближайшей к конечной точке маршрута до их общей длины.
не превышает 100 метров.
Создание 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)
}
Окончательный результат выглядит так:
Анимация линии маршрута
Следующим этапом на пути к отображению анимированной линии маршрута стал этап создания самой анимации.Но как реализовать анимацию, которая в конечном виде выглядит так, будто стрелка начинает свое движение в начальной точке выбранного участка маршрута и «плывет» по маршруту до конца этого участка? Я не буду описывать все возможные способы создания такой анимации; вместо этого я остановлюсь подробнее на методе, который мы выбрали.
После того, как участок маршрута выбран, мы делим его на участки равной длины — участки анимации.
Теги: #iOS #Разработка для iOS #Разработка мобильных приложений #Разработка для AR и VR #разработка ios #AR и VR #дополненная реальность #AR #Карты Яндекса
-
Цели И Задачи Lug, Как Вы Их Понимаете.
19 Oct, 24 -
Сетевая Инфраструктура Hp Helion Openstack
19 Oct, 24 -
Mips 2014: Потрачено
19 Oct, 24 -
Выбор Цвета/Нахождение Цветовой Ниши
19 Oct, 24