
Вот и прошел день долгожданного официального релиза iOS 11, а значит откладывать знакомство с ARKit – SDK производства Apple для создания приложений с дополненной реальностью — больше никак нельзя. О сути инструмента наслышаны многие: с помощью ARKit можно накладывать созданную виртуальную реальность на реальный мир вокруг нас. iPhone или iPad при этом выступают в роли смотрового окна, через которое мы можем наблюдать за происходящим и что-то в нем менять. В Интернете уже представлено немало различных демо-приложений – с их помощью можно расставлять мебель, парковать автомобиль на стоянке, рисовать в окружающем пространстве, создавать двери в другие миры и многое другое. Словом, круг возможностей широкий, нужно только разобраться с технической реализацией.
ARKit поддерживают девайсы исключительно с iOS 11 и процессором A9 или A10. Соотвественно, для написания и запуска приложения нам потребуется, во-первых, Xcode 9, во-вторых, девайс с одним из указанных процессоров и установленной последней версией iOS. Стартовый проект можно скачать отсюда.
ARKit использует данные с камеры и других датчиков девайса, чтобы распознавать ключевые точки и горизонтальные поверхности в окружающем пространстве в режиме реального времени. В скобках отметим, что процесс довольно ресурсозатратный – девайс будет нагреваться. Для начала добавим в метод viewDidLoad() строчку:
sceneView.debugOptions = ARSCNDebugOptions.showFeaturePoints
Это позволит нам видеть ключевые точки, которые находит ARKit. Теперь можно запустить приложение, и через некоторое время перед нами предстанет следующая картина:
/
Стоит отметить, что девайс необходимо немного перемещать в пространстве – в процессе движения в систему будет поступать больше меняющейся информации, чем в неподвижном состоянии. Обилие данных помогает ARKit определять ключевые точки, и в итоге их получается больше.
Для того чтобы «прощупать» возможности ARKit мы возьмем в качестве примера простое приложение-линейку и проследим весь процесс его создания. Сначала нам необходимо реализовать отрисовку линии между двумя точками, затем рассчитать ее длину, настроить вывод результата на экран – и наша примитивная линейка будет готова. Добавим переменные, которые нам понадобятся для отрисовки линии в пространстве:
private var points: (start: SCNVector3?, end: SCNVector3?) private var line = SCNNode() private var isDrawing = false private var canPlacePoint = false
Кортеж points будет содержать в себе точки начала и конца линии. line – это SCNNode, объект, который добавляется в сцену SceneKit, isDrawing – переменная показывающая, закончили мы выбор точек или нет. Переменная сanPlacePoint говорит сама за себя: она показывает, можно ли расположить точку в фокусе. В нашем случае фокусом будет являться центр экрана.
Для того, чтобы определить, можем ли мы поместить точку в фокус, нужно использовать метод hitTest объекта ARSCNVeiw. Этот метод на основе данных ARKit определяет все распознанные объекты и поверхности, пересекающие луч, направленный от камеры, и возвращает в порядке удаления от девайса полученные данные о пересечении.
Использовать его мы будем в ARSCNViewDelegate, в методе
func renderer(_ renderer: SCNSceneRenderer, updateAtTime time: TimeInterval)
чтобы получать данные в режиме реального времени. В итоге у нас получится как-то так:
func renderer(_ renderer: SCNSceneRenderer, updateAtTime time: TimeInterval) { DispatchQueue.main.async { self.measure() } } private func measure() { let hitResults = sceneView.hitTest(view.center, types: [.featurePoint]) if let hit = hitResults.first { canPlacePoint = true focus.image = UIImage(named: "focus") } else { canPlacePoint = false focus.image = UIImage(named: "focus_off") } }
Данный код проверяет наличие результатов hitTest в режиме реального времени, а дальше уже, в зависимости от них, выставляет значение canPlacePoint и изображение нашего фокуса (зеленое или красное). Также в методе hitTest есть список опций, задающий, какие объекты учитывать в реализации – в нашем случае это ключевые точки. При желании можно добавить горизонтальные поверхности.
Тап по экрану будет обозначать начальную либо конечную точку. Непосредственно в функции реализации тапа по экрану мы будем менять переменную isDrawing и обнулять значения начала и конца всякий раз, когда начинаем новую линию:
@objc private func tapped() { if canPlacePoint { isDrawing = !isDrawing if isDrawing { points.start = nil points.end = nil } } }
Вернемся к функции делегата updateAtTime. Имея результат hitTest, мы получаем координаты точки и затем, в зависимости от значения переменных, ставим начальную или конечную точку линии:
if isDrawing { let hitTransform = SCNMatrix4(hit.worldTransform) let hitPoint = SCNVector3Make(hitTransform.m41, hitTransform.m42, hitTransform.m43) if points.start == nil { points.start = hitPoint } else { points.end = hitPoint } }
Теперь у нас есть координаты начала и конца линии – осталось ее начертить. Для этого добавим функцию, которая будет возвращать геометрию линии (по ней SceneKit поймет, как и где рисовать SCNNode):
func lineFrom(vector vector1: SCNVector3, toVector vector2: SCNVector3) -> SCNGeometry { let indices: [Int32] = [0, 1] let source = SCNGeometrySource(vertices: [vector1, vector2]) let element = SCNGeometryElement(indices: indices, primitiveType: .line) return SCNGeometry(sources: [source], elements: [element]) }
И, наконец, добавим код, который будет отрисовывать нашу линию в пространстве:
if points.start == nil { points.start = hitPoint } else { points.end = hitPoint line.geometry = lineFrom(vector: points.start!, toVector: points.end!) if line.parent == nil { line.geometry?.firstMaterial?.diffuse.contents = UIColor.white line.geometry?.firstMaterial?.isDoubleSided = true sceneView.scene.rootNode.addChildNode(line) } }
Теперь, запустив приложение, мы можем провести линию между двумя точками: первый тап отмечает начало и начинает отрисовывать линию к точке в фокусе, второй тап прекращает режим отрисовки и фиксириует линию. Остается только рассчитать ее длину и вывести полученное значение на экран.

func distance(from startPoint: SCNVector3, to endPoint: SCNVector3) -> Float { let vector = SCNVector3Make(startPoint.x - endPoint.x, startPoint.y - endPoint.y, startPoint.z - endPoint.z) let distance = sqrtf(vector.x * vector.x + vector.y * vector.y + vector.z * vector.z) return distance }
Эта функция вычисляет расстояние между двумя точками в пространстве. Дело за малым: добавить текстовое поле и выводить результат в него. В SceneKit 0.01 равняется одному сантиметру. В итоге получаем следущее:

Длина нарисованной линии, по мнению приложения, составляет 9 см, что довольно хорошо соотносится с показаниями реальной линейки. Но, на самом деле, точность не слишком высокая. Максимальная точность получается в тех случаях, когда объекты располагаются на небольшом от камеры расстоянии и измерение производится из положения девайса перпендикулярно поверхности (то есть нужно двигать телефон параллельно поверхности, а не поворачивать его). Измерение на горизонтальных поверхностях будут более точным. Также, если наводить камеру на далекие объекты, hitTest может возвращать невалидные результаты – расстояние до найденных объектов определяется неверно. Хотя здесь нужно оговориться, что все это тестировалось на iPhone 7, у которого нет двойной камеры. Да и если посмотреть на демо различных линеек в интернете, по большей части можно заметить те же самые ограничения и неточности в измерениях.
Вот что получилось в результате.
Если подытожить: ARKit – отличное SDK для создания игр и развлекательных приложений, с ним можно придумать много интересного. Существенная заслуга Apple в том, что они пустили дополненную реальность в массы: девайсов, поддерживающих ARKit, довольно много и теперь уже не нужно приобретать специальные шлемы и прочие аксессуары. К тому же, ARKit поддерживает работу как и с нативными SpriteKit SceneKit и Metal, так и с Unity и Unreal Engine, что упрощает разработку.