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 vertices, normals, texture 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)
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 doubleSided
property
Example:
let plane = SCNPlane(width: 1.0, height: 1.5)
// Make the plane visible from both sides
plane.firstMaterial?.doubleSided = true
You can create a plane with rounded corners by setting the plane’s cornerRadius
property. Example:
plane.cornerRadius = 0.1
Box
SCNBox objects let you create cubes by specifying their width
, height
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)
The chamferRadius
determines the amount by which the boxe’s corners should be rounded.
Example:
box.chamferRadius = 0.05
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)
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)
Cone
SCNCone objects let you create cones and conical frustums(cones with their top cut off). You create cones by specifying a bottom radius
, top radius
and height
.
Example:
let cone = SCNCone(topRadius: 0.5, bottomRadius: 1.0, height: 1.5)
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.
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)
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)
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)
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
}
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.
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°.
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 / Hypotenuse, cos(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
}
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)
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)
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.
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.
let floor = SCNFloor()
let floorNode = SCNNode(geometry: floor)
floorNode.position.y = -2.5
self.rootNode.addChildNode(floorNode)
2) Position the primitives in a spiral that goes twice around the origin
Increase the y coordinate at each iteration
You have to make the angle increment 2 times bigger to go twice around the circle
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
}

3) Draw a flight of stairs 20 stairs.
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)
}
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.
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
}
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.
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)
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)
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).
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)
}
For example here’s x² + z²
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)
BONUS: Find some interesting 2D functions for these sort of animations
Loved the tutorial! Please do some more for SceneKit. Could you tell me how I could make a 3D Rectangular grid?
Something like this:
http://imgur.com/rS4ElIA
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…
Have a look at that shows you how to draw a wireframe cube.
Thank you so much for the great tutorial. This is very helpful.