Вот и прошел день долгожданного официального релиза 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, что упрощает разработку.
Комментарии (5)
PapaBubaDiop
13.09.2017 17:34+1Можно ли получить расстояние до ключевых точек от камеры и какова относительная точность?
Nagg
hitTest по фича поинтом работает довольно криво :( если нужно мерять горизонтальную поверхность и только пол — можно подождать полноценного ARPlaneAnchor и виртуально отмасштабировать его в бесконечность :)
EverydayTools Автор
В опциях для hitTest можно указать .existingPlane — он будет использовать найденные поверхности без учета их размера (то есть виртуально бесконечные) — или .existingPlaneWithExtent — с учетом размера
Nagg
Да, как вариант. Лично я использую свою геометрию и делаю по ней обычный рейкаст, а не аркитовый.