TL;DR We’ll add SceneKit to a project and build a simple scene
What is SceneKit?
SceneKit is a powerfull high level framework for adding 3D graphics to your apps. You can use it to build games, interactive visualizations, 3D user interfaces or any kind of gaming or non-gaming appliclications. SceneKit has been around since 2012 but was previously only available on OSX. Before SceneKit iOS Developers had to learn OpenGL or use 3rd party Frameworks which often did not interact well with the rest of iOS or had significant limitations.
Creating a new SceneKit Project
Let’s look at creating a new project that uses SceneKit. Apple provides a template for SceneKit projects(Application -> Games) but for this tutorial we’ll be adding SceneKit to an existing project to get a better understanding on how it works.
Open Xcode and create a new Single View Application (iOS -> Application -> Single View Application). Make sure to select Swift as the Language and iPad as the Devices. This tutorial is build for iPad because of the larger screen size.
SceneKit isn’t included by default in our project so we’ll have to add it.
- Select your project from the organizer on the left.
- Select your project’s target under “TARGETS”.
- Select the “Build Phases” tab
- Expand “Link Binary With Libraries” and click the plus button on the bottom
- Search for “scenekit”, select it and click “Add”
Setting up the view
Classes in the SceneKit library are prefixed with SCN. SCNView is a sublass of UIView
that is used to display your 3D Scene.
The actual contents of your 3D Scene are stored in an instance of the SCNScene class which can be created programmatically or loaded from Collada(dae) or Alembic(abc)(Only works for OS X apps) files.
It’s a good idea to create a custom subclass of SCNScene so that you make the scene’s logic self contained. This also helps when you start having multiple scenes in your app.
SCNScenes
are displayed by SCNViews
. Let’s create a custom scene for our project. Create a new class named PrimitivesScene
(File -> New File -> Cocoa Touch Class), make sure to subclass SCNScene
We’ll have to change the class of the ViewController’s view to SCNView so that we can display our PrimitivesScene
. Open the “Main.storyboard” File. Select ViewController -> View go to “Identity Inspector” and change the Class field to SCNView
.
To present the scene open the ViewController.swift
file and in the viewDidLoad
method set the view’s scene
property to a new instance of PrimitivesScene
. Notice that we have to cast the ViewController view to SCNView
to access the scene
property. We also set the backgroundColor
to black.
let scnView = self.view as! SCNView
scnView.scene = PrimitivesScene()
scnView.backgroundColor = UIColor.blackColor()
Make sure to also import SceneKit at the top of the file.
import SceneKit
Running the app right now will just produce a black screen, that’s because our scene is currently empty. Let’s add a sphere to it. Open the PrimitivesScene.swift
file. We’ll initialize the contents of our scene in the init method:
override init() {
super.init()
}
We also have to provide an init with coder method because SCNScene
conforms to the NSCoder
protocol which has an required init method. We’ll just leave the default implementation because we don’t plan on loading our scene from a file.
required init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
Note: You should also add “import SceneKit” to the top of the “PrimitivesScene.swift” file if it’s not added automatically for you.
Adding a sphere
Each scene manages a hierarchy of nodes called the scene graph. Nodes can contain child nodes, and each node except the topmost has a parent. It’s simillar to the way other hierarchical nested structures work like the hierarchy of UIViews
or file systems. We add 3D objects to our scene by creating new nodes that contain geometry information.
To create a sphere you have to create a new instance of SCNNode using a geometry of type SCNSphere. SCNSphere
is a subclass of SCNGeometry and provides the 3D data needed to draw the node’s geometry.
let sphereGeometry = SCNSphere(radius: 1.0)
let sphereNode = SCNNode(geometry: sphereGeometry)
A scene’s root node is the node at the top of the node hierarchy and can be accessed using the scene’s rootNode
property.
To add the sphereNode to the scene we use the addChildNode
method on the rootNode.
self.rootNode.addChildNode(sphereNode)
If you run the app right now you’ll see a white sphere at the center of the screen.
Let there be light
The sphere is indistinguishable from a circle because no lights are enabled in the scene. SceneKit provides some default lighting that can be enabled by setting the SCNView
autoenablesDefaultLighting
property to true
. In the ViewController
class add the following line at the end of the viewDidLoad
method
scnView.autoenablesDefaultLighting = true
The sphere will now appear shaded.
Moving around
To get a better view of the scene it’s useful to be able to change the camera of the scene. The camera controls the point of view and the direction of view of the scene. Like with lighting SceneKit provides a helpful default camera that can be enabled by setting the scnView.allowsCameraControl
to true in the ViewController
class
scnView.allowsCameraControl = true
The point of view of the scene can now be modified. The camera can be controlled in the following ways:
- rotate the scene around the camera’s target by panning with one finger.
- move the scene by panning with 2 fingers
- zoom the scene by pinching with 2 fingers
- tilt the camera by performing a rotation gesture with 2 fingers
More spheres!
Let’s add a second sphere to the scene to get a better idea of how the camera behaves. We’ll have to position the second sphere so that it doesn’t overlap with the first one. We can set a SCNNode
‘s position
property to specify it’s position in 3D space. SceneKit uses a right handed coordinate system where the x axis points right, the y axis points up and the z axis points towards you. Positions are expressed using an instance of SCNVector3
where the x,y,z components specify the displacement along each axis.
Here’s a picture for better understanding. We have one sphere of radius 1.0 centered at (0.0,0.0,0.0) – the origin of our coordinate system. Nodes are positioned at the origin by default.
We’ll create another sphere centered at (3,0,0) with a radius of 0.5 In the init method of the PrimitivesScene
class add the following code:
let secondSphereGeometry = SCNSphere(radius: 0.5)
let secondSphereNode = SCNNode(geometry: secondSphereGeometry)
secondSphereNode.position = SCNVector3(x: 3.0, y: 0.0, z: 0.0)
self.rootNode.addChildNode(secondSphereNode)
The app should display 2 spheres now. SpriteKit automatically adjusts the camera so that all objects in the scene are visible. Moving around will give you a better sense of depth.
This is how the spheres looks in the coordinate system:
Adding Color
To change the color of an object we have to assign a new UIColor
instance to the contents
of the diffuse
property of a geometry’s firstMaterial
. That’s quite a handfull and we’ll go into more details on this in a future tutorial on materials and lighting. For now lets make the first sphere orange and the second sphere purple. Add the following line below the declaration of sphereGeometry
sphereGeometry.firstMaterial?.diffuse.contents = UIColor.redColor()
and the following line below the declaration of secondSphereGeometry
secondSphereGeometry.firstMaterial?.diffuse.contents = UIColor.greenColor()
The scene should look like this now:
In Part 2 we’ll look at the different types of primitives that SceneKit provides, animations and nesting nodes.
Challenges
These challenges are intended to give you a better understanding of positioning objects in 3D space.
1) Create a third sphere with a radius of 1.5, red color and position it above the orange sphere
let thirdSphereGeometry = SCNSphere(radius: 1.5)
thirdSphereGeometry.firstMaterial?.diffuse.contents = UIColor.redColor()
let thirdSphereNode = SCNNode(geometry: thirdSphereGeometry)
thirdSphereNode.position = SCNVector3(x: 0.0, y: 3.0, z: 0.0)
self.rootNode.addChildNode(thirdSphereNode)
2) Create a line of 20 spheres with radius 1.0, with distance 1.0 between each other along the xAxis (coordinates (0,0,0),(3,0,0),(6,0,0)…) Make their colors alternate between red and green.
Create spheres in a for loop using an index
Color the sphere orange if the index is even, purple if it’s odd
var x:Float = 0.0
var radius:CGFloat = 1.0
let numberOfSpheres = 20
for i in 1...numberOfSpheres {
let sphereGeometry = SCNSphere(radius: radius)
if (i % 2 == 0) {
sphereGeometry.firstMaterial?.diffuse.contents = UIColor.orangeColor()
} else {
sphereGeometry.firstMaterial?.diffuse.contents = UIColor.purpleColor()
}
let sphereNode = SCNNode(geometry: sphereGeometry)
sphereNode.position = SCNVector3(x: x, y: 0.0, z: 0.0)
self.rootNode.addChildNode(sphereNode)
x += 3 * Float(radius)
}
3) Create a line of 20 spheres tangent (just touching) to each of their neighbors. Make the radius of each sphere 0.05 smaller then the sphere to it’s left.
The distance between the centers of 2 tangent spheres of radii r1 and r2 is r1 + r2
var x:Float = 0.0
var radius:CGFloat = 1.0
let numberOfSpheres = 20
for i in 0..<numberOfSpheres {
let sphereGeometry = SCNSphere(radius: radius)
if (i % 2 == 0) {
sphereGeometry.firstMaterial?.diffuse.contents = UIColor.orangeColor()
} else {
sphereGeometry.firstMaterial?.diffuse.contents = UIColor.purpleColor()
}
let sphereNode = SCNNode(geometry: sphereGeometry)
sphereNode.position = SCNVector3(x: x, y: 0.0, z: 0.0)
self.rootNode.addChildNode(sphereNode)
x += Float(radius)
radius -= 0.05
x += Float(radius)
}
4) Make a grid of 20×20 spheres of radius 1 in the XY plane. Make each sphere tangent to it’s neighbors. Color the spheres in a grid pattern with orange and purple.
var y:Float = 0.0
var radius:CGFloat = 1.0
let yCount = 20
let xCount = 20
for row in 0..<yCount {
var x:Float = 0.0
for column in 0..<xCount {
let sphereGeometry = SCNSphere(radius: radius)
if (row + column) % 2 == 0 {
sphereGeometry.firstMaterial?.diffuse.contents = UIColor.orangeColor()
} else {
sphereGeometry.firstMaterial?.diffuse.contents = UIColor.purpleColor()
}
let sphereNode = SCNNode(geometry: sphereGeometry)
sphereNode.position = SCNVector3(x: x, y: y, z: 0.0)
self.rootNode.addChildNode(sphereNode)
x += 2 * Float(radius)
}
y += 2 * Float(radius)
}
5) Make a grid like in the previous exercise. Assume that each sphere has a row and column number from 0 to 19. Use the formula sphereColor = i & j != 0 ? UIColor.purpleColor() : UIColor.orangeColor()
Here’s some more information about these types of patterns:
Bitwise And Fractals
Sierpinki in Pascal’s Triangle
var y:Float = 0.0
var radius:CGFloat = 1.0
let yCount = 20
let xCount = 20
for row in 0..<yCount {
var x:Float = 0.0
for column in 0..<xCount {
let sphereGeometry = SCNSphere(radius: radius)
sphereGeometry.firstMaterial?.diffuse.contents = row & column != 0 ? UIColor.purpleColor() : UIColor.orangeColor()
let sphereNode = SCNNode(geometry: sphereGeometry)
sphereNode.position = SCNVector3(x: x, y: y, z: 0.0)
self.rootNode.addChildNode(sphereNode)
x += 2 * Float(radius)
}
y += 2 * Float(radius)
}
6) Make a triangle out of spheres.
BONUS: Make the spheres tangent to each other
var y:Float = 0.0
var radius:CGFloat = 1.0
let yCount = 20
let xCount = 20
for row in 0..<yCount {
var x:Float = Float(radius) * Float(row)
for column in row..<xCount {
let sphereGeometry = SCNSphere(radius: radius)
if ((row + column) % 2 == 0) {
sphereGeometry.firstMaterial?.diffuse.contents = UIColor.orangeColor()
} else {
sphereGeometry.firstMaterial?.diffuse.contents = UIColor.purpleColor()
}
let sphereNode = SCNNode(geometry: sphereGeometry)
sphereNode.position = SCNVector3(x: x, y: y, z: 0.0)
self.rootNode.addChildNode(sphereNode)
x += 2 * Float(radius)
}
y += sqrt(3.0) * Float(radius)
}
7) Make a 7x7x7 cube out of spheres. Alternate each level of the cube with orange and purple.
BONUS: Don’t add any spheres inside the cube
var y:Float = 0.0
var radius:CGFloat = 1.0
let yCount = 7
let zCount = 7
let xCount = 7
for row in 0..<yCount {
var z:Float = 0.0
for depth in 0..<zCount {
var x:Float = 0.0
for column in 0..<xCount {
let sphereGeometry = SCNSphere(radius: radius)
if (row % 2 == 0) {
sphereGeometry.firstMaterial?.diffuse.contents = UIColor.orangeColor()
} else {
sphereGeometry.firstMaterial?.diffuse.contents = UIColor.purpleColor()
}
let sphereNode = SCNNode(geometry: sphereGeometry)
sphereNode.position = SCNVector3(x: x, y: y, z: z)
self.rootNode.addChildNode(sphereNode)
x += 2.0 * Float(radius)
}
z += 2 * Float(radius)
}
y += 2 * Float(radius)
}
I can’t find any information on importing an Alembic file in the documentation. Do you know how to do this?
Unfortunately this only works for OSX apps.
Updated the article.
Ok, thanks. So would I be able to load an .abc file through the SCNScene::scenenamed or SCNNode? SCNGeometrySource?
I can see preview the .abc in xcode but I can’t figure out how to load it in a scene.
example .abc file:
https://alembic.googlecode.com/files/Alembic_Octopus_Example.tgz
You use the SCNScene::scenenamed method to load models. Don’t forget to add the “.abc” extension to the file you want to load.
For example:
let scene = SCNScene(named: “alembic_octopus.abc”)
Then you can traverse the node hierarchy starting from the rootNode to get the node you want.
http://imgur.com/R5MorG6
Thanks so much for this tutorial and your time with it. I greatly appreciate it as a newbie trying to learn.
What a fantastic tutorial! I have a question: how do we adjust position of camera? Can we customize camera control?
Hey,
To have greater control of the camera you need to create a new instance of SCNCamera and attach it to a node
let camera = SCNCamera()
let cameraNode = SCNNode()
cameraNode.camera = camera
cameraNode.position = SCNVector3Make(0, 0, 5.0)
rootNode.addChildNode(cameraNode)
You can then move/rotate the node to position the camera.
You don’t necessarily have to attach the camera to the rootNode. For example you could create a 3rd person camera by adding the camera behind some object in your 3D Scene and moving the object.
Great tutorial. Can’t wait for part 2. Will there be anything on SCNShape?
Hi,
I’m currently working on part 2, should be ready by Thursday. Unfortunately I won’t cover SCNShape and SCNText as they are more advanced and specialized primitives.
hi. Awesome tutorial. Will you be also showing how to make different scene files like mainmenu, gameplay and option scene???
You can bet on it! We are going to make more SceneKit tutorials. The first series will introduce basic concepts.
“change the class of the ViewController’s view to SCNView” does not work and just creating the file : “PrimitivesScene.swift” with
“//
// PrimitivesScene.swift
// SceneKitTut1
//
// Created by David Karlsson on 2014-11-11.
// Copyright (c) 2014 davidkarlsson. All rights reserved.
//
import UIKit
class PrimitivesScene: SCNScene {
}
”
Does not work, the class field just switches back and the PrimitivesScene file complaints that SCNScene is undeclared, Is there missing some import step?
Make sure to select the “View” in interface builder and not “View Controller”.
Reference
In the PrimitivesScene class add “import SceneKit” at the top of the file, below “import UIKit”. This should happen automatically but it seams that Xcode is bugged.
Thanks for tutorial. It’s really helpful.
And I have a doubt. I had to cast PrimitiveScene to remove the error: ‘init()’ is unavailable: superseded by import of -[NSObject init].
scnView.scene = PrimitiveScene() as SCNScene
Can you explain why!!
Seems to be some issue in recent versions of Swift. (This error was not present at the time of writing of the tutorial).
SO is there anyway to fix this error? It’s impeding any progress.
Found the error.
Above you had written:
scnView.scene = PrimitivesScene()
When it should be:
let scene = PrimitivesScene()
Now it works!
Confirmed! At 26 dec 2014, the code:
scnView.scene = PrimitivesScene()
doesn’t work anymore, probably because Apple has changed something in the Swift.
But it works, as Scott provided:
let scene = PrimitivesScene()
Also, after adding a single sphere (“If you run the app right now you’ll see a white sphere at the center of the screen.”) I see a black screen. Looks like without light in the scene it can be displayed.
and then you need scnView.scene = scene
Found it eventually in the project download!
Great tutorial, thanks for taking the time to explain the basics….
Spheres in my View are getting out of screen. Why would that happen. I’m new and learning. Can you please help.
Yeah, that sometimes happens with the default camera controls in SceneKit. Seems to be a SceneKit issue.
Can we do something with the camera settings so that the scene fit in screen. Please guide.
Got it. Read you previous comment and rearranged the camera position. I could have done that before commenting
I have the latest Xcode. Got some errors on a couple of lines:
let sphereNode = SCNNode(geometry: sphereGeometry)
– ‘PrimitivesScene.Type’ does not have a member named ‘sphereGeometry’
self.rootNode.addChildNode(sphereNode)
– expect declaration
Any thoughts?
Here are my codes so far:
import UIKit
import SceneKit
class PrimitivesScene: SCNScene {
let sphereGeometry = SCNSphere(radius: 1.0)
let sphereNode = SCNNode(geometry: sphereGeometry)
self.rootNode.addChildNode(sphereNode)
override init() {
super.init()
}
required init(coder aDecoder: NSCoder) {
fatalError(“init(coder:) has not been implemented”)
}
}
Code should look like this:
override init() {
super.init()
let sphereGeometry = SCNSphere(radius: 1.0)
let sphereNode = SCNNode(geometry: sphereGeometry)
self.rootNode.addChildNode(sphereNode)
}
I’ll update the article soon to avoid confusion on where all the pieces of code go.
I am new to animation. I created single view application with swift, now i am trying to create “PrimitivesScene” but it is not giving me SCNScene in subclass list. I recently installed xcode 6.1.1
Please help.
will this work with iPhone? What about Collada or Alembic?
yes and yes
You have to link the SceneKit framework. Follow the steps or download the starter project from here.
Even if it does not give you SCNScene in the list you can still manually type in the name in the “subclass of” field and it will still work as expected. Make sure not to make a typo though “SCNScene”. Code completion in swift is wonky at times.
Hey guys,
Loving this set of lessons, thanks a lot for taking the time to put this up.
-Nikhil.
This no longer seems to work in Xcode 6.4. The compiler requires you to use “as!” here:
let scnView = self.view as! SCNView
to fix compiler error.
But even when that is fixed all you get is a black view. I even tried the Towers of Hanoi starter project. Same thing happens. All black.
Something must have changed..
ok got it working. Just need to fix compiler error by using “as!”.
Must be camera angles or whatnot, the first sphere appears dead center, but when I add the second sphere the first one goes half off the screen to the left, and the new one half off the screen to the right.
Plus the radius doesn’t seem to do anything on my first sphere, it’s always huge when by itself.
Weirdness, but really good tutorial, I’m finding my way just a bit better now.
It’s a really good tutorial,but maybe code could be more “Swift”…
”
for i in 0..<20
{
let sphereGeometry = SCNSphere(radius: 1)
sphereGeometry.firstMaterial?.diffuse.contents = i % 2 == 0 ? UIColor.orangeColor() : UIColor.purpleColor()
let sphereNode = SCNNode(geometry: sphereGeometry)
sphereNode.position = SCNVector3(i * 3,0,0)
rootNode.addChildNode(sphereNode)
}
"