First SceneKit Project

This post describes how I developed a pet appliction for Mac OS X Cocoa using SceneKit. The goal was to have a simple application were the user could press a button and have a ball move as a result. Here is the basic flow:

  • What is SceneKit?
  • My History with 3D-Application Programming
  • Basic Application Setup
  • What This Is Not
  • Into XCode: The Programming and UI Design
  • Why am I doing this, the big picture

What is SceneKit?

SceneKit is a Cocoa Framework for developing applications with 3D graphics for Mac OS X and iOS. This framework allows one to construct a scene filled with 3D objects using a scene graph. A scene graph uses nodes that serve as locations for objects and starting points for animations, simulations, and other dynamic actions in the scene. Nodes describe the objects in the scene and their relation to one another. Each scene has a root node which serves as the parent node for all objects in the scene. An child node can have child nodes.

My “History” with 3D-Application Programming

Around 2011, I got the wild idea for creating an application for a 3D robot simulator. Through research, I came to the conclusion that I needed to learn about and use OpenGL. I acquired the OpenGL SuperBible 4th Edition and commenced to learning the basics. Once I felt comfortable, I then hacked my way to an application using primitive shapes to approximate the links, joints, and base resembling the 5-degree-of-freedom (DOF) robot inside the graduate lab were I worked. The application allowed one to perform forward and inverse kinematics for the robot.

This was not an easy undertaking. OpenGL is about as low-level as one can get when it comes to graphics programming. Luckily, there was the GLU Library for drawing the most primitive of shapes, without it the effort would have ground to a full stop. Having to come up with the code to create the vertices and lines to form the primitives shapes I needed would have been a project in itself, and I probably would have never gotten to the real task.

Long story short, the application worked, but then I tried to get fancy. I attempted to add some fancy stuff to it. I do not remember quite what I did because this was 2011, but I ‘broke’ the application and it no longer worked as is once did. Then Apple updated the platform, OpenGL moved to the shader model and away from the fixed pipeline model, and graduate school got real. The application was doomed.

In late 2015, I got the idea for doing another a simple 3D application for my own pleasure. The application would allow a user to push a button and when doing so a force would be applied to a ball causing it to roll in a direction. I then remembered my trial with the 5-DOF simulator and OpenGL, and I cringed. But by 2015, Apple’s application platform had advanced greatly. Now, there was SceneKit, and after looking into the documentation and the initial sample projects of others, I realized the pain from all the infrastructural code I had to write for the 5-DOF simulator could be avoided. In addition, SceneKit had a proper physics engine that would allow me to do kinematics and dynamics.

In retrospect, I could have used a number of other game engine frameworks and platforms even back in 2011, but ignorance kept my focus narrow and with graduate school I could not do too much.

Basic Application Setup

A few things before we get into the meat of this tutorial:

  • OS X El Capitan 10.11.x
  • MacBook Pro Mid-2009 Core-2 Duo
  • XCode 7.x+
  • SceneKit Framework

What This Is Not

This tutorial not mention to teach one how to use XCode. This tutorial is not a definitive guide on creating applications using this SceneKit framework. This is a tutorial designed to walk one through the same process and code used to create this simple application. In other words, please prepare for imperfections.

Into XCode: The Programming and UI Design

To get started, open XCode. Create a new OS X Cocoa Application project and save it. Make sure to choose Swift as the programming language, as the code presented will be in Swift. I named my project PushingABallSK.

In the window, make sure the Navigation pane is visible. Show the Project Navigator by clicking on the folder icon. Add a new Swift file to the project. Name the file AppController.swift. Make sure the following code is in the new file to begin:

class AppController: NSObject {

}

In the Project Navigator open MainMenu.xib. This will open Interface Builder, the UI design tool for OS X and iOS. Click on the Window object in the Document Outline (see picture below). In the Utilities pane select the Window’s Size Inspector, set the mininum size of the window to 800×600 pixels (just for ease of development). Add an instance of NSObject to the Document Outline.

Make sure the NSObject instance is still selected, in the Utilities pane -> Identity Inspector, set the class of the NSObject instance to AppController, the class being coded to handle the events and processing for the application. Add an SCNView and two NSButton objects to the main window. For the SCNView, give its scene the name PushedBall. Also, set the Rendering API to whatever you desire. Here, Metal is chosen for the rendering API, only because Metal is the future of low-level 3D graphics API on the Mac and OpenGL is kind of the past. Position the views and add constraints to taste. The following video demonstrates the needed steps except adding constraints:

Back in the AppController file, add the following code:

import Cocoa
import SceneKit

class AppController: NSObject {

    @IBOutlet private weak var _sceneView: SCNView!

    @IBOutlet private weak var _pushButton: NSButton!

    @IBOutlet private weak var _resetButton: NSButton!

}

Make sure there is a closing brace for the class declaration/definition.

Make the connections between the outlets in the code and their objects by control-clicking the AppController object in the Document Outline in Interface Builder and then click-dragging from each outlet to the associated view in the main window.

Save your project.

Next, we need to add two actions for our application, one for pushing the ball and one to stop and reset the ball to the initial position. In code, add the following methods:

import Cocoa
import SceneKit

class AppController: NSObject {

    /* Code shortened */

    @IBAction func moveBall(sender: AnyObject) {

    }

    @IBAction func resetBall(sender: AnyObject) {

    }
}

Save your project. In addition, other functions are needed for the structure of our project. In code, add the following:

import Cocoa
import SceneKit

class AppController: NSObject {

    /* Code shortened */

    private func setupScene() {

    }

    private func setupBall() {

    }

    private func stopBall() {

    }

    override func awakeFromNib() {

    }

    /* code shortened */
}

The awakeFromNib() method gets called after IB finishes initializes UI objects under its responsibility. Here, that would be the NSWindow, the SCNView, and the NSButton objects. Modify awakeFromNib() method resemble the following:

import Cocoa
import SceneKit

class AppController: NSObject {

    /* Code shortened */

    override func awakeFromNib() {

        _sceneView.scene = SCNScene()

        setupScene()

        setupBall()
    }

    /* Code shortened */
}

Here, we initialize the SCNView’s scene property. It is necessary because, through my own trial and error, I discovered that IB does not initialized property this for you. The setupScene() method is where we will build the static elements of our scene. The setupBall() method is where we will add necessary physics and set values for our ball.

Before we start with the setupScene() function, three addition instance variables are needed. Added the following variables immediately after the outlet references:

class AppController: NSObject {

    private let _mySphereNode = SCNNode()

    private let _myPlaneNode = SCNNode()

    private let _gravityFieldNode = SCNNode()

    /* Code shortened... */
}

Code from the setupScene() function will be broken up into small parts, presented, and explained. First, the scene’s ambient light source is created, configured, then added:

private func setupScene() {

    // setup ambient light source
    let ambientLightNode = SCNNode()
    ambientLightNode.light = SCNLight()
    ambientLightNode.light!.type = SCNLightTypeAmbient
    ambientLightNode.light!.color = NSColor(white: 0.35, alpha: 1.0).CGColor
    // add ambient light the scene
    _sceneView.scene!.rootNode.addChildNode(ambientLightNode)

    /* Code shortened... */
}

Ambient light is a type of light that illuminates all objects in a space and is the same regardless of an object’s location in said space. A node for the light is first created. Next, a light source is assigned to the node’s light property. Ambient light is what is desired at this point, so the light type is set using the SCNLightTypeAmbient constant. After setting the ambient light’s color, its node is added as a child node to the scene’s root node. Next, added the following lines of code direction after:

private func setupScene() {

    /* Code shortened... */

    // setup onmidirectional light
    let omniLightNode = SCNNode()
    omniLightNode.light = SCNLight()
    omniLightNode.light!.type = SCNLightTypeOmni
    omniLightNode.light!.color = NSColor(white: 0.56, alpha: 1.0).CGColor
    omniLightNode.position = SCNVector3Make(0.0, 200.0, 0.0)
    _sceneView.scene!.rootNode.addChildNode(omniLightNode)

    /* Code shortened... */
}

Omni-directional light is also known as point light, and thus requires a location in space. Here, a node is created, the light is set to the appropriate type and given color, and the node is positioned along the positive y-axis. Finally added as a child node to the scene’s root node.

Now, we need a surface for the ball to rest and also to roll when the user wants to apply a force to push the ball. Add the following code immediately after the omni-directional light:

private func setupScene() {

    /* Code shortened... */

    // add plane
    let myPlane = SCNPlane(width: 110.0, height: 180.0)
    myPlane.firstMaterial!.diffuse.contents = NSColor.orangeColor().CGColor
    myPlane.firstMaterial!.specular.contents = NSColor.whiteColor().CGColor
    _myPlaneNode.geometry = myPlane

    /* Code shortened... */
}

Here, the plane is set to 110.0 x 180.0 units. When SceneKit initially creates a plane geometry, it places the plane at the origin along the z-axis, with the along the y-axis and the width along the x-axis. If one wants the plane to have a different along, the plane’s transform must be changed (more on this in a minute). Next, the plane is given colors for its diffuse and specular properties. Then, the plane geometry is added to the plane node defined and initialized at the top of the class declaration/definition. Now, we desire for the plane’s center to be along the y-axis and its longest side (in this case the height of the place) to be along the x-axis. Add the lines of code below behind the previous lines:

private func setupScene() {

    /* Code shortened... */

    // rotate -90.0 about the x-axis, then rotote -90.0 degrees about the y-axis
    var rotMat = SCNMatrix4MakeRotation(-3.14/2.0, 0.0, 1.0, 0.0)
    rotMat = SCNMatrix4Rotate(rotMat, -3.14/2.0, 1.0, 0.0, 0.0)
    _myPlaneNode.transform = rotMat
    _myPlaneNode.position = SCNVector3Make(0.0, 0.0, 0.0)

    /* Code shortened... */
}

A rotation matrix is created for turning the plane about the y-axis, then create a matrix for turning the plane about the x-axis and combine that one with the first matrix. The math for the rotation matrices occurs in an inside out first order. That is, the rotation about the x-axis will happen first, then the rotation about the y-axis.

The plane will require physics in order to properly interact with the ball, which will rest on top of it, i.e. participate in the physics simulation. First, some much needed constants will need to be defined. Insert the next three constants right after the lines of code declaring/defining the nodes:

class AppController: NSObject {

    /* Code shortened... */

    private let BallType = 1
    private let PlaneType = 2
    private let GravityType = 3

    /* Code shortened... */
}

These constants will be used for setting the node’s physicsbody contact test bitmask, collision bit mask, and category bit mask properties. Add the following lines to your code inside the setupScene() method directly beneath the rotation matrix lines from previous:

private func setupScene() {

    /* Code shortened... */

    // add physcis to plane
    _myPlaneNode.physicsBody = SCNPhysicsBody(type: .Static, shape: SCNPhysicsShape(geometry: myPlane, options: nil))
    // configure physics body
    _myPlaneNode.physicsBody!.contactTestBitMask = BallType
    _myPlaneNode.physicsBody!.collisionBitMask = BallType
    _myPlaneNode.physicsBody!.categoryBitMask = PlaneType

    /* Code shortened... */
}

Here, we are signaling to the physics engine that the plane will interact with the ball by setting its contactTestBitMask and collisionBitMask to the BallType value. Then, we set categoryBitMask to the value representing the plane. That value will be used when we set the contactTestBitMask and collisionBitMask for the ball. Next, add the plane node and the sphere node:

private func setupScene() {

    /* Code shortened... */

    // add plane to scene
    _sceneView.scene!.rootNode.addChildNode(_myPlaneNode)
    // add sphere node
    _sceneView.scene!.rootNode.addChildNode(_mySphereNode)

    /* Code shortened... */
}

In the final section of the setupScene() function, we setup a gravity field. The default gravity in SceneKit does not provide realistic world action and feel. Luckily, SceneKit provides a class to instantiate and setup custom fields. Add the following lines of code:

private func setupScene() {

    /* Code shortened... */

    // gravity folks...
    // first, set the position for field effect
    let gravityField = SCNPhysicsField.linearGravityField()
    gravityField.categoryBitMask = GravityType
    gravityField.active = true
    gravityField.direction = SCNVector3(0.0, -9.81, 0.0)
    gravityField.strength = 9.0
    gravityField.exclusive = true
    _gravityFieldNode.physicsField = gravityField
    _sceneView.scene!.rootNode.addChildNode(_gravityFieldNode)
        
    // set the default gravity to zero vector
    _sceneView.scene!.physicsWorld.gravity = SCNVector3(0.0, 0.0, 0.0)
}

The SCNPhysicsField class allows use to create an instance of a linear gravity field. The identify of the field with respect to the physics engine is set by assigning the field’s categoryBitMask to the GravityType value. Next, we make the field’s affect immediately active by setting the active property to true. The code indicates that the gravity field’s affect pushes objects down by setting its direction vector (0.0, -9.81, 0.0). The default vector value is (0.0, -1.0, 0.0); we set it to the standard gravity constant to ease the math. The strength property is a multiplier for the direction vector and how it affects objects in the scene. For example, the higher the value the faster an object will fall. It is important to keep in mind that SceneKit’s gravity is not “realistic” in the sense that it functions like real world gravity. This is why one may need to tune the scene’s gravity by setting the direction and/or strength to different values and testing the application. Next, we indicate that this field is to be the sole field acting in our scene. The custom gravity field is added to the gravity field node, and then the gravity field node itself is added to the scene’s root node as a child node. The last line is not really necessary, but it is extra assurance that the default gravity will have zero effect. Here is the entire setupScene method:

private func setupScene() {
        
    // setup ambient light source
    let ambientLightNode = SCNNode()
    ambientLightNode.light = SCNLight()
    ambientLightNode.light!.type = SCNLightTypeAmbient
    ambientLightNode.light!.color = NSColor(white: 0.35, alpha: 1.0).CGColor
    // add ambient light the scene
    _sceneView.scene!.rootNode.addChildNode(ambientLightNode)
        
    // setup onmidirectional light
    let omniLightNode = SCNNode()
    omniLightNode.light = SCNLight()
    omniLightNode.light!.type = SCNLightTypeOmni
    omniLightNode.light!.color = NSColor(white: 0.56, alpha: 1.0).CGColor
    omniLightNode.position = SCNVector3Make(0.0, 200.0, 0.0)
    _sceneView.scene!.rootNode.addChildNode(omniLightNode)
        
    // add plane
    let myPlane = SCNPlane(width: 110.0, height: 180.0)
    myPlane.firstMaterial!.diffuse.contents = NSColor.orangeColor().CGColor
    myPlane.firstMaterial!.specular.contents = NSColor.whiteColor().CGColor
    _myPlaneNode.geometry = myPlane
        
    // rotate -90.0 about the x-axis, then rotote -90.0 degrees about the y-axis
    var rotMat = SCNMatrix4MakeRotation(-3.14/2.0, 0.0, 1.0, 0.0)
    rotMat = SCNMatrix4Rotate(rotMat, -3.14/2.0, 1.0, 0.0, 0.0)
    _myPlaneNode.transform = rotMat
    _myPlaneNode.position = SCNVector3Make(0.0, 0.0, 0.0)
        
    // add physcis to plane
    _myPlaneNode.physicsBody = SCNPhysicsBody(type: .Static, shape: SCNPhysicsShape(geometry: myPlane, options: nil))
    // configure physics body
    _myPlaneNode.physicsBody!.contactTestBitMask = BallType
    _myPlaneNode.physicsBody!.collisionBitMask = BallType
    _myPlaneNode.physicsBody!.categoryBitMask = PlaneType
        
        
    // add plane to scene
    _sceneView.scene!.rootNode.addChildNode(_myPlaneNode)
    // add sphere node
    _sceneView.scene!.rootNode.addChildNode(_mySphereNode)
        
    // gravity folks...
    // first, set the position for field effect
    let gravityField = SCNPhysicsField.linearGravityField()
    gravityField.categoryBitMask = GravityType
    gravityField.active = true
    gravityField.direction = SCNVector3(0.0, -9.81, 0.0)
    gravityField.strength = 9.0
    gravityField.exclusive = true
    _gravityFieldNode.physicsField = gravityField
    _sceneView.scene!.rootNode.addChildNode(_gravityFieldNode)
        
    // set the default gravity to zero vector
    _sceneView.scene!.physicsWorld.gravity = SCNVector3(0.0, 0.0, 0.0)
}

Let us examine the setupBall() method:

private func setupBall() {
        
    let radius = 25.0
        
    // sphere geometry
    let mySphere = SCNSphere(radius: CGFloat(radius))
    mySphere.geodesic = true
    mySphere.segmentCount = 50
    mySphere.firstMaterial!.diffuse.contents = NSColor.purpleColor().CGColor
    mySphere.firstMaterial!.specular.contents = NSColor.whiteColor().CGColor
        
    // position sphere geometry, add it to node
    _mySphereNode.position = SCNVector3(0.0, CGFloat(radius), 0.0)
    _mySphereNode.geometry = mySphere
        
    // physics body and shape
    _mySphereNode.physicsBody = SCNPhysicsBody(type: .Dynamic, shape: SCNPhysicsShape(geometry: mySphere, options: nil))
    _mySphereNode.physicsBody!.mass = 0.5
    _mySphereNode.physicsBody!.contactTestBitMask = PlaneType
    _mySphereNode.physicsBody!.collisionBitMask = PlaneType
    _mySphereNode.physicsBody!.categoryBitMask = BallType    
}

The code above does several things related to the ball for the program. The ball’s geometry is created and configured, with its radius, segment count, and diffuse and specular properties assigned values. The sphere’s node is the assigned its position within the scene and the newly created and configured geometry is assigned to the sphere node’s geometry property. Next, the mySphere geometry object is used to create a dynamic physicsbody object and assign it to the sphere node’s physicsbody property. The sphere node physicsbody’s mass is given a value of 0.5 kilograms. All physicsbody properties are in SI units. As was done for the plane, the contactTestBitMask, collisionBitMask, and categoryBitMask sub-properties of the sphere node’s physicsbody property.

Remember the moveBall() function from earlier? Now, that function will be fully implemented. Fill in the moveBall() function to resemble the following code snippet:

class AppController: NSObject {

    /* Code shortened... */

    @IBAction func moveBall(sender: AnyObject) {
        
        let forceApplied = SCNVector3Make(40.0, 0.0, 0.0)
        
        if _mySphereNode.physicsBody?.isResting == true {
            
            _mySphereNode.physicsBody!.applyForce(forceApplied, impulse: true)
        }
        else if _mySphereNode.physicsBody?.isResting == false {
            print("ball not at rest...")
        }
        else {
            print("No physics associated with the node...")
        } 
    }
    /* Code shortened... */
}

First, a vector is created to specify the magnitude and direction of the force desired. Next, if/else statements are used to only apply the force if the sphere node contains a physicsbody and is resting. If the sphere node’s physicsbody is not resting, a statement is printed to the console indicating that the sphere is not at rest. The final statement will show in the console in the case that the sphere node does not have a physicsbody. The 2nd and 3rd statements of the if-else structure are there for debugging, and were seriously helpful.

The stopBall() method will allow for the rolling sphere to be stopped. This method will also cause the sphere to be removed from the scene. Modify the stopBall() method to resemble the following:

class AppController: NSObject {

    /* Code shortened... */

    private func stopBall() {
        
        _mySphereNode.geometry = nil
        _mySphereNode.physicsBody = nil
    }
    /* Code shortened... */
}

By setting the sphere node’s geometry and physicbody properties to nil, the goal previous mentioned is accomplished. This method will be call when the resetBall() action method is called.

class AppController: NSObject {

    /* Code shortened... */

    @IBAction func resetBall(sender: AnyObject) {
        
        // remove the ball from the sphere node
        stopBall()
        
        // reset the ball
        setupBall()
    }
}

The stopBall() and setupBall() methods from previous are called within the resetBall() method when the user presses the button associated with reseting the ball. This arrangement assures that the ball will stop and be removed from the screen (stopBall()), then a new ball will be created, configured, assigned to the sphere node, and reappear on screen ready for the user once more (setupBall()).

Why am I Doing This and The Big Picture

There is not much to say here, but here it is: improve my coding skills on the OS X platform and make more applications.

Leave a Reply

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