Introduction To SceneKit – Part 2

In this tutorial we’ll be looking at the various types of primitives you can use with SceneKit. We’ll look at how we can position objects in 3D space and ways of animating the positions of objects via actions.

Here’s a SceneKit starter project with an empty scene to help you follow along.

Geometries in SceneKit are instances of the SCNGeometry class. Geometries provide the data necessary to draw the 3D objects they represent. Among other things this data is made up of verticesnormalstexture coordinates and indices. We also plan on releasing a series of tutorials on custom geometries which will explain all these concepts in detail.
If you want more information on geometry data in SceneKit have a look at this great tutorial this great tutorial which explains how to create custom geometries.

SceneKit provides the following basic primitives out of the box:

There’s also a set of more advanced and specialized objects which we’ll discuss in later tutorials:

Note that all of these classes subclass SCNGeometry.

Primitives are useful among other things for creating some basic shapes in your apps without importing 3D models designed in external programs. For example cylinder can represent barrels, boxes can represent crates, spheres can represent balls, etc.

Most primitives are positioned relative to their center unless noted otherwise. Let’s look at each primitive in part.

Sphere

SCNSphere objects let you create 3D spheres. Spheres are defined using a radius, the distance from the center of the sphere to any point on the sphere.

Example:

let sphere = SCNSphere(radius: 1.0)

Sphere

Plane

SCNPlane objects let you create 2D planes by specifying the plane’s width and height. Planes result in a 2D geometry that is only visible from one side by default. To make planes visible from both sides set the doubleSidedproperty

Example:

let plane = SCNPlane(width: 1.0, height: 1.5)

// Make the plane visible from both sides
plane.firstMaterial?.doubleSided = true

Plane

You can create a plane with rounded corners by setting the plane’s cornerRadius property. Example:

plane.cornerRadius = 0.1

Plane Rounded Corners

Box

SCNBox objects let you create cubes by specifying their widthheight and chamfer radius

Boxes are defined width, height and length and chamfer radius:
Width is the length of the box along the x-axis.
Height is the length of the box along the y-axis.
Length is the length of the box along the z-axis.

Example:

let box = SCNBox(width: 1.0, height: 1.5, length: 2.0, chamferRadius: 0.0)

Box

The chamferRadius determines the amount by which the boxe’s corners should be rounded.

Example:

box.chamferRadius = 0.05

Box Rounded Corners

Pyramid

SCNPyramid objects let you create pyramids by specifying the width and length of the base and the height of the pyramid.

Note that the pyramid is positioned relative to the center of it’s base instead of the center of the geometries like the other primitives.

Example:

let pyramid = SCNPyramid(width: 2.0, height: 1.5, length: 1.0)

Pyramid

Cylinder

SCNCylinder objects let you create cylinders by specifying the cylinder’s height and the radius of it’s base. You can use cylinders to draw lines by specifying a small radius.

Example:

let cylinder = SCNCylinder(radius: 1.0, height: 1.5)

Cylinder

Cone

SCNCone objects let you create cones and conical frustums(cones with their top cut off). You create cones by specifying a bottom radiustop radius and height.

Example:

let cone = SCNCone(topRadius: 0.5, bottomRadius: 1.0, height: 1.5)

Cone

Example:

let cone = SCNCone(topRadius: 0.0, bottomRadius: 1.0, height: 1.5)

Notice that setting the topRadius property to 0.0 results in a cone. If topRadius is equal to bottomRadius the resulting geometry is a cylinder.

Cone No Top

Torus

Torus objects let you create donut like shapes by specifying a ring radius and a pipe radius (the actual radius of the donut). Have a look at the image below to see how these work.

Example:

let torus = SCNTorus(ringRadius: 1.0, pipeRadius: 0.2)

Torus Explanation Torus

Tube

Tube objects let you create cylinders with a hole going through their middle. You create cones by specifying the outerRadius (radius of the cylinder), innerRadius(radius of the hole going through it’s middle) and a height.

Example:

let tube = SCNTube(innerRadius: 0.5, outerRadius: 1.0, height: 1.5)

Tube

Capsule

Capsule objects let you create cylinders that are capped with hemispheres at both ends. You create capsules by specifying their height and the radius of the hemispheres at the ends.

Example:

let capsule = SCNCapsule(capRadius: 0.5, height: 2.0)

Capsule

All primitives in a line

Let’s draw all the primitives mentioned above in a straight line along the x axis. First of all we’ll create an array to hold our geometries.

var geometries = [SCNSphere(radius: 1.0),
                  SCNPlane(width: 1.0, height: 1.5),
                  SCNBox(width: 1.0, height: 1.5, length: 2.0, chamferRadius: 0.0),
                  SCNPyramid(width: 2.0, height: 1.5, length: 1.0),
                  SCNCylinder(radius: 1.0, height: 1.5),
                  SCNCone(topRadius: 0.5, bottomRadius: 1.0, height: 1.5),
                  SCNTorus(ringRadius: 1.0, pipeRadius: 0.2),
                  SCNTube(innerRadius: 0.5, outerRadius: 1.0, height: 1.5),
                  SCNCapsule(capRadius: 0.5, height: 2.0)]

We’ll then iterate through our array of geometries with an index. Create a new node with each geometry and add it to our root node in our scene.
We’ll keep track of the x position of our geometries incrementing it by 2.5 at each iteration of the loop.
We can get a cool coloring by creating colors with hue, saturation, brightness and varying the hue based on the index.

var x:Float = 0.0
for index in 0..<geometries.count {

  let hue:CGFloat = CGFloat(index) / CGFloat(geometries.count)
  let color = UIColor(hue: hue, saturation: 1.0, brightness: 1.0, alpha: 1.0)

  let geometry = geometries[index]
  geometry.firstMaterial?.diffuse.contents = color

  let node = SCNNode(geometry: geometry)
  node.position = SCNVector3(x: x, y: 0.0, z: 0.0)

  self.rootNode.addChildNode(node)

  x += 2.5
}

Primitives Line

Hue Saturation Brightness

The UIColor initWithHue:saturation:brightness:alpha: constructor creates a new color instance by specifying it’s hue, saturation, brightness and alpha values. All these values have to be floating point numbers between 0 and 1.
To get a feeling for how these numbers affect the resulting color open up a color picker and play with the sliders.

Color Picker

The above dark orange color can be created in code via:

UIColor(hue: 25.0 / 359.0, saturation: 0.8, brightness: 0.7, alpha: 1.0)

All primitives in a circle

Let’s say we wanted to position our primitives in a circle. The gist of doing this is working in polar coordinates. Polar coordinates are the natural choice when dealing with coordinates that are along a circle and can be naturally expressed via a distance and an angle.

A short explanation of polar coordinates

Note: if you’re familiar with polar coordinates feel free to skip this section

Polar coordinates are used to represent 2D points via a radius and an angle.
The radius represents the distance from (0,0) to the point.
The angle represents the angle that the line connecting the point to (0,0) makes with the horizontal axis.

In the below image we have a point in polar coordinates with radius of 4 and an angle of 60°. Polar Coordinates

We can convert a point in polar coordinates to cartesian(x,y) coordinates by noticing that we can draw right triangle with vertices (0,0), our point and the projection of our point along the x-axis. (triangle OBC in the above figure). We know that the hypotenuse of the triangle is equal to the radius and that one of its angles is equal to our polar angle.
Using some basic trigonometry we can deduce the x and y coordinates by knowing the points radius and angle in polar coordinates.
By definition sin(angle) = Oposite / Hypotenusecos(angle) = Adjacent / Hypotenuse
In our case:
cos(angle) = x / radius
sin(angle) = y / radius

It follows that:
x = radius * cos(angle)
y = radius * sin(angle)

Note that when using the sin and cos functions in swift we have to specify the angle in radians. If you’re not familliar with radians have a look at this great explanation.

Applying polar coordinates

We’ll be using polar coordinates to arrange our primitives in the XZ plane.
A radius of 4 is great for nicely arranging the primitives.
We’ll be starting the angle off at 0 and incrementing it by 2π / geometries.count so that the primitives are positioned uniform along the circle. We have to convert from polar coordinates to x,z coordinates when actually setting the position of the primitive.

var angle:Float = 0.0
let radius:Float = 4.0
let angleIncrement:Float = Float(M_PI) * 2.0 / Float(geometries.count)

for index in 0..<geometries.count {

    let hue:CGFloat = CGFloat(index) / CGFloat(geometries.count)
    let color = UIColor(hue: hue, saturation: 1.0, brightness: 1.0, alpha: 1.0)

    let geometry = geometries[index]
    geometry.firstMaterial?.diffuse.contents = color

    let node = SCNNode(geometry: geometry)

    let x = radius * cos(angle)
    let z = radius * sin(angle)

    node.position = SCNVector3(x: x, y: 0, z: z)

    self.rootNode.addChildNode(node)

    angle += angleIncrement
}

Primitives Circle

Animations

Let’s give our scene some life by animating the movement of our objects.

SceneKit provides a really convenient way of creating animations via the SCNAction class. It’s similar to the way animations in Cocos2D and SpriteKit work. You create an action and then you can run it on a node.

To animate the position of a node we have to create a new SCNAction via the class constructor moveByX(_:y:z:duration:) or moveTo(_:duration:).

moveByX(_:y:z:duration:) moves a node by an x,y,z offset
moveTo(_:duration:) takes a SCNVector which represents the position where the node will end up at the end of the animation.
Both methods also take the duration of the animation as a parameter.

Example:

let sphere = SCNSphere(radius: 1.0)
sphere.firstMaterial?.diffuse.contents = UIColor.redColor()
let sphereNode = SCNNode(geometry: sphere)

self.rootNode.addChildNode(sphereNode)

let moveUp = SCNAction.moveByX(0.0, y: 1.0, z: 0.0, duration: 1.0)
sphereNode.runAction(moveUp)

Sphere Move Up

Actions can be composed by creating a sequence. For example if we wanted to move the sphere up and then down again we would create two animations and composite them into a sequence animation then run the sequence animation on the sphere node.
Sequence animations are created via the constructor sequence(actions) were actions is an array of actions that will be played in a sequence.

Example:

let moveUp = SCNAction.moveByX(0.0, y: 1.0, z: 0.0, duration: 1.0)
let moveDown = SCNAction.moveByX(0.0, y: -1.0, z: 0.0, duration: 1.0)
let sequence = SCNAction.sequence([moveUp,moveDown])
sphereNode.runAction(sequence)

Sphere Up Down

Actions can be repeated by creating a new action via one of the constructors:
repeatAction(_:count:) – Repeats the action count times
repeatActionForever(_:) – Repeats the action forever

Example:

let moveUp = SCNAction.moveByX(0.0, y: 1.0, z: 0.0, duration: 1.0)
let moveDown = SCNAction.moveByX(0.0, y: -1.0, z: 0.0, duration: 1.0)
let sequence = SCNAction.sequence([moveUp,moveDown])
let repeatedSequence = SCNAction.repeatActionForever(sequence)
sphereNode.runAction(repeatedSequence)

Let’s also have a look at how we can animate our circle of primitives. We can create a SCNAction instance and run it inside the loop where we create our node.

  let sign:CGFloat = index % 2 == 0 ? 1.0 : -1.0
  let move1 = SCNAction.moveByX(0.0, y: sign * CGFloat(1.0), z: 0.0, duration: 1.0)
  let move2 = SCNAction.moveByX(0.0, y: sign * CGFloat(-1.0), z: 0.0, duration: 1.0)
  let sequence = SCNAction.sequence([move1,move2])
  let repeatedSequence = SCNAction.repeatActionForever(sequence)

The animation we’re creating here is similar to the one above. Only we alternate the movement between up/down and down/up based on the index.

Cricle of primitives

Check out the documentation for a complete list of actions available in SceneKit.

Next time we’ll look at at making a 3D simulation of the Towers of Hanoi problem.

Challenges

1) Add a new node to the scene with a geometry of type SCNFloor, position it below the primitives. The floor is an infinite plane that reflects the geometries above it. Try tweaking the intensity of the reflection via the floor’s reflectivity property.

Solution

let floor = SCNFloor()
let floorNode = SCNNode(geometry: floor)
floorNode.position.y = -2.5
self.rootNode.addChildNode(floorNode)

[collapse]

Floor

2) Position the primitives in a spiral that goes twice around the origin

Hint1

Increase the y coordinate at each iteration

[collapse]
Hint2

You have to make the angle increment 2 times bigger to go twice around the circle

[collapse]
Solution

var angle:Float = 0.0
let radius:Float = 2.0
let angleIncrement:Float = Float(M_PI) * 4.0 / Float(geometries.count)

var y:Float = 0.0

for index in 0..<geometries.count {

  let hue:CGFloat = CGFloat(index) / CGFloat(geometries.count)
  let color = UIColor(hue: hue, saturation: 1.0, brightness: 1.0, alpha: 1.0)

  let geometry = geometries[index]
  geometry.firstMaterial?.diffuse.contents = color

  let node = SCNNode(geometry: geometry)

  let x = radius * cos(angle)
  let z = radius * sin(angle)

  node.position = SCNVector3(x: x, y: y, z: z)

  self.rootNode.addChildNode(node)

  angle += angleIncrement
  y += 2.0
}

[collapse]

Primitives Spiral

3) Draw a flight of stairs 20 stairs.

Solution

let numberOfStairs = 20
let stairWidth:CGFloat = 1.0
let stairHeight:CGFloat = 0.2
let stairLength:CGFloat = 0.5

var z:Float = 0.0
var y:Float = 0.0

for index in 0..<numberOfStairs {

  let hue:CGFloat = CGFloat(index) / CGFloat(numberOfStairs)

  let stairNode = SCNNode(geometry: SCNBox(width: stairWidth, height: stairHeight, length: stairLength, chamferRadius: 0.0))

  if (index % 3 == 0) {
      stairNode.geometry?.firstMaterial?.diffuse.contents = UIColor.redColor()
  } else if (index % 3 == 1){
      stairNode.geometry?.firstMaterial?.diffuse.contents = UIColor.orangeColor()
  } else {
      stairNode.geometry?.firstMaterial?.diffuse.contents = UIColor.purpleColor()
  }

  stairNode.position = SCNVector3(x: 0.0, y: y, z: z)

  y += Float(stairHeight)
  z += Float(stairLength)

  self.rootNode.addChildNode(stairNode)
}

[collapse]

Flight of stairs

4) Draw a fir tree by stacking N cones on top of each other. Make the base a cylinder. Make the topmost cone have a topRadius of 0.

Solution

let baseHeight:CGFloat = 0.8
let treeBase = SCNNode(geometry: SCNCylinder(radius: 0.2, height: baseHeight))

treeBase.geometry?.firstMaterial?.diffuse.contents = UIColor.brownColor()

let numberOfLevels = 4

var y:Float = Float(baseHeight / 2.0)
var bottomRadius:CGFloat = 0.8
var topRadius:CGFloat = 0.5
var leaveHeight:CGFloat = 0.4
let lastLevelHeight:CGFloat = 0.6

let scale:CGFloat = 0.8
for i in 0..<numberOfLevels {

  if (i == numberOfLevels - 1) {

      topRadius = 0.0
      y += Float((lastLevelHeight - leaveHeight) / 2.0)
      leaveHeight = lastLevelHeight

  }

  let leavesNode = SCNNode(geometry: SCNCone(topRadius: topRadius, bottomRadius: bottomRadius, height: leaveHeight))

  leavesNode.position.y = y
  y += Float(leaveHeight)

  leavesNode.geometry?.firstMaterial?.diffuse.contents = UIColor.greenColor()

  treeBase.addChildNode(leavesNode)

  bottomRadius *= scale
  topRadius *= scale

}

[collapse]

Fir Tree

5) Draw a toy by stacking N tori on top of each other with a cone in the middle Note: Tori are transparent for reference.

Solution

let numberOfTori = 6
var cylinderRadius:CGFloat = 0.5
var pipeRadius:CGFloat = 0.3

var cylinderHeight:CGFloat = 2.5

let cylinder = SCNCone(topRadius: 0.15, bottomRadius: cylinderRadius, height: cylinderHeight)

let cylinderNode = SCNNode(geometry: cylinder)

cylinderNode.position.y += Float(cylinderHeight) / 2.0 - Float(pipeRadius)

var y:Float = 0.0
for index in 0..<numberOfTori {
  let torus = SCNTorus(ringRadius: cylinderRadius + pipeRadius, pipeRadius: pipeRadius)

  let hue:CGFloat = CGFloat(index) / CGFloat(numberOfTori)
  let color = UIColor(hue: hue, saturation: 1.0, brightness: 1.0, alpha: 1.0)

  torus.firstMaterial?.diffuse.contents = color
  torus.firstMaterial?.transparency = 0.8

  let torusNode = SCNNode(geometry: torus)

  torusNode.position = SCNVector3(x: 0.0, y: y, z: 0.0)

  self.rootNode.addChildNode(torusNode)

  y += Float(pipeRadius)

  println(cylinderRadius)

  cylinderRadius *= 0.8
  pipeRadius *= 0.8


  y += Float(pipeRadius)
}

self.rootNode.addChildNode(cylinderNode)

[collapse]

Toy

6) Create a 25×25 grid of capsules in the unit square on the XZ plane. Animate they’re movement along the Y Axis. Determine the amount of movement via a 2 variable function. Color each capsule with a hue of abs(x * z)

Hint

The unit square is the square in the XZ planewith corners of coordinates (-1,0,1), (1,0,1), (1,0,-1), (-1,0,-1).

[collapse]

Solution

func sinFunction(x: Float,z: Float) -> Float {
   return 0.2 * sin(x * 5 + z * 3) + 0.1 * cos(x * 5 + z * 10 + 0.6) + 0.05 * cos(x * x * z)
}

func squareFunction(x: Float,z: Float) -> Float {
   return x * x + z * z
}

let gridSize = 25

let capsuleRadius:CGFloat = 1.0 / CGFloat(gridSize - 1)
let capsuleHeight:CGFloat = capsuleRadius * 4.0

var z:Float = Float(-gridSize + 1) * Float(capsuleRadius)

for row in 0..<gridSize {
  var x:Float = Float(-gridSize + 1) * Float(capsuleRadius)
  for column in 0..<gridSize {
      let capsule = SCNCapsule(capRadius: capsuleRadius, height: capsuleHeight)

      let hue = CGFloat(abs(x * z))
      let color = UIColor(hue: hue, saturation: 1.0, brightness: 1.0, alpha: 1.0)

      capsule.firstMaterial?.diffuse.contents = color

      let capsuleNode = SCNNode(geometry: capsule)

      self.rootNode.addChildNode(capsuleNode)

      capsuleNode.position = SCNVector3Make(x, 0.0, z)

      let y = CGFloat(squareFunction(x,z: z))
      //let y = CGFloat(sinFunction(x, z: z))

      let moveUp = SCNAction.moveByX(0, y: y, z: 0, duration: 1.0)
      let moveDown = SCNAction.moveByX(0, y: -y, z: 0, duration: 1.0)

      let sequence = SCNAction.sequence([moveUp,moveDown])

      let repeatedSequence = SCNAction.repeatActionForever(sequence)

      capsuleNode.runAction(repeatedSequence)

      x += 2.0 * Float(capsuleRadius)

  }

  z += 2.0 * Float(capsuleRadius)
}

[collapse]

For example here’s x² + z²

Square

And here’s: 0.2 * sin(5 * x + 3 * z) + 0.1 * cos(5 * x + 10 * z + 0.6) + 0.05 * cos(x * x * z)

Test

BONUS: Find some interesting 2D functions for these sort of animations

  7 comments for “Introduction To SceneKit – Part 2

  1. Atharva Vaidya
    November 18, 2014 at 2:50 pm

    Loved the tutorial! Please do some more for SceneKit. Could you tell me how I could make a 3D Rectangular grid?

  2. Atharva Vaidya
    November 18, 2014 at 2:54 pm

    Something like this:

    http://imgur.com/rS4ElIA

    • Grustaf
      November 22, 2014 at 8:50 pm

      You need to use open gl primitives then, specifically lines, using custom geometry. You can do that sort of easily in Scenekit, but animating the mesh is slow because you need to rebuild the geometry on every frame. If you post a question on stack overflow you’ll get a longer answer…

    • November 26, 2014 at 4:33 pm

      Have a look at that shows you how to draw a wireframe cube.

  3. A guy
    November 18, 2014 at 3:06 pm

    Thank you so much for the great tutorial. This is very helpful.

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.

Subscribe
We send about one email per week with our latest tutorials and updates
Never display this again :)