We are going to start a new series of tutorials about SceneKit, the framework that allows you to build and manipulate 3D objects in a 3D scene. SceneKit was introduced for the first time in macOS 10.8 (Mountain Lion) and successively in iOS 8. Recently, SceneKit was added to watchOS 3.0 and tvOS 9.

SceneKit allows developers to create 3D games and add 3D content to apps using high-level scene descriptions. Because of the recent introduction of ARKit, SceneKit is today a very relevant framework. If you want to build Augmented Reality application, you should learn SceneKit. The framework provides a reach set of APIs that make easy to add animations, physics simulation, particle effects, and realistic physically based rendering.

We are going to explore the basics of SceneKit, first. In future posts, I will show you more sophisticated techniques, writing custom shaders and mixing SceneKit with Metal.

Nodes and Scene Graph

SceneKit is based of the concept of nodes. Each 3D object you want to render using SceneKit is a node, an object of type SCNNode. An SCNNode object by itself has no visible content when the scene containing is rendered on screen. An SCNNode is only a model object providing the coordinate space transform (position, orientation, and scale) relative to its parent node.

To build a SceneKit 3D scene, you use a hierarchy of nodes to create its structure, then you add lights, cameras, and geometry to each node to create the visible content. SceneKit implements content as a hierarchical tree structure of nodes, also known as scene graph. You may create a node hierarchy programmatically or load one from a file created using 3D authoring tools, or combine the two approaches.

The rootNode object in a scene defines the coordinate system of the rendered 3D world. Each child node you add to this root node has its own coordinate system, which is in turn inherited by its own children. This is very similarly to the view hierarchy in UIKit.

SceneKit displays scenes in a SCNView, processing the scene graph and performing animations before efficiently rendering each frame on the GPU.

Before working with SceneKit, you should be familiar with basic graphics concepts such as coordinate systems and the mathematics of three-dimensional geometry. SceneKit uses a right-handed coordinate system where (by default) the direction of view is along the negative z-axis, as illustrated below.

Figure 1. SceneKit coordinate system.
Figure 1. SceneKit coordinate system.

You add 2D and 3D objects to a scene by attaching SCNGeometry objects to nodes. Geometries, in turn, have attached SCNMaterial objects that determine their appearance. To shade the geometries in a scene with light and shadow effects, add nodes with attached SCNLight objects. To control the viewpoint from which the scene appears when rendered, add nodes with attached SCNCamera objects. We are going to look at all these classes in this post.

Creating a node

To create a node you use the init(geometry:) to initialize an object with a specific geometry (see later, Geometry). You can also initialize a node using a Model I/O object using the init(mdlObject:) method. Model I/O is a relative new Cocoa framework that provides a system-level understanding of 3D model assets and related resources. Model I/O was introduced in iOS 9.

Another way to create a node is to use the SceneKit node editor available in Xcode. The editor appears when you select a SceneKit file. Then, you drag nodes from the object library and assign a name to them.

Managing node’s transformations

Once the node is created, you can assign it a position, eulerAngles and scale to transform the node.

The node’s position locates it within the coordinate system of its parent, as modified by the node’s pivot property. The default position is the zero vector, indicating that the node is placed at the origin of the parent node’s coordinate system.

The node’s eulerAngles represent the node’s orientation, expressed as pitch, yaw, and roll angles, each in radians. The order of components in this vector matches the axes of rotation:

  • Pitch (the x component) is the rotation about the node’s x-axis.
  • Yaw (the y component) is the rotation about the node’s y-axis.
  • Roll (the z component) is the rotation about the node’s z-axis.

SceneKit applies these rotations relative to the node’s pivot property in the reverse order of the components: first roll, then yaw, then pitch.

The node’s scale represents the scale factor applied to the node. Each component of the scale vector multiplies the corresponding dimension of the node’s geometry. The default scale is 1.0 in all three dimensions.

The node’s pivot is similar to the anchorPoint of a CALayer in Core Animation. The default pivot is SCNMatrix4Identity, specifying that the node’s position locates the origin of its coordinate system, its rotation is about an axis through its center, and its scale is also relative to that center point.

The node’s orientation is expressed as a quaternion (SCNQuaternion). The rotation, eulerAngles, and orientation properties all affect the rotational aspect of the node’s transform property. Any change to one of these properties is reflected in the others.

The transform property of a node is a 4x4 matrix of type SCNMatrix4. This property combines the node’s rotation, position, and scale properties. The default transformation is SCNMatrix4Identity. When you set the value of this property, the node’s rotation, orientation, eulerAngles, position, and scale properties automatically change to match the new transform, and vice versa. SceneKit can perform this conversion only if the transform you provide is a combination of rotation, translation, and scale operations. If you set the value of this property to a skew transformation or to a nonaffine transformation, the values of these properties become undefined.

In iOS 11, the SCNNode class duplicates all the above properties and uses SIMD types to simplify the interoperability between Metal and SceneKit. For example, you can now set the position of a node using the old position property and the new simdPosition. In the same way, you can now use simdEulerAngles, simdScale, simdOrientation, simdTransform and simdPivot in place of eulerAngles, scale, orientation, transform and pivot.

Managing node’s attributes

A node has additional properties you can set:
- name is a String you ca use as unique identifier for a node
- light is a SCNLight object you can attach to the node to represent a light
- camera is a SCNCamera object representing a point of view
- geometry is a SCNGeometry object representing the shape of the node
- morpher is a SCNMorpher object responsible of blending node’s geometry
- skinner is a SCNSkinner object responsible for skeletal animations of a node
- categoryBitMask is an Int that defines which category the node belongs to. You can assign each node of a scene to one or more categories.

Modifying the node visibility

You can modify the visibility of a node using the following properties:
- isHidden is a boolean that determines the visibility of the node
- opacity is a CGFloat representing the opacity value of a node
- renderingOrder is an Int representing the order the node’s content is drawn respect to other nodes. Default value is zero.
- castsShadow is a boolean that determines whether SceneKit renders the node’s contents into shadow maps
- movabilityHint controls how the node contributes to various motion-related effects during rendering. The default value is fixed. This value is merely a hint that communicates to SceneKit’s rendering system about how you want to move content in your scene; it does not affect your ability to change the node’s position or add animations or physics to the node.

Managing the node hierarchy

To inspect the scene graph, you can use the parent property to ask for the parent node. The rootNode of a scene does not have a parent. Additionally, you can use the childNodes property to retrieve the array of child nodes for the current node.

The methods addChild(_:) and insert_:at:) allow you to add a new node to another node. The method removeFromParentNode() removes a node from the scene graph. Finally, replaceChildNode(_:with:) removes a child from the node’s array of children and inserts another node in its place.

Searching the node hierarchy

If you want to search a specific node in the scene graph, you can use methods such as childNode(withName:recursively:). This method requires the name of the node you are searching and a boolean value. If the boolean value is true, SceneKit search the entire node subtree, otherwise it searches only the immediate children.

The childNodes(passingTest:) method returns every node satisfying a provided test. For example, you can search for empty nodes using a block that returns true for nodes whose light, camera, and geometry properties are all nil.

enumerateChildNodes(_:) applies a closure to the child node and descendant nodes. Finally, enumerateHierarchy(_:) executes a specified closure for each of the node’s child and descendant nodes, as well as for the node itself.

Customizing node rendering

If you want to customize the appearance of a node, you have different options. This is a very wide topic that I will cover in a future post. However, there is also a simpler solution based on Core Image. The filters property of a node is an array of CIFilters you can assign to a node. The filters are then applied to the rendered contents of the node. SceneKit renders the node (and its child node hierarchy) into an image buffer and then applies the filters before compositing the filters’ output into the rendered scene. The order of the array determines the order of the Core Image filter chain.

You can assign to the node a rendererDelegate object responsible for rendering custom contents for the node using Metal or OpenGL.

Working with positional audio

You can add an audio player or multiple audio players to a node using the addAudioPlayer(:_) method or the audioPLayers property. You pass an SCNAudioPlayer object to the node. This player is initialized either using an SCNAudioSource or a AVAudioNode. The audio player is a controller for playback of a positional audio source in a SceneKit scene.

You can use the removeAudioPlayer(_:) and removeAllAudioPlayers()methods to remove the audio player from the node.

Copy a node

It sometimes necessary to copy a node. The clone() method creates a copy of a node and its children. For a non-recursive copy, use the copy() method, which creates a copy of the node without any child nodes. Be aware that cloning or copying a node creates a duplicate of the node object, but not the geometries, lights, cameras, and other SceneKit objects attached to it—instead, each copied node shares references to these objects.

If you have node hierarchy, you can use the flattenedClone() method and obtain a new single node containing the combined geometries and materials of the node and its child node subtree.

Converting between node coordinate space

If you want to convert the position of a node from and to another node, you can use convertPosition(_:from:) and convertPosition(_:to:) methods. Instead the methods convertTransform(_:from:) and convertTransform(_:to:) convert a transformation matrix from and to the node coordinate space defined by another node.

Scene View

SceneKit uses an SCNView to render a 3D scene. An SCNView is a subclass of UIView on iOS and tvOS or NSView on macOS. You create an SCNView using Interface Builder or programmatically using the init(frame:options:) method. The second argument of the init(frame:options:) method is a set of options you can pass to the view to define:

  1. The preferred rendering API. This can be either Metal, OpenGL ES 2.0, Open GL, Open GL 3.2 or Open GL 4.1. Please, check the documentation for the SCNRenderingAPI enumeration for more details.
  2. The preferred Metal device. If you selected Metal as the rendering API, you can also select the metal device (MTLDevice) you want to use. For example, on a macOS system with multiple GPUs, you can define which GPU will be used by SceneKit.
  3. If your system has multiple GPUs, you can set assign a boolean value to the preferLowPowerDevice option.

After creating a SceneKit view, you assign a scene to it, using the view scene property. The SCNView class provides different properties and methods. For example, you can use the allowsCameraControl property to determine whether the user can manipulate the current point of view that is used to render the scene. If you set this property to true, the user can modify the current point of view with the mouse or trackpad in macOS or with multitouch gestures on iOS. This action does not modify camera objects already existing in the scene graph or the nodes containing them. The default value of this property is false.

If you need to smooth edges of a rendered scene, you can change the sampling rate of the rendering using the antialiasingMode property. This property can be set to the following values:

  1. none (removes the multisampling)
  2. multisampling2X
  3. multisampling4X
  4. multisampling8X
  5. multisampling16X

Multisampling renders each pixel multiple times and combines the results, creating a higher quality image at a performance cost proportional to the number of samples it uses.

The SCNView class also provide the snapshot() method to render the scene view in an image (UIImage for iOS and NSImage for macOS). Notice that this method is thread-safe and may be called at any time.

Build a scene

Let’s see how to build a very simple scene. Let’s create a new Xcode project. I am going to use Xcode 9 for this and next examples. Name the project SimpleScene. The first thing we do is to add a scene view to the storyboard. Open the Main.storyboard file and add a SceneKit view on top of the main view of the view-controller. Add the layout constraints to keep the scene view full screen. Now, go to the ViewController.swift and add import SceneKit below the import UIKit statement. Let’s create an outlet for the scene view:

Connect the outlet to the scene view.

Now, let’s create a scene. In the finder of your mac, create a new folder. Rename the folder scene.scnassets. Drag and drop the folder in the Xcode project. Create a new SceneKit file.

Figure 2. Adding a scene to the Xcode project.
Figure 2. Adding a scene to the Xcode project.

Name it scene.scn and final drag it in to the recently created folder. This folder improves the performance of loading this file at runtime. In the viewDidLoad() method, add the following lines of code:

Finally, let’s add something to the scene. Open the scene.scn file. Drag and drop a sphere from the Object Library to the scene. If you click on the small button on the bottom left corner of the scene (see next figure), you will unveil the Scene graph. In our Scene graph, we can see 2 nodes: camera and sphere.

Figure 3. Open the node graph.
Figure 3. Open the node graph.

The camera is a special node to provide a point of view for the user. Without the camera, you cannot render the scene at runtime. When we created the outlet, we set the property allowsCameraControl to true to let the user control the camera using pan and drag gestures. When doing so, we are moving the camera in the 3D space.

Go to the scene and select the camera node. Then, open the Node Inspector in Xcode (third tab in the Utilities panel). There, set the camera Position, Euler and Scale to the following values:

Figure 4. The camera settings in the SceneKit Editor.
Figure 4. The camera settings in the SceneKit Editor.

Now, select the sphere, and set its Position, Euler and Scale to the following values:

Figure 5. New values of the camera settings.
Figure 5. New values of the camera settings.

Finally, run the application. You should see the sphere on the screen.

The camera

The camera is a very important SceneKit node. Without the camera we cannot visualize the 3D world. To display a scene, you must designate a node whose camera property contains a camera object as the point of view. Every SCNNode object has a camera property that defines a point of view—that is, the position and orientation of the camera. The camera’s direction of view is always along the negative z-axis of the node’s local coordinate system. To point the camera at different parts of your scene, use the position, rotation, or transform property of the node containing it.

The camera defines a region of the 3D space called frustum. Figure 6 highlights the important parts of the frustum.

Figure 6. The camera frustum.
Figure 6. The camera frustum.

As you can see in Figure 6, the camera is at the apex of the pyramid. You can set the camera field of view using the fieldOfView property. Instead, you can set the camera near and the far depth limits, using the zNear and zFar properties. Starting with iOS 10, Apple introduced also other properties that make your camera simulate more realistic camera features. For example, you can decide to use an High Dynamic Range (HDR) camera and set different properties such as wantsHDR, minimumExposure, maximumExposure, whitePoint and so on. You can also adjust the contrast, saturation and color grading of the camera (contrast, saturation, colorGrading). In iOS 11, Apple added additional properties such as focalLenght, focusDistance, sensorHeight, fStop, and more.

Geometry

I would like to highlight that the sphere node we used in the previous example is a node with a specific shape. Indeed, an SCNNode does not have any specific shape. Instead, you need to set the geometry property of a node to the specific shape you want. SceneKit provides built-in shapes:

  1. Floor (SCNFloor)
  2. Box (SCNBox)
  3. Capsule (SCNCapule)
  4. Cone (SCNCone)
  5. Cylinder (SCNCylinder)
  6. Plane (SCNPlane)
  7. Pyramid (SCNPyramid)
  8. Sphere (SCNSphere)
  9. Torus (SCNTorus)
  10. Tube (SCNTube)
  11. Text (SCNText)
  12. Shape (SCNShape).

All these built-in shapes are subclasses of SCNGeometry.

Let’s give a look at these geometries. The following picture highlights all the shapes available in SceneKit.

Figure 7. The available built-in geometries.
Figure 7. The available built-in geometries.

SCNFloor

A floor extends in the x- and z-axis dimensions of its local coordinate space, and is located in the plane whose y-coordinate is zero. You can set length and width for a floor, using the length and width properties. Other properties, such as reflectivity, reflectionFalloffStart, reflectionFalloffEnd, and reflectionResolutionScaleFactor, are used to control the reflectivity of the floor.

SceneKit creates a reflection effect by rendering the scene twice. First, it renders the scene into an offscreen buffer, using a point of view whose position is the reflection of the camera’s position. Next, it renders the scene from the camera’s point of view, using the offscreen buffer as a texture map for the floor’s surface. Rendering the scene twice incurs a performance cost. Reducing the resolution of the offscreen buffer reduces this cost but causes the reflected image to appear blurry.

SCNBox

A SCNBox geometry defines the shape of the box in the x-, y-, and z-axis dimensions of its local coordinate space by setting its width, height, and length properties. If you want to add rounded edges and corners to a box, you can use the chamferRadius property. To position and orient a box in a scene, attach it to the geometry property of an SCNNode object.

Figure 8. A box geometry with different chamfer radius.
Figure 8. A box geometry with different chamfer radius.

SCNCapsule

A SCNCapsule geometry defines the size of the two hemispheres forming the ends of a capsule with the capRadius property. Because the cylindrical body of the capsule stretches between its two hemispherical ends, its circular cross section in the x- and z-axis dimensions has the same radius. You can define the capsule’s extent in the z-axis dimension of its local coordinate space with the height property. To change the orientation of a capsule, you simply adjust the transform property of the node containing the capsule geometry.

Figure 9. Two capsule with different height and radius.
Figure 9. Two capsule with different height and radius.

SCNCone

A cone defines the surface of a solid whose base is a circle and whose side surface tapers to a point centered above its base. You define the size of the cone’s base in the x- and z-axis dimensions of its local coordinate space with its bottomRadius property, and its extent in the y-axis dimension with its height property. You can create a cone that tapers to a point by setting its topRadius property to zero. The following figure highlights cones with different topRadius values.

Figure 10. Cones with different top radius value.
Figure 10. Cones with different top radius value.

SCNCylinder

A cylinder defines the surface of a solid whose every cross section along a linear axis is a circle of equal size. You can define the size of the cylinder’s cross section in the x- and z-axis dimensions of its local coordinate space with the radius property, and its extent in the y-axis dimension with the height property.

Figure 11. Cylinders with different height and radius.
Figure 11. Cylinders with different height and radius.

SCNPlane

A plane defines a flat surface in the x- and y-axis dimensions of its local coordinate space according to its width and height properties. To orient a plane differently, adjust the transform property of the node containing the plane geometry. You can create a rounded rectangular plane using the cornerRadius property.

Figure 12. Planes with different corner radii.
Figure 12. Planes with different corner radii.

SCNPyramid

A pyramid defines the surface of a solid whose base is a rectangle, and whose four triangular side faces converge at a point centered above its base. You define the shape of the pyramid’s base in the x- and z-axis dimensions of its local coordinate space with the width and length properties, and its extent in the y-axis dimension with the height property.

Figure 13. Pyramids with different width, height and length.
Figure 13. Pyramids with different width, height and length.

SCNSphere

A sphere defines a surface whose every point is equidistant from its center, which is placed at the origin of its local coordinate space. You define the size of the sphere in all three dimensions using its radius property.

If you set the sphere’s isGeodesic property to true, SceneKit constructs the sphere by successively subdividing the triangular surfaces of a regular icosahedron.

SCNTorus

A torus is mathematically defined as a surface of revolution formed by revolving a circle around a coplanar axis. It is the product of two circles: a large ring and a pipe that encircles the ring. SceneKit uses these terms to define the dimensions of a torus geometry in its local coordinate space. The torus’ ringRadius property defines a circle in the x- and z-axis dimensions, centered at the origin, and its pipeRadius property defines the width of the surface encircling the ring.

Figure 14. Torus geometries with different ring and pipe radius.
Figure 14. Torus geometries with different ring and pipe radius.

SCNTube

The outer surface of a tube is a cylinder. Define the size of the cylinder’s cross section in the x- and z-axis dimensions of its local coordinate space with the outerRadius property, and its extent in the y-axis dimension with the height property.

Figure 15. Tubes with different inner and outer radii.
Figure 15. Tubes with different inner and outer radii.

SCNText

You provide text for the geometry using an NSString or NSAttributedString object. In the former case, the properties of the SCNText object determine the style and formatting of the entire body of text. When you create a text geometry from an attributed string, SceneKit styles the text according to the attributes in the string, and the properties of the SCNText object determine the default style for portions of the string that have no style attributes. SceneKit can create text geometry using any font and style supported by the Core Text framework, with the exception of bitmap fonts (such as those that define color emoji characters).

In the local coordinate system of the text geometry, the origin corresponds to the lower left corner of the text, with the text extending in the x- and y-axis dimensions. The geometry is centered along its z-axis.

Figure 16. 3D text in SceneKit.
Figure 16. 3D text in SceneKit.

SCNShape

SceneKit can create a 3D geometry by extruding a Bézier path, which extends in the x- and y-axis directions of its local coordinate space, along the z-axis by a specified amount.
This geometry is not available in the SceneKit editor. So, you can only build it programatically. You create this geometry using the init(path:extrusionDepth:) method, passing a Bezier path and the amount of extrusion. After that, you can chamfer the resulting 3D shape in different way using the chamferMode, chamferProfile and chamferRadius properties.

Figure 17. A custom 3D shape.
Figure 17. A custom 3D shape.

Conclusions

In this post, we look at some of the main classes in SceneKit. We just touched the surface of this framework that offers a huge set of functionalities. We will cover more about this framework in next posts. So, stay tuned.

Geppy

Geppy Parziale (@geppyp) is cofounder of InvasiveCode. He has developed many iOS applications and taught iOS development to many engineers around the world since 2008. He worked at Apple as iOS and OS X Engineer in the Core Recognition team. He has developed several iOS and OS X apps and frameworks for Apple, and many of his development projects are top-grossing iOS apps that are featured in the App Store. Geppy is an expert in computer vision and machine learning.

iOS Consulting | INVASIVECODE

iOS Training | INVASIVECODE