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

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

Давайте копаться…

В этом уроке мы изучим необходимые инструменты и методы AR в Swift, которые позволят нам создать приложение, которое украсит ваш пол классной напольной плиткой и деревянными текстурами. Готовое приложение будет выглядеть примерно так:

Начнем с создания приложения с одним представлением в Xcode и назовем его Home Decor.

Добавление разрешений камеры

Теперь первое, что мы сделаем, - это перейдем к файлу info.plist и включим использование камеры. Возможности камеры - это первое, что вам нужно для приложения AR. Найдите ключ описания использования камеры, как на изображении ниже, и дайте ему подходящее сообщение. Это сообщение появится при самом первом запуске приложения при запросе разрешений камеры у пользователя.

Добавление возможностей ARKit в приложение

Перейдите на Main.storyboard. Перетащите представление ARKit SceneKit View на ViewController и прикрепите ARSCNView к краям ViewController.

Создайте IBOutlet для класса ViewController и назовите его sceneView. Как только вы это сделаете, появится сообщение об ошибке undeclared ARSCNView ,, поскольку наш контроллер представления не распознает ничего типа ARSCNView. Чтобы решить эту проблему и использовать другие функции ARKit, нам необходимо импортировать ARKit в контроллер представления.

Теперь перейдите от раскадровки к файлу view controller.swift. Объявите свойство типа ARWorldTrackingConfiguration перед методом viewDidLoad () и назовите его config. И наш контроллер представления будет выглядеть так (я удалил метод didReceiveMemoryWarning):

import UIKit
import ARKit
class ViewController: UIViewController {
@IBOutlet weak var sceneView: ARSCNView!
let config = ARWorldTrackingConfiguration()
override func viewDidLoad() {
super.viewDidLoad()
}

Разрешить отладку

Эта переменная конфигурации будет определять конфигурации сеанса сцены. Мы увидим его использование позже в этом разделе. Теперь в методе viewDidLoad после super.viewDidLoad () добавьте следующее:

sceneView.debugOptions = [ARSCNDebugOptions.showFeaturePoints, ARSCNDebugOptions.showWorldOrigin]

Здесь мы включаем параметры отладки для нашего sceneView, который представляет собой не что иное, как вид камеры с возможностями инфраструктуры AR. ARSCNDebugOptions.showWorldOrigin отобразит мировое происхождение на экране. Это поможет нам найти ориентир для всех остальных позиций. ARSCNDebugOptions.showFeaturePoints отобразит все точки на экране, которые камера AR распознала в окрестностях.

Теперь, чтобы начать сеанс AR, нам нужно запустить сеанс в нашем sceneView с конфигурациями, указанными в переменной config. Чуть ниже строки sceneView.debugOptions напишите:

sceneView.session.run(config)

Теперь запустите приложение на своем устройстве (не на симуляторе, поскольку в нем нет камеры). Появится предупреждение с запросом разрешения камеры с написанным вами сообщением, и вам необходимо его разрешить. Подождите немного, пока он загрузит мировое происхождение.

Если вы здесь, значит, у вас уже запущено приложение AR. Поздравляю!

Как работают оси AR

Красная полоса или ось X используются для позиционирования объектов слева или справа от начала координат мира. Зеленая полоса или ось Y используется для позиционирования объектов вверх или вниз от начала координат мира. А синяя полоса или ось Z используются для определения того, насколько близко или далеко объект будет размещен от начала координат мира.

Положительное значение X расположит объект справа от начала координат мира, а отрицательное - слева. Положительное значение для Y поместит его сверху, а отрицательное - внизу начала координат мира. Положительное значение для Z расположит его ближе, а отрицательное - дальше от начала мира.

Добавление виртуального объекта

Давайте добавим на сцену несколько виртуальных объектов. 3D-капсула будет хорошим выбором. Объявите капсульный узел типа SCNNode и придайте ему геометрию капсулы. Дайте ему высоту 0,1 метра и радиус 0,03 метра.

let capsuleNode = SCNNode(geometry: SCNCapsule(capRadius: 0.03, height: 0.1

Теперь поместите его на 0,1 метра левее начала мира, на 0,1 метра выше начала мира и на 0,1 метра от начала координат мира:

capsuleNode.position = SCNVector3(0.1, 0.1, -0.1)

Теперь добавьте узел в сцену:

sceneView.scene.rootNode.addChildNode(capsuleNode)

SceneView содержит сцену, которая отвечает за хранение всех 3D-объектов в формате SCNNode, которые будут формировать 3D-сцену. Мы добавляем капсулу к корневому узлу сцены. Положение корневого узла точно совпадает с положением начала координат мира. Это означает, что его позиция (0,0,0).

В настоящее время наш метод viewDidLoad выглядит так:

override func viewDidLoad() {
super.viewDidLoad()
sceneView.debugOptions = [ARSCNDebugOptions.showFeaturePoints, ARSCNDebugOptions.showWorldOrigin]
sceneView.session.run(config)
let capsuleNode = SCNNode(geometry: SCNCapsule(capRadius: 0.03, height: 0.1))
capsuleNode.position = SCNVector3(0.1, 0.1, -0.1)
sceneView.scene.rootNode.addChildNode(capsuleNode)
}

Теперь запустите приложение.

Прохладный! Мы только что разместили виртуальный объект в реальном мире. Вы можете играть с разными позициями и разной геометрией, чтобы исследовать больше. Теперь давайте повернем капсулу на 90 градусов вокруг оси Z, чтобы она лежала ровно по оси X и изменила свой цвет на синий.

Углы Эйлера

Углы Эйлера отвечают за угол отображения SCNNode. Мы увидим, как использовать его для вращения капсулы.

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

В viewDidLoad добавьте следующие строки после установки положения капсулы.

capsuleNode.geometry?.firstMaterial?.diffuse.contents = UIColor.blue //1
capsuleNode.eulerAngles = SCNVector3(0,0,Double.pi/2)
//2

Здесь, в первой строке, мы устанавливаем синий цвет для самого первого материала узла, который будет распространяться по капсуле и заставит его выглядеть синим. В строке 2 мы устанавливаем угол Эйлера Z равным 90 градусам в радианах. Наконец, наше представление загружается и выглядит так:

override func viewDidLoad() {
super.viewDidLoad()
sceneView.debugOptions = [ARSCNDebugOptions.showFeaturePoints, ARSCNDebugOptions.showWorldOrigin]
sceneView.session.run(config)
let capsuleNode = SCNNode(geometry: SCNCapsule(capRadius: 0.03, height: 0.1))
capsuleNode.position = SCNVector3(0.1, 0.1, -0.1)
capsuleNode.geometry?.firstMaterial?.diffuse.contents = UIColor.blue //1
capsuleNode.eulerAngles = SCNVector3(0,0,Double.pi/2)//2
sceneView.scene.rootNode.addChildNode(capsuleNode)
}

Теперь запустите приложение.

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

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

ARSCENEView Delegates

Перед тем, как начать обнаружение пола, мы рассмотрим некоторые методы делегирования нашего sceneView, чтобы понять, какими возможностями мы обладаем для взаимодействия с текущим сеансом AR.

func renderer(SCNSceneRenderer, didAdd: SCNNode, for: ARAnchor)

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

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

func renderer(_ renderer: SCNSceneRenderer, didUpdate node: SCNNode, for anchor: ARAnchor)

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

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

func renderer(SCNSceneRenderer, didRemove: SCNNode, for: ARAnchor)

После обновления первого уникального узла размерами всех других повторяющихся узлов ARKit удаляет все повторяющиеся узлы, и метод делегата уведомляет нас. Мы будем использовать все вышеперечисленные методы делегата в нашем приложении (и их цель станет более ясной).

Обнаружение самолета

В настоящее время наша сцена пытается собрать все якоря, с которыми она сталкивается, так как это поведение по умолчанию. Но поскольку пол - это горизонтальная поверхность, нас интересуют только анкеры, расположенные на горизонтальных плоскостях. Итак, вернитесь к нашему методу viewDidLoad и напишите приведенный ниже код перед запуском сеанса (то есть перед строкой sceneView.session.run (config)).

config.planeDetection = .horizontal

В методе viewDidLoad вы можете удалить все после sceneView.session.run (config), как это было для размещения капсулы на экране, и нам это больше не нужно. Поскольку мы будем использовать все вышеупомянутые методы делегата, нам нужно сделать наш viewController делегатом sceneView. Перед закрывающей фигурной скобкой метода viewDidLoad () добавьте следующую строку.

sceneView.delegate = self

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

extension ViewController:ARSCNViewDelegate{}

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

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

Создание узлов этажа AR

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

func createFloorNode(anchor:ARPlaneAnchor) ->SCNNode{
let floorNode = SCNNode(geometry: SCNPlane(width: CGFloat(anchor.extent.x), height: CGFloat(anchor.extent.z))) //1
floorNode.position=SCNVector3(anchor.center.x,0,anchor.center.z)                                               //2
floorNode.geometry?.firstMaterial?.diffuse.contents = UIColor.blue                                             //3
floorNode.geometry?.firstMaterial?.isDoubleSided = true                                                        //4
floorNode.eulerAngles = SCNVector3(Double.pi/2,0,0)                                                    //5
return floorNode                                                                                               //6
}

Давайте рассмотрим функцию построчно и обсудим ее более подробно. Следуйте описанию каждой строки, так как это самая сложная часть.

1. Мы создаем узел с геометрией SCNPlane, которая имеет размер привязки. Экстент ARPlaneAnchor содержит информацию о местоположении. Тот факт, что extension.z использовался как height, а не extension.y, может немного сбить с толку. Если вы визуализируете, что 3D-куб помещен на пол, и вы хотите сделать его плоским вдоль 2D-поверхности, вы должны изменить y на ноль, и он станет плоским. Теперь, чтобы получить длину этой двумерной поверхности, вы бы рассмотрели z, не так ли? Наш пол плоский, поэтому нам нужен плоский узел, а не куб.

2. Устанавливаем положение узла. Так как высота нам не нужна, мы делаем y равным нулю.

3. Установите цвет пола на синий.

4. Цвет материала будет отображаться только с одной стороны, если мы специально не укажем, что он двусторонний.

5. По умолчанию плоскость будет размещена вертикально. Чтобы сделать его горизонтальным, нам нужно повернуть его на 90 градусов.

Реализация методов делегата

Теперь давайте реализуем метод делегата didAdd SCNNode.

func renderer(_ renderer: SCNSceneRenderer, didAdd node: SCNNode, for anchor: ARAnchor) {
guard let planeAnchor = anchor as? ARPlaneAnchor else {return} //1
let planeNode = createFloorNode(anchor: planeAnchor) //2
node.addChildNode(planeNode) //3
}

В строке 1 мы проверяем, является ли якорь ARPlaneAnchor, поскольку мы будем иметь дело только с этим типом привязки.

В строке 2 создается новый узел на основе привязки. В строке 3 он добавляется к узлу.

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

func renderer(_ renderer: SCNSceneRenderer, didUpdate node: SCNNode, for anchor: ARAnchor) {
guard let planeAnchor = anchor as? ARPlaneAnchor else {return}
node.enumerateChildNodes { (node, _) in
node.removeFromParentNode()
}
let planeNode = createFloorNode(anchor: planeAnchor)
node.addChildNode(planeNode)
}

В методе делегата didRemove SCNNode мы хотим очистить все наши ненужные узлы цивилизованным способом.

func renderer(_ renderer: SCNSceneRenderer, didRemove node: SCNNode, for anchor: ARAnchor) {
guard let _ = anchor as? ARPlaneAnchor else {return}
node.enumerateChildNodes { (node, _) in
node.removeFromParentNode()
}
}

Уф! Вот и все! Запустите приложение.

Добавление эффекта плитки

Чего ждать? Синий пол? Нет, мы еще не закончили. Всего лишь небольшое изменение, и у нас будет потрясающий пол!

Чтобы изменить синий пол на плитку, нам нужна текстура. Давайте поищем в Google текстуру напольной плитки. Я искал «текстура деревянного пола» и нашел несколько красивых текстурных изображений. Сохраните любой из них на своем Mac и перетащите его в Assets.xcassets.

Я назвал его WoodenFloorTile. Вы можете называть его как хотите. Снова вернемся к файлу ViewController.swift. В функции createFloorNode вместо установки UIColor.blue в качестве диффузного содержимого сделайте его UIImage с именем, которое вы дали изображению в папке ресурсов.

floorNode.geometry?.firstMaterial?.diffuse.contents = UIImage(named: "WoodenFloorTile")

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

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

Загрузите полный проект с GitHub здесь.

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