Вот и прошел день долгожданного официального релиза 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)


  1. Nagg
    13.09.2017 16:03
    +1

    hitTest по фича поинтом работает довольно криво :( если нужно мерять горизонтальную поверхность и только пол — можно подождать полноценного ARPlaneAnchor и виртуально отмасштабировать его в бесконечность :)


    1. EverydayTools Автор
      14.09.2017 06:55

      В опциях для hitTest можно указать .existingPlane — он будет использовать найденные поверхности без учета их размера (то есть виртуально бесконечные) — или .existingPlaneWithExtent — с учетом размера


      1. Nagg
        14.09.2017 09:46

        Да, как вариант. Лично я использую свою геометрию и делаю по ней обычный рейкаст, а не аркитовый.


  1. PapaBubaDiop
    13.09.2017 17:34
    +1

    Можно ли получить расстояние до ключевых точек от камеры и какова относительная точность?


    1. Nagg
      14.09.2017 00:03

      Ну как раз hitTest с флагом featurepoints это и делает