Статьи

Введение в SceneKit: взаимодействие с пользователем, анимация и физика

Конечный продукт
Что вы будете создавать

Это вторая часть нашей вводной серии по SceneKit. В этом руководстве я предполагаю, что вы знакомы с концепциями, описанными в первой части, включая настройку сцены с использованием источников света, теней, камер, узлов и материалов.

В этом уроке я собираюсь рассказать вам о некоторых более сложных — но и более полезных — функциях SceneKit, таких как анимация, взаимодействие с пользователем, системы частиц и физика. Реализуя эти функции, вы можете создавать интерактивный и динамический 3D-контент, а не статические объекты, как вы делали в предыдущем уроке.

Создайте новый проект Xcode на основе шаблона iOS> Приложение> Single View Application .

Шаблон приложения для iOS

Назовите проект, установите « Язык» в Swift и « Устройства» в « Универсальный» .

Информация о приложении

Откройте ViewController.swift и импортируйте инфраструктуру SceneKit.

1
2
import UIKit
import SceneKit

Затем объявите следующие свойства в классе ViewController .

1
2
3
4
5
6
7
var sceneView: SCNView!
var camera: SCNNode!
var ground: SCNNode!
var light: SCNNode!
var button: SCNNode!
var sphere1: SCNNode!
var sphere2: SCNNode!

Мы устанавливаем сцену в методе viewDidLoad как показано ниже.

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
override func viewDidLoad() {
    super.viewDidLoad()
     
    sceneView = SCNView(frame: self.view.frame)
    sceneView.scene = SCNScene()
    self.view.addSubview(sceneView)
     
    let groundGeometry = SCNFloor()
    groundGeometry.reflectivity = 0
    let groundMaterial = SCNMaterial()
    groundMaterial.diffuse.contents = UIColor.blueColor()
    groundGeometry.materials = [groundMaterial]
    ground = SCNNode(geometry: groundGeometry)
     
    let camera = SCNCamera()
    camera.zFar = 10000
    self.camera = SCNNode()
    self.camera.camera = camera
    self.camera.position = SCNVector3(x: -20, y: 15, z: 20)
    let constraint = SCNLookAtConstraint(target: ground)
    constraint.gimbalLockEnabled = true
    self.camera.constraints = [constraint]
     
    let ambientLight = SCNLight()
    ambientLight.color = UIColor.darkGrayColor()
    ambientLight.type = SCNLightTypeAmbient
    self.camera.light = ambientLight
     
    let spotLight = SCNLight()
    spotLight.type = SCNLightTypeSpot
    spotLight.castsShadow = true
    spotLight.spotInnerAngle = 70.0
    spotLight.spotOuterAngle = 90.0
    spotLight.zFar = 500
    light = SCNNode()
    light.light = spotLight
    light.position = SCNVector3(x: 0, y: 25, z: 25)
    light.constraints = [constraint]
     
    let sphereGeometry = SCNSphere(radius: 1.5)
    let sphereMaterial = SCNMaterial()
    sphereMaterial.diffuse.contents = UIColor.greenColor()
    sphereGeometry.materials = [sphereMaterial]
    sphere1 = SCNNode(geometry: sphereGeometry)
    sphere1.position = SCNVector3(x: -15, y: 1.5, z: 0)
    sphere2 = SCNNode(geometry: sphereGeometry)
    sphere2.position = SCNVector3(x: 15, y: 1.5, z: 0)
     
    let buttonGeometry = SCNBox(width: 4, height: 1, length: 4, chamferRadius: 0)
    let buttonMaterial = SCNMaterial()
    buttonMaterial.diffuse.contents = UIColor.redColor()
    buttonGeometry.materials = [buttonMaterial]
    button = SCNNode(geometry: buttonGeometry)
    button.position = SCNVector3(x: 0, y: 0.5, z: 15)
     
    sceneView.scene?.rootNode.addChildNode(self.camera)
    sceneView.scene?.rootNode.addChildNode(ground)
    sceneView.scene?.rootNode.addChildNode(light)
    sceneView.scene?.rootNode.addChildNode(button)
    sceneView.scene?.rootNode.addChildNode(sphere1)
    sceneView.scene?.rootNode.addChildNode(sphere2)
}

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

Как видно из названия, класс SCNFloor используется для создания пола или основания для сцены. Это намного проще по сравнению с созданием и вращением SCNPlane как мы делали в предыдущем уроке.

Свойство zFar определяет, как далеко на расстоянии может видеть камера или как далеко может достигать свет от определенного источника. Создайте и запустите ваше приложение. Ваша сцена должна выглядеть примерно так:

Начальная сцена

Взаимодействие с пользователем обрабатывается в SceneKit с помощью комбинации класса UIGestureRecognizer и тестов попаданий. Например, чтобы обнаружить отвод, вы сначала добавляете UITapGestureRecognizer в SCNView , определяете положение отвода в представлении и смотрите, соприкасается ли он с каким-либо из узлов или UITapGestureRecognizer с SCNView .

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
override func viewDidLoad() {
    super.viewDidLoad()
     
    sceneView = SCNView(frame: self.view.frame)
    sceneView.scene = SCNScene()
    self.view.addSubview(sceneView)
         
    let tapRecognizer = UITapGestureRecognizer()
    tapRecognizer.numberOfTapsRequired = 1
    tapRecognizer.numberOfTouchesRequired = 1
    tapRecognizer.addTarget(self, action: «sceneTapped:»)
    sceneView.gestureRecognizers = [tapRecognizer]
         
    …
}

Затем добавьте следующий метод в класс ViewController :

01
02
03
04
05
06
07
08
09
10
func sceneTapped(recognizer: UITapGestureRecognizer) {
    let location = recognizer.locationInView(sceneView)
     
    let hitResults = sceneView.hitTest(location, options: nil)
    if hitResults?.count > 0 {
        let result = hitResults![0] as!
        let node = result.node
        node.removeFromParentNode()
    }
}

В этом методе вы сначала получаете местоположение крана как CGPoint . Затем вы используете эту точку для выполнения проверки sceneView объекта sceneView и сохранения объектов SCNHitTestResult в массиве с именем hitResults . Параметр options этого метода может содержать словарь ключей и значений, о котором вы можете прочитать в документации Apple . Затем мы проверяем, вернул ли тест попадания хотя бы один результат, и, если он это сделал, мы удаляем первый элемент в массиве из его родительского узла.

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

Создайте и запустите свое приложение, а также коснитесь объектов на сцене. Они должны исчезнуть при нажатии каждого из них.

Сцена с некоторыми удаленными узлами

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

Есть два класса, которые можно использовать для анимации в SceneKit:

  • SCNAction
  • SCNTransaction

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

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

Для вашей первой анимации я собираюсь показать вам код с использованием классов SCNAction и SCNTransaction . Пример переместит вашу кнопку вниз и станет белым, когда она нажата. Обновите реализацию метода sceneTapped(_:) как показано ниже.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
func sceneTapped(recognizer: UITapGestureRecognizer) {
    let location = recognizer.locationInView(sceneView)
         
    let hitResults = sceneView.hitTest(location, options: nil)
    if hitResults?.count > 0 {
        let result = hitResults![0] as!
        let node = result.node
             
        if node == button {
            SCNTransaction.begin()
            SCNTransaction.setAnimationDuration(0.5)
            let materials = node.geometry?.materials as!
            let material = materials[0]
            material.diffuse.contents = UIColor.whiteColor()
            SCNTransaction.commit()
                 
            let action = SCNAction.moveByX(0, y: -0.8, z: 0, duration: 0.5)
            node.runAction(action)
        }
    }
}

В sceneTapped(_:) мы получаем ссылку на узел, который sceneTapped(_:) пользователь, и проверяем, является ли это кнопкой на сцене. Если это так, мы анимируем его материал с красного на белый, используя класс SCNTransaction , и перемещаем его вдоль оси y в отрицательном направлении, используя экземпляр SCNAction . Продолжительность анимации установлена ​​на 0,5 секунды.

Снова постройте и запустите приложение и нажмите кнопку. Он должен переместиться вниз и изменить свой цвет на белый, как показано на скриншоте ниже.

Анимированная кнопка

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

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

Настройка для этого моделирования очень проста. Используйте объект SCNPhysicsBody для каждого узла, на который вы хотите воздействовать физическим моделированием, и объект SCNPhysicsField для каждого узла, который вы хотите использовать в качестве источника поля. Обновите метод viewDidLoad как показано ниже.

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
override func viewDidLoad() {
    …
 
    buttonGeometry.materials = [buttonMaterial]
    button = SCNNode(geometry: buttonGeometry)
    button.position = SCNVector3(x: 0, y: 0.5, z: 15)
     
    // Physics
    let groundShape = SCNPhysicsShape(geometry: groundGeometry, options: nil)
    let groundBody = SCNPhysicsBody(type: .Kinematic, shape: groundShape)
    ground.physicsBody = groundBody
     
    let gravityField = SCNPhysicsField.radialGravityField()
    gravityField.strength = 0
    sphere1.physicsField = gravityField
     
    let shape = SCNPhysicsShape(geometry: sphereGeometry, options: nil)
    let sphere1Body = SCNPhysicsBody(type: .Kinematic, shape: shape)
    sphere1.physicsBody = sphere1Body
    let sphere2Body = SCNPhysicsBody(type: .Dynamic, shape: shape)
    sphere2.physicsBody = sphere2Body
     
    sceneView.scene?.rootNode.addChildNode(self.camera)
    sceneView.scene?.rootNode.addChildNode(ground)
    sceneView.scene?.rootNode.addChildNode(light)
     
    …
}

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

Из этой формы вы затем создаете экземпляр SCNPhysicsBody и добавляете его в основу сцены. Это необходимо, потому что каждая сцена SceneKit по умолчанию имеет существующее гравитационное поле, которое тянет каждый объект вниз. Kinematic тип, который вы даете этому SCNPhysicsBody означает, что объект будет участвовать в столкновениях, но не подвержен воздействию сил (и не будет падать из-за гравитации).

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

Наконец, вам нужно установить силу этого поля, чтобы активировать его при нажатии кнопки. Добавьте следующую строку в метод sceneTapped(_:) :

1
2
3
4
5
6
7
8
func sceneTapped(recognizer: UITapGestureRecognizer) {
    …
        if node == button {
            …
            sphere1.physicsField?.strength = 750
        }
    }
}

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

Первая сфера движется ко второй

Однако остается сделать только одну вещь — заставить сферы взорваться при столкновении.

Чтобы создать эффект взрыва, мы собираемся использовать класс SCNParticleSystem . Система частиц может быть создана с помощью внешней 3D-программы, исходного кода или, как я собираюсь вам показать, редактора системы частиц Xcode. Создайте новый файл, нажав Ctrl + N, и выберите SceneKit Particle System из раздела iOS> Ресурс .

Шаблон системы частиц

Установите шаблон системы частиц в Reactor .   Нажмите Далее , назовите файл Explosion и сохраните его в папке вашего проекта.

Тип системы частиц

В Навигаторе проектов вы увидите два новых файла: Explosion.scnp и spark.png . Изображение spark.png — это ресурс, используемый системой частиц, автоматически добавляемый в ваш проект. Если вы откроете Explosion.scnp , вы увидите его анимацию и рендеринг в реальном времени в Xcode. Редактор системы частиц является очень мощным инструментом в XCode и позволяет вам настраивать систему частиц без необходимости делать это программно.

Редактор системы частиц Xcodes

Открыв систему частиц, перейдите к инспектору атрибутов справа и измените следующие атрибуты в разделе Emitter :

  • Рождаемость до 300
  • Режим направления на случайный

Измените следующие атрибуты в разделе Simulation :

  • Срок службы до 3
  • Коэффициент скорости до 2

И наконец, измените следующие атрибуты в разделе жизненного цикла :

  • Выброс дур. до 1
  • Зацикливание на пьесах
Атрибуты системы частиц 1
Атрибуты системы частиц 2

Ваша система частиц теперь должна стрелять во все стороны и выглядеть примерно так, как на следующем скриншоте:

Система готовых частиц

Откройте ViewController.swift и приведите ваш класс ViewController соответствие с протоколом SCNPhysicsContactDelegate . Принятие этого протокола необходимо для обнаружения коллизии между двумя узлами.

1
class ViewController: UIViewController, SCNPhysicsContactDelegate

Затем назначьте текущий экземпляр ViewController как contactDelegate вашего объекта viewDidLoad методе viewDidLoad .

01
02
03
04
05
06
07
08
09
10
override func viewDidLoad() {
    super.viewDidLoad()
     
    sceneView = SCNView(frame: self.view.frame)
    sceneView.scene = SCNScene()
    sceneView.scene?.physicsWorld.contactDelegate = self
    self.view.addSubview(sceneView)
     
    …
}

Наконец, реализуйте метод physicsWorld(_:didUpdateContact:) в классе ViewController :

01
02
03
04
05
06
07
08
09
10
11
12
func physicsWorld(world: SCNPhysicsWorld, didUpdateContact contact: SCNPhysicsContact) {
    if (contact.nodeA == sphere1 || contact.nodeA == sphere2) && (contact.nodeB == sphere1 || contact.nodeB == sphere2) {
        let particleSystem = SCNParticleSystem(named: «Explosion», inDirectory: nil)
        let systemNode = SCNNode()
        systemNode.addParticleSystem(particleSystem)
        systemNode.position = contact.nodeA.position
        sceneView.scene?.rootNode.addChildNode(systemNode)
         
        contact.nodeA.removeFromParentNode()
        contact.nodeB.removeFromParentNode()
    }
}

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

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

Взрыв, когда две сферы сталкиваются

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

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