Это вторая часть нашей вводной серии по SceneKit. В этом руководстве я предполагаю, что вы знакомы с концепциями, описанными в первой части, включая настройку сцены с использованием источников света, теней, камер, узлов и материалов.
В этом уроке я собираюсь рассказать вам о некоторых более сложных — но и более полезных — функциях SceneKit, таких как анимация, взаимодействие с пользователем, системы частиц и физика. Реализуя эти функции, вы можете создавать интерактивный и динамический 3D-контент, а не статические объекты, как вы делали в предыдущем уроке.
1. Настройка сцены
Создайте новый проект Xcode на основе шаблона iOS> Приложение> Single View Application .
Назовите проект, установите « Язык» в 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
определяет, как далеко на расстоянии может видеть камера или как далеко может достигать свет от определенного источника. Создайте и запустите ваше приложение. Ваша сцена должна выглядеть примерно так:
2. Взаимодействие с пользователем
Взаимодействие с пользователем обрабатывается в 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, то есть по порядку их появления с точки зрения текущей камеры. Например, в текущей сцене, если вы нажмете одну из двух сфер или кнопку, узел, который вы нажали, сформирует первый элемент в возвращенном массиве. Однако поскольку с точки зрения камеры земля появляется непосредственно за этими объектами, наземный узел будет другим элементом в массиве результатов, вторым в данном случае. Это происходит из-за того, что касание в этом же месте ударило бы по наземному узлу, если бы там не было сфер и кнопки.
Создайте и запустите свое приложение, а также коснитесь объектов на сцене. Они должны исчезнуть при нажатии каждого из них.
Теперь, когда мы можем определить, когда узел коснулся, мы можем начать добавлять анимацию в микс.
3. Анимация
Есть два класса, которые можно использовать для анимации в 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 секунды.
Снова постройте и запустите приложение и нажмите кнопку. Он должен переместиться вниз и изменить свой цвет на белый, как показано на скриншоте ниже.
4. Физика
Настроить реалистичные симуляции физики легко с каркасом 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
}
}
}
|
Создайте и запустите приложение, нажмите кнопку и наблюдайте, как вторая сфера медленно ускоряется по направлению к первой. Обратите внимание, что может пройти несколько секунд, прежде чем вторая сфера начнет двигаться.
Однако остается сделать только одну вещь — заставить сферы взорваться при столкновении.
5. Обнаружение столкновений и системы частиц
Чтобы создать эффект взрыва, мы собираемся использовать класс SCNParticleSystem
. Система частиц может быть создана с помощью внешней 3D-программы, исходного кода или, как я собираюсь вам показать, редактора системы частиц Xcode. Создайте новый файл, нажав Ctrl + N, и выберите SceneKit Particle System из раздела iOS> Ресурс .
Установите шаблон системы частиц в Reactor . Нажмите Далее , назовите файл Explosion и сохраните его в папке вашего проекта.
В Навигаторе проектов вы увидите два новых файла: Explosion.scnp и spark.png . Изображение spark.png — это ресурс, используемый системой частиц, автоматически добавляемый в ваш проект. Если вы откроете Explosion.scnp , вы увидите его анимацию и рендеринг в реальном времени в Xcode. Редактор системы частиц является очень мощным инструментом в XCode и позволяет вам настраивать систему частиц без необходимости делать это программно.
Открыв систему частиц, перейдите к инспектору атрибутов справа и измените следующие атрибуты в разделе Emitter :
- Рождаемость до 300
- Режим направления на случайный
Измените следующие атрибуты в разделе Simulation :
- Срок службы до 3
- Коэффициент скорости до 2
И наконец, измените следующие атрибуты в разделе жизненного цикла :
- Выброс дур. до 1
- Зацикливание на пьесах
Ваша система частиц теперь должна стрелять во все стороны и выглядеть примерно так, как на следующем скриншоте:
Откройте 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. Методы, которые вы изучили в этой серии, могут быть применены к любому проекту с любым количеством анимаций, физических симуляций и т. Д.
Теперь вам должно быть удобно создавать простую сцену и добавлять к ней динамические элементы, такие как анимация и системы частиц. Концепции, которые вы изучили в этой серии, применимы к самой маленькой сцене с одним объектом вплоть до крупномасштабной игры.