
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!