Adding spheres to a scene using ARKit

IMG_1CA961D76C9F-1
Just some spheres floating above my bed

Boilerplate

First we’ll want to set up the basic ARKit scene. All this is boilerplate provided if you create an ARKit project in XCode


//  ViewController.swift

import UIKit
import SceneKit
import ARKit

class ViewController: UIViewController, ARSCNViewDelegate {

    @IBOutlet var sceneView: ARSCNView!

    override func viewDidLoad() {
        super.viewDidLoad()

        // Set the view's delegate
        sceneView.delegate = self

        // Show statistics such as fps and timing information
        sceneView.showsStatistics = true

        // Create a new scene
        let scene = SCNScene()

        // Set the scene to the view
        sceneView.scene = scene
    }

    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)

        // Create a session configuration
        let configuration = ARWorldTrackingSessionConfiguration()
        configuration.planeDetection = .horizontal

        // Run the view's session
        sceneView.session.run(configuration)
    }

    override func viewWillDisappear(_ animated: Bool) {
        super.viewWillDisappear(animated)

        // Pause the view's session
        sceneView.session.pause()
    }
}

One nice thing to add when developing with ARKit is the debug point thingies:


// Goes in viewDidLoad()
sceneView.debugOptions = ARSCNDebugOptions.showFeaturePoints

Spheres

Now, we’ll create a separate sphere class to abstract away the SCNNodes which will contain the sphere:


//  Sphere.swift

import Foundation
import ARKit

class Sphere: SCNNode {

    static let radius: CGFloat = 0.01

    let sphereGeometry: SCNSphere

    // Required but unused
    required init?(coder aDecoder: NSCoder) {
        sphereGeometry = SCNSphere(radius: Sphere.radius)
        super.init(coder: aDecoder)
    }

   	// The real action happens here
    init(position: SCNVector3) {
        self.sphereGeometry = SCNSphere(radius: Sphere.radius)

        super.init()

        let sphereNode = SCNNode(geometry: self.sphereGeometry)
        sphereNode.position = position

        self.addChildNode(sphereNode)
    }

    func clear() {
        self.removeFromParentNode()
    }

}

Now, when we want to add a sphere, we can do something like this


    func addSphere(position: SCNVector3) {
        print("adding sphere at point: \(position)")
        let sphere: Sphere = Sphere(position: position)
        self.sceneView.scene.rootNode.addChildNode(sphere)
// if we keep an array of these babies, then calling
// sphere.clear() on each will remove them from the scene
spheres.append(sphere)
    }

Insert into scene

Now for the interseting bit. To calculate the position at which we’d like to place the sphere, we can take the position of the camera and add a transform in the direction the camera is facing. All this code could be executed in a button press handler, for instance.

I found this handy screenSpacePosition function in a stack overflow post, it’ll do the vector math addition along the z axis. The distance is negative because we want to go further into the scene.


    func sceneSpacePosition(inFrontOf node: SCNNode, atDistance distance: Float) -> SCNVector3 {
        let localPosition = SCNVector3(x: 0, y: 0, z: -distance)
        let scenePosition = node.convertPosition(localPosition, to: nil)
        // to: nil is automatically scene space
        return scenePosition
    }

then, in the button press handler:


if let cameraNode = self.sceneView.pointOfView {

	let distance: Float = 0.3 // Hardcoded depth
    let pos = sceneSpacePosition(inFrontOf: cameraNode, atDistance: distance)    

    addSphere(position: pos)
}

hitTest()

Another way to do this would be to try to add the object “into” the scene, at a depth which is the location in z space of the object in front of the camera. We can do this with a hit test:


if let cameraNode = self.sceneView.pointOfView {

	let width = sceneView.frame.size.width;
	let height = sceneView.frame.size.height;

	let distance = sceneView.hitTest(CGPoint(x: width, y: height), types: [.existingPlaneUsingExtent, .featurePoint]).first!.distance;
	let pos = sceneSpacePosition(inFrontOf: cameraNode, atDistance: Float(distance))

    addSphere(position: pos)
}

And that’s all for this post!

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

%d bloggers like this: