Advertisement
  1. Code
  2. SceneKit

Una introducción a SceneKit: interacción del usuario, animaciones & físicas

Scroll to top
Read Time: 12 min

Spanish (Español) translation by Javier Salesi (you can also view the original English article)

Final product imageFinal product imageFinal product image
What You'll Be Creating

É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.

iOS App TemplateiOS App TemplateiOS App Template

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

App InformationApp InformationApp Information

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í:

Initial sceneInitial sceneInitial scene

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.

Scene with some deleted nodesScene with some deleted nodesScene with some deleted nodes

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.

Animated buttonAnimated buttonAnimated button

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.

First sphere moves towards the secondFirst sphere moves towards the secondFirst sphere moves towards the second

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.

Particle system templateParticle system templateParticle system template

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.

Particle system typeParticle system typeParticle system type

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.

Xcodes particle system editorXcodes particle system editorXcodes particle system editor

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
Particle system attributes 1Particle system attributes 1Particle system attributes 1
Particle system attributes 2Particle system attributes 2Particle system attributes 2

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

Finished particle systemFinished particle systemFinished particle system

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.

Explosion when the two spheres collideExplosion when the two spheres collideExplosion when the two spheres collide

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!

Advertisement
Did you find this post useful?
Want a weekly email summary?
Subscribe below and we’ll send you a weekly email summary of all new Code tutorials. Never miss out on learning about the next big thing.
Advertisement
Looking for something to help kick start your next project?
Envato Market has a range of items for sale to help get you started.