Scene Kit Tutorial: Getting Started

Learn how to easily create 3D scenes in your iOS apps or games in this Scene Kit tutorial! By Ricardo Rendon Cepeda.

Leave a rating/review
Save for later
Share
You are currently viewing page 2 of 3 of this article. Click here to view the first page.

Lights, Camera, Action!

The built-in SCNView features are very useful, but also rather limited. In order to fully customize and control your scene, you must learn how to create and add multiple nodes to your scene graph, of various different types.

As film directors would say, “Lights, camera, action!” :]

Add the following lines to sceneSetup, just below let scene = SCNScene():

let ambientLightNode = SCNNode()
ambientLightNode.light = SCNLight()
ambientLightNode.light!.type = SCNLightTypeAmbient
ambientLightNode.light!.color = UIColor(white: 0.67, alpha: 1.0)
scene.rootNode.addChildNode(ambientLightNode)

Here, you’re creating a new ambient light node with a 67% white color, then adding it to your scene.

Ambient light is a basic form of lighting that illuminates all objects in a scene evenly, with a constant intensity from all directions, much like cloud-filtered sunlight. It’s a great addition to most scenes when you want a more natural look. Otherwise, objects in shadow would be pure black and that’s not the desired effect in most cases – especially when you build a model-viewer type of app, like the one in this tutorial.

Although ambient light is part of the lighting equation, it’s not very useful on its own because it does little to illuminate surface details. So, add one more light to your scene with the following lines, just after your previous addition:

let omniLightNode = SCNNode()
omniLightNode.light = SCNLight()
omniLightNode.light!.type = SCNLightTypeOmni
omniLightNode.light!.color = UIColor(white: 0.75, alpha: 1.0)
omniLightNode.position = SCNVector3Make(0, 50, 50)
scene.rootNode.addChildNode(omniLightNode)

This new type of light is called an omnidirectional light, or point light. It’s similar to ambient light in that it has constant intensity, but differs because it has a direction. The light’s position relative to other objects in your scene determines its direction. Your box node’s default position is (0, 0, 0), so positioning your new light node at (0, 50, 50) means that it’s above and in front of your box.

Note: Don’t worry too much about units at this point. All units are relative to the scene and defined by your own set of rules. A position of (0, 50, 50) doesn’t mean 50 meters, miles, or light years; it’s simply 50 units. You’ll define a proper unit of measurement shortly.

Now that you’ve set up some lights, delete the following line from sceneSetup:

sceneView.autoenablesDefaultLighting = true

Build and run! Pinch to zoom out, and you’ll find your box properly illuminated:

BuildRun04

Behold your amazing and talented box! For this tutorial, these two sources will do just fine, but be sure to experiment with directional (SCNLightTypeDirectional) and spot (SCNLightTypeSpot) lights in future projects.

Next, set up a camera by adding the following lines just below where you setup the lights in sceneSetup:

let cameraNode = SCNNode()
cameraNode.camera = SCNCamera()
cameraNode.position = SCNVector3Make(0, 0, 25)
scene.rootNode.addChildNode(cameraNode)

Just like the light nodes, a camera node is just as easy – if not easier – to set up. The default projection settings are spot-on for a basic scene, so you don’t have to modify the camera’s field of view, focal range or other properties. The only thing you need to define is its position, conveniently set right in front of your box at (0, 0, 25).

Build and run to “get behind the lens.”

BuildRun05

Note: The default projection type of a SCNCamera is perspective, but you can easily change this to orthographic projection by enabling usesOrthographicProjection and setting an appropriate orthographicScale value.

Now, if you navigate your scene with the default camera controls (rotation, specifically), you’ll notice an odd effect: you might expect the box to rotate and the lighting to stay in place but, in fact, it’s actually the camera rotating around the scene. That’s why you see the box from different points of view.

The default camera controls are very powerful, but not customizable. In order to gain total control, you’ll create your own. First, add the following variables to ViewController.swift, just above viewDidLoad:

// Geometry
var geometryNode: SCNNode = SCNNode()
  
// Gestures
var currentAngle: Float = 0.0

Dealing with a single box is simple enough, but once you have more complex models with multiple geometry sources, it’ll be much easier to manage them as a single node – hence the addition of geometryNode. Furthermore, currentAngle will help modify the y-axis rotation of geometryNode exclusively, leaving the rest of your scene nodes untouched.

Next up, add the following function below sceneSetup:

func panGesture(sender: UIPanGestureRecognizer) {
  let translation = sender.translationInView(sender.view!)
  var newAngle = (Float)(translation.x)*(Float)(M_PI)/180.0
  newAngle += currentAngle
      
  geometryNode.transform = SCNMatrix4MakeRotation(newAngle, 0, 1, 0)
    
  if(sender.state == UIGestureRecognizerState.Ended) {
    currentAngle = newAngle
  }
}

Whenever sceneView detects a pan gesture, this function will be called, and it transforms the gesture’s x-axis translation to a y-axis rotation on the geometry node (1 pixel = 1 degree).

In this implementation, you modify the transform property of geometryNode by creating a new rotation matrix, but you could also modify its rotation property with a rotation vector. A transformation matrix is better because you can easily expand it to include translation and scale.

Next, add the following lines to sceneSetup, just above sceneView.scene = scene:

geometryNode = boxNode
    
let panRecognizer = UIPanGestureRecognizer(target: self, action: "panGesture:")
sceneView.addGestureRecognizer(panRecognizer)

These lines finalize the setup by connecting your new functions and variables. Finally, remove the default camera controls by deleting the following line:

sceneView.allowsCameraControl = true

Build, run, action! Now try rotating your cube with a one-finger pan.

BuildRun06

Much better :]

Note: A full 3D transformation engine with gestures is far too complex for this tutorial, but definitely worth a look. Luckily, for you, it’s something we’ve covered on our site before! Check out OpenGL ES Transformations with Gestures for the implementation details.

Atoms

Your box has been a fine star for the scene so far, and did its job in helping you create a pilot app, but now it’s time to kick it to the curb. Since you’re going to build an app with a purpose, you’re going to recast your actors and develop a 3D visualizer starring the common carbon compounds: natural gas, alcohol and Teflon.

These compounds will display as ball-and-stick models (molecules) made of bonded atoms. Atoms are quite complex, but for this tutorial a single sphere will represent each one as it’s an easy way to depict a basic unit of matter. This model is a simple, but useful way to represent molecular structures in chemistry, and you can learn more about it here.

The first atom you’ll create is, of course, carbon. Open up Atoms.swift and add the following class function inside the Atoms class:

class func carbonAtom() -> SCNGeometry {
  // 1
  let carbonAtom = SCNSphere(radius: 1.70)
    
  // 2
  carbonAtom.firstMaterial!.diffuse.contents = UIColor.darkGrayColor()
    
  // 3
  carbonAtom.firstMaterial!.specular.contents = UIColor.whiteColor()
    
  // 4
  return carbonAtom
}

Note: If you’re just getting to know Swift, a class function (otherwise, known as type function), is a function that is called directly on a class, rather than on an instance of that class.

In Objective-C, these methods are indicated by the use of plus sign before for the actual method name. For example: + (SCNGeometry *)carbonAtom;

This is a very compact function, but each line is worth understanding completely:

  1. As previously mentioned, atoms will be modeled as spheres. 1.70 is the van der Waals radius of carbon, which corresponds to the radius of an atom, in angstroms (10-10m), if it were a perfect sphere. Now that you’ve assigned a unit of measurement to your atom, the rest of your scene will be relative to this unit.
  2. firstMaterial is a property of type SCNMaterial, which defines your atom’s material. Think of diffused shading as the intrinsic/base color of a surface. In Scene Kit, you can define this by a color, texture, or other source. For additional information, please refer to How to Export Blender Models to OpenGL ES: Part 2/3
  3. Specular shading is another material property, which is the reflective color of a surface. For most materials, this is usually pure white.
  4. All atom models in this tutorial will be declared as type methods, following a factory method pattern. This will be especially useful when creating molecules with many different atoms of the same type, with these methods returning a fully modeled geometry object.

Now that you understand how to create a type method for a carbon atom, the rest of the atoms in this tutorial will be just as easy to model. Add the following code to the Atoms class:

class func hydrogenAtom() -> SCNGeometry {
  let hydrogenAtom = SCNSphere(radius: 1.20)
  hydrogenAtom.firstMaterial!.diffuse.contents = UIColor.lightGrayColor()
  hydrogenAtom.firstMaterial!.specular.contents = UIColor.whiteColor()
  return hydrogenAtom
}
  
class func oxygenAtom() -> SCNGeometry {
  let oxygenAtom = SCNSphere(radius: 1.52)
  oxygenAtom.firstMaterial!.diffuse.contents = UIColor.redColor()
  oxygenAtom.firstMaterial!.specular.contents = UIColor.whiteColor()
  return oxygenAtom
}
  
class func fluorineAtom() -> SCNGeometry {
  let fluorineAtom = SCNSphere(radius: 1.47)
  fluorineAtom.firstMaterial!.diffuse.contents = UIColor.yellowColor()
  fluorineAtom.firstMaterial!.specular.contents = UIColor.whiteColor()
  return fluorineAtom
}

There you have it: hydrogen, oxygen, and fluorine.

All atoms have the same method template and simply differ in sphere radius and diffuse color. In order to display all these atoms together, you’ll add them to a single node. To do so, add the following type method to the Atoms class, at the bottom of the file (just before the closing brace):

class func allAtoms() -> SCNNode {
  let atomsNode = SCNNode()
    
  let carbonNode = SCNNode(geometry: carbonAtom())
  carbonNode.position = SCNVector3Make(-6, 0, 0)
  atomsNode.addChildNode(carbonNode)
    
  let hydrogenNode = SCNNode(geometry: hydrogenAtom())
  hydrogenNode.position = SCNVector3Make(-2, 0, 0)
  atomsNode.addChildNode(hydrogenNode)
    
  let oxygenNode = SCNNode(geometry: oxygenAtom())
  oxygenNode.position = SCNVector3Make(+2, 0, 0)
  atomsNode.addChildNode(oxygenNode)
    
  let fluorineNode = SCNNode(geometry: fluorineAtom())
  fluorineNode.position = SCNVector3Make(+6, 0, 0)
  atomsNode.addChildNode(fluorineNode)
    
  return atomsNode
}

This is a rather lengthy function, but it’s also easy to digest. It simply creates and returns a new node that contains four children, one for each atom, nicely positioned along the x-axis. To see them in your scene, open up ViewController.swift and add the following lines to viewDidAppear(animated:), just below the call to sceneSetup:

geometryLabel.text = "Atoms\n"
geometryNode = Atoms.allAtoms()
sceneView.scene!.rootNode.addChildNode(geometryNode)

Finally, remove your previous box object by deleting these lines from inside sceneSetup:

let boxGeometry = SCNBox(width: 10.0, height: 10.0, length: 10.0, chamferRadius: 1.0)
let boxNode = SCNNode(geometry: boxGeometry)
scene.rootNode.addChildNode(boxNode)
geometryNode = boxNode

Build and run (or, as Radioactive Man would say: “Up and atom!”). You should see four nicely rendered atoms:

BuildRun07

Awesome job so far! The scene is coming together, and now is a good time to take a quick break if you need to.

Ricardo Rendon Cepeda

Contributors

Ricardo Rendon Cepeda

Author

Over 300 content creators. Join our team.