Статьи

Кодирование измерительного приложения с помощью ARKit: взаимодействие и измерение

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

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

  • Дополненная реальность
    Зашифруйте свое первое приложение дополненной реальности с ARKit

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

Первым шагом при проверке касаний является создание распознавателя жестов касания при запуске приложения. Для этого создайте обработчик касаний следующим образом:

1
2
// Creates a tap handler and then sets it to a constant
let tapRecognizer = UITapGestureRecognizer(target: self, action: #selector(handleTap))

Первая строка создает экземпляр класса UITapGestureRecognizer() и передает при инициализации два параметра: цель и действие. Цель — получатель уведомлений, которые отправляет этот распознаватель, и мы хотим, чтобы наш класс ViewController был целью. Действие — это просто метод, который следует вызывать каждый раз, когда происходит касание.

Чтобы установить количество нажатий, добавьте это:

1
2
// Sets the amount of taps needed to trigger the handler
tapRecognizer.numberOfTapsRequired = 1

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

Добавьте обработчик в представление сцены следующим образом:

1
2
// Adds the handler to the scene view
sceneView.addGestureRecognizer(tapRecognizer)

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

Когда мы создали UITapGestureRecognizer() , вы можете помнить, что мы установили метод handleTap для действия. Теперь мы готовы объявить этот метод. Для этого просто добавьте в свое приложение следующее:

1
2
3
@objc func handleTap(sender: UITapGestureRecognizer) {
    // Your code goes here
}

Хотя объявление функции может быть довольно понятным, вы можете задаться вопросом, почему перед ним @objc тег @objc . Начиная с текущей версии Swift, чтобы предоставлять методы Objective-C, вам нужен этот тег. Все, что вам нужно знать, это то, что #selector нужен, чтобы указанный метод был доступен для Objective-C. Наконец, параметр метода позволит нам получить точное местоположение, которое было нажато на экране.

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

Начните с добавления следующих трех строк кода в ваш handleTap() :

1
2
3
4
5
6
7
8
// Gets the location of the tap and assigns it to a constant
let location = sender.location(in: sceneView)
 
// Searches for real world objects such as surfaces and filters out flat surfaces
let hitTest = sceneView.hitTest(location, types: [ARHitTestResult.ResultType.featurePoint])
 
// Assigns the most accurate result to a constant if it is non-nil
guard let result = hitTest.last else { return }

Если вы помните параметр, который мы взяли в handleTap() , вы можете вспомнить, что он был назван sender и имел тип UITapGestureRecognizer . Ну, эта первая строка кода просто берет местоположение касания на экране (относительно вида сцены) и устанавливает его в постоянное именованное location .

Далее, мы делаем что-то, называемое тестом попадания на самом SceneView . Проще говоря, это проверка сцены на предмет реальных объектов, таких как таблицы, поверхности, стены, полы и т. Д. Это позволяет нам получить представление о глубине и получить довольно точные измерения между двумя точками. Кроме того, мы указываем типы обнаруживаемых объектов и, как вы можете видеть, мы советуем ему искать featurePoints , которые по сути являются плоскими поверхностями, что имеет смысл для измерительного приложения.

Наконец, строка кода принимает наиболее точный результат, который в случае hitTest является последним результатом, и проверяет, не равен ли он nil . Если это так, он игнорирует остальные строки в этом методе, но если результат действительно есть, он будет присвоен константе, которая называется result .

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

Добавьте следующие строки в ваш handleTap() , и мы подробно рассмотрим их:

1
2
3
4
5
6
7
8
// Converts the matrix_float4x4 to an SCNMatrix4 to be used with SceneKit
let transform = SCNMatrix4.init(result.worldTransform)
 
// Creates an SCNVector3 with certain indexes in the matrix
let vector = SCNVector3Make(transform.m41, transform.m42, transform.m43)
 
// Makes a new sphere with the created method
let sphere = newSphere(at: vector)

Прежде чем перейти к первой строке кода, важно понимать, что тест на попадание, который мы делали ранее, возвращает тип matrix_float4x4 , который по сути представляет собой матрицу значений с плавающей запятой размером четыре на четыре. Так как мы находимся в   SceneKit, тем не менее, нам нужно преобразовать в нечто, что SceneKit может понять — в данном случае, в SCNMatrix4 .

Затем мы будем использовать эту матрицу для создания SCNVector3 , который, как следует из его названия, представляет собой вектор с тремя компонентами. Как вы уже догадались, эти компоненты x x , y и z , чтобы дать нам положение в пространстве. transform.m41 , transform.m42 и transform.m43 являются соответствующими значениями координат для трех векторов компонентов.

Наконец, давайте используем метод newSphere() который мы создали ранее, вместе с информацией о местоположении, которую мы проанализировали из события касания, чтобы создать сферу и присвоить ее константе, называемой sphere .

Теперь вы, возможно, поняли небольшой недостаток в нашем коде; если пользователь продолжает нажимать, новая сфера будет продолжать создаваться. Мы не хотим этого, потому что это затрудняет определение того, какие сферы должны быть измерены. Кроме того, пользователю сложно отслеживать все сферы!

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

1
var spheres: [SCNNode] = []

Это массив SCNNodes потому что это тип, который мы вернули из нашего newSphere() который мы создали в начале этого урока. Позже мы поместим сферы в этот массив и проверим, сколько их. Исходя из этого, мы сможем манипулировать их номерами, удаляя и добавляя их.

Далее мы будем использовать серию операторов if-else и циклов for, чтобы выяснить, есть ли сферы в массиве или нет. Для начала добавьте следующую необязательную привязку к своему приложению:

1
2
3
4
5
if let first = spheres.first {
    // Your code goes here
} else {
    // Your code goes here
}

Во-первых, мы проверяем, есть ли какие-либо элементы в массиве spheres , и если нет, выполняем код в else else пункт.

После этого добавьте следующее в первую часть (ветку if ) вашего if-else   заявление:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
// Adds a second sphere to the array
spheres.append(sphere)
print(sphere.distance(to: first))
 
// If more that two are present…
if spheres.count > 2 {
     
    // Iterate through spheres array
    for sphere in spheres {
         
        // Remove all spheres
        sphere.removeFromParentNode()
    }
     
    // Remove extraneous spheres
    spheres = [spheres[2]]
}

Поскольку мы уже участвуем в мероприятии, мы знаем, что создаем другую сферу. Так что, если уже есть одна сфера, нам нужно определить расстояние и показать его пользователю. Вы можете вызвать метод distance() для сферы, потому что позже мы создадим расширение SCNNode .

Далее нам нужно знать, существует ли уже больше двух сфер. Для этого мы просто используем свойство count нашего массива spheres и оператор if . Мы перебираем все сферы в массиве и удаляем их со сцены. (Не волнуйтесь, мы вернемся к некоторым из них позже.)

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

Наконец, в предложении else мы знаем, что массив spheres пуст, поэтому нам нужно просто добавить сферу, которую мы создали во время вызова метода. В вашем предложении else добавьте следующее:

1
2
// Add the sphere
spheres.append(sphere)

Ура! Мы просто добавили сферу в наш массив spheres , и наш массив готов к следующему нажатию. Теперь мы подготовили наш массив со сферами, которые должны быть на экране, поэтому теперь давайте просто добавим их в массив.

Чтобы перебрать и добавить сферы, добавьте этот код:

1
2
3
4
5
6
// Iterate through spheres array
for sphere in spheres {
     
    // Add all spheres in the array
    self.sceneView.scene.rootNode.addChildNode(sphere)
}

Это просто цикл for , и мы добавляем сферы ( SCNNode ) как дочерний элемент корневого узла сцены. В SceneKit это предпочтительный способ добавления вещей.

Вот как должен выглядеть финальный handleTap() :

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
@objc func handleTap(sender: UITapGestureRecognizer) {
     
    let location = sender.location(in: sceneView)
    let hitTest = sceneView.hitTest(location, types: [ARHitTestResult.ResultType.featurePoint])
     
    guard let result = hitTest.last else { return }
     
    let transform = SCNMatrix4.init(result.worldTransform)
    let vector = SCNVector3Make(transform.m41, transform.m42, transform.m43)
    let sphere = newSphere(at: vector)
     
    if let first = spheres.first {
        spheres.append(sphere)
        print(sphere.distance(to: first))
         
        if spheres.count > 2 {
            for sphere in spheres {
                sphere.removeFromParentNode()
            }
             
            spheres = [spheres[2]]
        }
     
    } else {
        spheres.append(sphere)
    }
     
    for sphere in spheres {
        self.sceneView.scene.rootNode.addChildNode(sphere)
    }
}

Теперь, если вы помните, мы вызвали метод distance(to:) в нашем SCNNode , сфере, и я уверен, что Xcode кричит на вас за использование необъявленного метода. Давайте SCNNode это сейчас, создав расширение класса SCNNode .

Чтобы создать расширение, просто сделайте следующее вне вашего класса ViewController :

1
2
3
extension SCNNode {
    // Your code goes here
}

Это просто позволяет вам изменить класс (как если бы вы редактировали реальный класс). Затем мы добавим метод, который будет вычислять расстояние между двумя узлами.

Вот объявление функции, чтобы сделать это:

1
2
3
func distance(to destination: SCNNode) -> CGFloat {
    // Your code goes here
}

Если вы увидите, есть параметр, который является другим SCNNode , и в результате он возвращает CGFloat . Для фактического расчета, добавьте это к вашей функции distance() :

1
2
3
4
5
6
7
8
let dx = destination.position.x — position.x
let dy = destination.position.y — position.y
let dz = destination.position.z — position.z
 
let inches: Float = 39.3701
let meters = sqrt(dx*dx + dy*dy + dz*dz)
 
return CGFloat(meters * inches)

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

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

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

Ну вот и заверните! Вот как должен выглядеть ваш финальный проект:

Конечный результат, показывающий измерительное приложение в действии

Как видите, измерения не идеальны, но он считает, что 15-дюймовый компьютер составляет около 14,998 дюйма, так что это неплохо!

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

Кроме того, не забудьте проверить репозиторий GitHub для этого приложения. И пока вы еще здесь, ознакомьтесь с другими нашими учебными пособиями по разработке iOS здесь на Envato Tuts +!

  • iOS SDK
    Обновление вашего приложения для iOS 11
    Дорон Кац
  • iOS SDK
    Realm Мобильная база данных для iOS
    Дорон Кац
  • стриж
    Начните с обработки естественного языка в iOS 11
    Дорон Кац
  • iOS SDK
    3 ужасные ошибки разработчиков iOS