Spanish (Español) translation by Javier Salesi (you can also view the original English article)
Ésta es la segunda parte de nuestra serie introductoria de SceneKit. En éste tutorial, asumo que estás familiarizado con los conceptos explicados en la primera parte, incluyendo configurar una escena con luces, sombras, cámaras, nodos y materiales.
En éste tutorial, voy a enseñarte algunas de las más complicadas-pero también más útiles-características de SceneKit, tales como, animación, interacción del usuario, sistema de partículas y físicas. Al implementar éstas características, puedes crear contenido 3D interactivo y dinámico en lugar de objetos estáticos como lo hiciste en el anterior tutorial.
1. Configurar la Escena
Crea un nuevo proyecto en Xcode basado en la plantilla iOS>Application>Single View Application.



Nombra el proyecto, establece Language en Swift, y Devices en Universal.



Abre ViewController.swift e importa el framework SceneKit.
1 |
import UIKit |
2 |
import SceneKit |
Luego, declara las siguientes propiedades en la clase ViewController
.
1 |
var sceneView: SCNView! |
2 |
var camera: SCNNode! |
3 |
var ground: SCNNode! |
4 |
var light: SCNNode! |
5 |
var button: SCNNode! |
6 |
var sphere1: SCNNode! |
7 |
var sphere2: SCNNode! |
Configuramos la escena en el método viewDidLoad
como se muestra abajo.
1 |
override func viewDidLoad() { |
2 |
super.viewDidLoad() |
3 |
|
4 |
sceneView = SCNView(frame: self.view.frame) |
5 |
sceneView.scene = SCNScene() |
6 |
self.view.addSubview(sceneView) |
7 |
|
8 |
let groundGeometry = SCNFloor() |
9 |
groundGeometry.reflectivity = 0 |
10 |
let groundMaterial = SCNMaterial() |
11 |
groundMaterial.diffuse.contents = UIColor.blueColor() |
12 |
groundGeometry.materials = [groundMaterial] |
13 |
ground = SCNNode(geometry: groundGeometry) |
14 |
|
15 |
let camera = SCNCamera() |
16 |
camera.zFar = 10000 |
17 |
self.camera = SCNNode() |
18 |
self.camera.camera = camera |
19 |
self.camera.position = SCNVector3(x: -20, y: 15, z: 20) |
20 |
let constraint = SCNLookAtConstraint(target: ground) |
21 |
constraint.gimbalLockEnabled = true |
22 |
self.camera.constraints = [constraint] |
23 |
|
24 |
let ambientLight = SCNLight() |
25 |
ambientLight.color = UIColor.darkGrayColor() |
26 |
ambientLight.type = SCNLightTypeAmbient |
27 |
self.camera.light = ambientLight |
28 |
|
29 |
let spotLight = SCNLight() |
30 |
spotLight.type = SCNLightTypeSpot |
31 |
spotLight.castsShadow = true |
32 |
spotLight.spotInnerAngle = 70.0 |
33 |
spotLight.spotOuterAngle = 90.0 |
34 |
spotLight.zFar = 500 |
35 |
light = SCNNode() |
36 |
light.light = spotLight |
37 |
light.position = SCNVector3(x: 0, y: 25, z: 25) |
38 |
light.constraints = [constraint] |
39 |
|
40 |
let sphereGeometry = SCNSphere(radius: 1.5) |
41 |
let sphereMaterial = SCNMaterial() |
42 |
sphereMaterial.diffuse.contents = UIColor.greenColor() |
43 |
sphereGeometry.materials = [sphereMaterial] |
44 |
sphere1 = SCNNode(geometry: sphereGeometry) |
45 |
sphere1.position = SCNVector3(x: -15, y: 1.5, z: 0) |
46 |
sphere2 = SCNNode(geometry: sphereGeometry) |
47 |
sphere2.position = SCNVector3(x: 15, y: 1.5, z: 0) |
48 |
|
49 |
let buttonGeometry = SCNBox(width: 4, height: 1, length: 4, chamferRadius: 0) |
50 |
let buttonMaterial = SCNMaterial() |
51 |
buttonMaterial.diffuse.contents = UIColor.redColor() |
52 |
buttonGeometry.materials = [buttonMaterial] |
53 |
button = SCNNode(geometry: buttonGeometry) |
54 |
button.position = SCNVector3(x: 0, y: 0.5, z: 15) |
55 |
|
56 |
sceneView.scene?.rootNode.addChildNode(self.camera) |
57 |
sceneView.scene?.rootNode.addChildNode(ground) |
58 |
sceneView.scene?.rootNode.addChildNode(light) |
59 |
sceneView.scene?.rootNode.addChildNode(button) |
60 |
sceneView.scene?.rootNode.addChildNode(sphere1) |
61 |
sceneView.scene?.rootNode.addChildNode(sphere2) |
62 |
}
|
La implementación de viewDidLoad
debería parecerte familiar si has leído la primera parte de ésta serie. Todo lo que hacemos es configurar la escena que usaremos en éste tutorial. Las únicas cosas nuevas que incluímos son la clase SCNFloor
y la propiedad zFar
.
Como el nombre lo implica, la clase SCNFloor
es usado para crear un piso o terreno para la escena. Ésto es mucho más fácil comparado con crear y rotar un SCNPlane
como lo hicimos en el anterior tutorial.
La propiedad zFar
determina qué tan lejos una cámara puede ver o qué tan lejos puede llegar una luz de una fuente particular. Compila y ejecuta tu aplicación. Tu escena debería verse así:
2. Interacción del Usuario.
Interacción del usuario es manejado en SceneKit por una combinación de la clase UIGestureRecognizer
y hit tests (impactos). Para detectar una pulsación por ejemplo, primero añade un UITapGestureRecongnizer
a un SCNView
, determina la posición de la pulsación en la view, y ver si está en contacto con o golpea algunos de los nodos.
Para entender mejor como funciona ésto, utilizaremos un ejemplo. Cuando un nodo es pulsado, lo removemos de la escena. Añade el siguiente fragmento de código al método viewDidLoad
de la clase ViewController
:
1 |
override func viewDidLoad() { |
2 |
super.viewDidLoad() |
3 |
|
4 |
sceneView = SCNView(frame: self.view.frame) |
5 |
sceneView.scene = SCNScene() |
6 |
self.view.addSubview(sceneView) |
7 |
|
8 |
let tapRecognizer = UITapGestureRecognizer() |
9 |
tapRecognizer.numberOfTapsRequired = 1 |
10 |
tapRecognizer.numberOfTouchesRequired = 1 |
11 |
tapRecognizer.addTarget(self, action: "sceneTapped:") |
12 |
sceneView.gestureRecognizers = [tapRecognizer] |
13 |
|
14 |
...
|
15 |
}
|
Posteriormente, agrega el siguiente método a la clase ViewController
:
1 |
func sceneTapped(recognizer: UITapGestureRecognizer) { |
2 |
let location = recognizer.locationInView(sceneView) |
3 |
|
4 |
let hitResults = sceneView.hitTest(location, options: nil) |
5 |
if hitResults?.count > 0 { |
6 |
let result = hitResults![0] as! SCNHitTestResult |
7 |
let node = result.node |
8 |
node.removeFromParentNode() |
9 |
}
|
10 |
}
|
En éste método, primero obtienes la locación de la pulsación como un CGPoint
. Luego, usa éste punto para ejecutar un impacto en el objeto sceneView
y guarda los objetos SCNHitTestResult
en un arreglo llamado hitResults
. El parámetro options
de éste método puede contener un diccionarios de claves y valores, de las que puedes leer en la documentación de Apple. Después revisamos para ver si el hit test (impacto) regresó al menos un resultado y, si así es, removemos el primer elemento en el arreglo de su nodo padre.
Si el hit test (impacto) regresó múltiples resultados, los objetos son clasificados por su posición z, que es el orden en el que aparecen desde el punto de vista de la cámara actual. Por ejemplo, en la actual escena, si pulsas ya sea en cualquiera de las dos esferas o en el botón, el nodo que pulsaste formará el primer objeto en el arreglo retornado. Debido a que el terreno aparece directamente detrás de éstos objetos desde el punto de vista de la cámara, sin embargo, el nodo del terreno será otro objeto en el arreglo de resultados, el segundo en éste caso. Ésto ocurre porque una pulsación en esa misma locación, golpearía el nodo del terreno si las esferas y el botón no estuvieran ahí.
Compila y ejecuta tu aplicación, y pulsa los objetos en la escena. Deberían desaparecer cuando pulsas cada uno.
Ahora que podemos determinar cuando un nodo el pulsado, podemos comenzar a agregar animaciones a la mezcla.
3.Animación
Hay dos clases que pueden ser usadas para ejecutar animaciones en SceneKit:
SCNAction
SCNTransaction
Objetos SCNAction
son muy útiles para animaciones simples y reutilizables, como movimiento, rotación y escala. Puedes combinar cualquier número de acciones en un objeto de acción personalizado.
La clase SCNTransaction
puede ejecutar las mismas animaciones, pero es más versatil en algunas maneras, como animar materiales. Ésto añadió versatilidad, sin embargo, viene con el costo de que animaciones SCNTransaction
sólo tienen la misma usabilidad que una función y la configuración se hace vía métodos de clase.
Para tu primera animación, voy a mostrarte código usando las clases SCNAction
y SCNTransaction
. El ejemplo moverá tu botón hacia abajo y lo pondrá blanco cuando sea pulsado. Actualiza la implementación del método sceneTapped(_:)
como se muestra abajo.
1 |
func sceneTapped(recognizer: UITapGestureRecognizer) { |
2 |
let location = recognizer.locationInView(sceneView) |
3 |
|
4 |
let hitResults = sceneView.hitTest(location, options: nil) |
5 |
if hitResults?.count > 0 { |
6 |
let result = hitResults![0] as! SCNHitTestResult |
7 |
let node = result.node |
8 |
|
9 |
if node == button { |
10 |
SCNTransaction.begin() |
11 |
SCNTransaction.setAnimationDuration(0.5) |
12 |
let materials = node.geometry?.materials as! [SCNMaterial] |
13 |
let material = materials[0] |
14 |
material.diffuse.contents = UIColor.whiteColor() |
15 |
SCNTransaction.commit() |
16 |
|
17 |
let action = SCNAction.moveByX(0, y: -0.8, z: 0, duration: 0.5) |
18 |
node.runAction(action) |
19 |
}
|
20 |
}
|
21 |
}
|
En el método sceneTapped(_:)
, obtenemos una referencia al nodo que el usuario ha pulsado y revisa si éste es el botón en la escena. Si lo es, animamos su material de rojo a blanco, usando la clase SCNTransaction
, y lo movemos en el eje y en una dirección negativa usando una instancia SCNAction
. La duración de la animación se establece en 0.5 segundos.
Compila y ejecuta tu aplicación de nuevo, y pulsa el botón. Debería moverse hacia abajo y cambiar su color a blanco como se muestra en la captura de abajo.
4. Físicas
Configurar simulaciones físicas realistas es fácil con el framework SceneKit. La funcionalidad que ofrecen las simulaciones físicas de SceneKit, es extensa, van desde velocidades básicas, aceleraciones y fuerzas, hasta campos eléctricos y gravitacionales, e incluso detección de colisión.
Lo que vas a hacer en la actual escena es, aplicar un campo gravitacional a una de las esferas para que la segunda esfera sea jalada hacia la primera esfera como resultado de la gravedad. Ésta fuerza de gravedad llegará a estar activa cuando el botón es presionado.
La configuración para ésta simulación es muy simple. Utiliza un objeto SCNPhysicsBody
para cada nodo que quieras que sea afectado por la simulación de física y un objeto SCNPhysicsField
para cada nodo que quieras que sea la fuente de un campo. Actualiza el método viewDidLoad
como se muestra debajo.
1 |
override func viewDidLoad() { |
2 |
...
|
3 |
|
4 |
buttonGeometry.materials = [buttonMaterial] |
5 |
button = SCNNode(geometry: buttonGeometry) |
6 |
button.position = SCNVector3(x: 0, y: 0.5, z: 15) |
7 |
|
8 |
// Physics
|
9 |
let groundShape = SCNPhysicsShape(geometry: groundGeometry, options: nil) |
10 |
let groundBody = SCNPhysicsBody(type: .Kinematic, shape: groundShape) |
11 |
ground.physicsBody = groundBody |
12 |
|
13 |
let gravityField = SCNPhysicsField.radialGravityField() |
14 |
gravityField.strength = 0 |
15 |
sphere1.physicsField = gravityField |
16 |
|
17 |
let shape = SCNPhysicsShape(geometry: sphereGeometry, options: nil) |
18 |
let sphere1Body = SCNPhysicsBody(type: .Kinematic, shape: shape) |
19 |
sphere1.physicsBody = sphere1Body |
20 |
let sphere2Body = SCNPhysicsBody(type: .Dynamic, shape: shape) |
21 |
sphere2.physicsBody = sphere2Body |
22 |
|
23 |
sceneView.scene?.rootNode.addChildNode(self.camera) |
24 |
sceneView.scene?.rootNode.addChildNode(ground) |
25 |
sceneView.scene?.rootNode.addChildNode(light) |
26 |
|
27 |
...
|
28 |
}
|
Comenzamos por crear una instancia SCNPhysics
que especifica la forma actual del objeto que participa en la simulación de física. Para las formas básicas que estará utilizando en ésta escena, los objetos geométricos son perfectos para utilizar. Sin embargo, para modelos 3D complicados, es mejor combinar múltiples formas primitivas para crear una forma aproximada de tu objeto para simulación de física.
De ésta forma, puedes crear una instancia SCNPhysicsBody
y agregarla al terreno de la escena. Ésto es necesario, porque cada escena de SceneKit tiene un campo de gravedad existente predeterminado que jala cada objeto hacia abajo. El tipo Kinematic
que das a éste SCNPhysicsBody
significa que el objeto participará en colisiones, pero no es afectado por fuerzas (y no se caerá por la gravedad).
Luego, creas el campo gravitacional y asignas éste al primer nodo de la esfera. Siguiendo el mismo proceso utilizado para el terreno, después creas un cuerpo de física para cada una de las dos esferas. Aunque especificas la segunda esfera como un cuerpo físico Dymamic
(dinámico), porque quieres que sea afectado y movido por el campo gravitacional que creaste.
Finalmente, necesitas establecer la fuerza de éste campo para activarlo cuando el botón sea pulsado. Agrega la siguiente línea al método sceneTapped(_:)
:
1 |
func sceneTapped(recognizer: UITapGestureRecognizer) { |
2 |
...
|
3 |
if node == button { |
4 |
...
|
5 |
sphere1.physicsField?.strength = 750 |
6 |
}
|
7 |
}
|
8 |
}
|
Compila y ejecuta tu aplicación, pulsa el botón, y observa como la segunda esfera acelera lentamente hacia la primera. Nota que puede tomar varios segundos antes de que la segunda esfera empiece a moverse.
Sólo falta una cosa por hacer, sin embargo, hacer que las esferas exploten cuando colisionen.
5. Detección de Colisión y Sistemas de Partículas
Para crear el efecto de una explosión vamos a apalancar la clase SCNParticleSystem
. Un sistema de partículas puede ser creado por un programa 3D externo, de código libre, o, como voy a mostrarte, el editor de sistema de partículas de Xcode. Crea un nuevo archivo al presionar Command+N y elige SceneKitParticle System de la sección iOS > Resource.



Establece la plantilla del sistema de partículas en Reactor. Da click en Next, nombra el archivo Explosion, y guárdalo en el directorio de tu proyecto.



En el Project Navigator, ahora verás dos nuevos archivos, Explosion.scnp y spark.png. La imagen spark.png es un recurso usado por el sistema de partículas, automáticamente agregado a tu proyecto. Si abres Explosion.scnp, lo verás siendo animado y renderizado en tiempo real en Xcode. El editor de sistema de partículas es una herramienta muy poderosa en Xcode y te permite personalizar un sistema de partículas sin tener que hacerlo programáticamente.



Con el sistema de partículas abierto, ve a Attributes Inspector a la derecha, y cambia los siguientes atributos en la sección Emitter.
- Birth rate en 300
- Direction mode en Random
Cambia los siguientes atributos en la sección Simulation:
- LIfe span en 3
- Speed factor a 2
Y finalmente, cambia los siguientes atributos en la sección Life cycle:
- Emission dur. a 1
- Looping a Plays once






Tu sistema de partículas ahora debería disparar en todas direcciones y se vería similar a la siguiente captura:



Abre ViewController.swift y haz tu clase ViewController
conforme al protocolo SNCPhysicsContactDelegate
. Adoptar éste protocolo es necesario para detectar una colisión entre dos nodos.
1 |
class ViewController: UIViewController, SCNPhysicsContactDelegate |
Después, asigna la actual instancia ViewController
como el contactDelegate
de tu objeto physicsWorld
en el método viewDidLoad
.
1 |
override func viewDidLoad() { |
2 |
super.viewDidLoad() |
3 |
|
4 |
sceneView = SCNView(frame: self.view.frame) |
5 |
sceneView.scene = SCNScene() |
6 |
sceneView.scene?.physicsWorld.contactDelegate = self |
7 |
self.view.addSubview(sceneView) |
8 |
|
9 |
...
|
10 |
}
|
Finalmente, implementa el método physicsWorld(_:didUpdateContact:)
en la clase ViewController
:
1 |
func physicsWorld(world: SCNPhysicsWorld, didUpdateContact contact: SCNPhysicsContact) { |
2 |
if (contact.nodeA == sphere1 || contact.nodeA == sphere2) && (contact.nodeB == sphere1 || contact.nodeB == sphere2) { |
3 |
let particleSystem = SCNParticleSystem(named: "Explosion", inDirectory: nil) |
4 |
let systemNode = SCNNode() |
5 |
systemNode.addParticleSystem(particleSystem) |
6 |
systemNode.position = contact.nodeA.position |
7 |
sceneView.scene?.rootNode.addChildNode(systemNode) |
8 |
|
9 |
contact.nodeA.removeFromParentNode() |
10 |
contact.nodeB.removeFromParentNode() |
11 |
}
|
12 |
}
|
Primero revisamos si los dos nodos involucrados en la colisión son las dos esferas. Si ese es el caso, entonces cargamos el sistema de partículas del archivo que creamos hace un momento y lo añadimos a un nuevo nodo. Finalmente, removemos ambas esferas involucradas en la colisión de la escena.
Compila y ejecuta tu aplicación otra vez, y pulsa el botón. Cuando las esferas hacen contacto, ambas deberían desaparecer y tu sistema de partículas debería aparecer y animarse.
Conclusión
En éste tutorial, te mostré como implementar la interacción del usuario, animación, simulación de físicas, y sistemas de partículas usando el framework SceneKit. Las técnicas que has aprendido en ésta serie pueden ser aplicadas a cualquier proyecto con cualquier número de animaciones, simulaciones físicas, etc.
Ahora deberías estar cómodo creando una escena simple y añadir elementos dinámicos a ella, tales como animación y sistemas de partículas. Los conceptos que has aprendido en ésta serie son aplicables a la escena más pequeña con un solo objeto e igualmente a un juego de gran escala.
¡Sé el primero en conocer las nuevas traducciones–sigue @tutsplus_es en Twitter!