Measuring lengths using Swift's ARKit

Have you ever experienced needing a measuring device but don’t have any with you now? Fear not! Because, we can actually use our iOS devices using the ARKit to measure an object from one point to another.

In this app, we will add two points that will determine the start and end of the measurement. Also, a text of the length in inches will be shown right above the end point.

For better reference, you may also want to check out the whole code base here.

Setup

To begin, we need to create an Augmented Reality app. For this case, we will name it ARRuler. Select Swift as its language and the content technology as SceneKit. We can uncheck the Unit Test and UI Test for this as we won’t be utilizing it.

In our viewDidLoad() method, we can also remove the following lines, as we won’t be needing them.

sceneView.showsStatistics = true
let scene = SCNScene(named: "art.scnassets/ship.scn")!
sceneView.scene = scene

Also, in the same viewDidLoad() method, let’s add sceneView.debugOptions = [ARSCNDebugOptions.showFeaturePoints] so that we can track device position that will be used by ARKit.

Adding Start and End Points

Now that our project is already set, let’s override the touchesBegan(_:with) with the following code, to detect that the touches occurred in the view.

override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
}

Next, we need to get the location of the first touch within our ARSCNView. Then perform a hit test using the touch location to see if it corresponds to a feature point. Since the hit test result returns an array, we will use the first one to add a dot.

if let touchLocation = touches.first?.location(in: sceneView) {
    let hitTestResults = sceneView.hitTest(touchLocation, types: .featurePoint)

    if let hitResult = hitTestResults.first {
        addDot(at: hitResult)
    }
}

As you noticed, we haven’t created the addDot(at:) method yet so let’s do that. This method should accept a ARHitTestResult.

func addDot(at hitResult: ARHitTestResult) {
}

Inside the addDot(at:) method, we will add a SCNSphere geometry and give it a material with a red color.

let dotGeometry = SCNSphere(radius: 0.005)

let material = SCNMaterial()
material.diffuse.contents = UIColor.red

dotGeometry.materials = [material]

Then we will create a SCNNode having the SCNSphere geometry. Our dot node should have a x, y and z positions based on the passed ARHitTestResult. Then we will add the dot node into our scene view.

let dotNode = SCNNode(geometry: dotGeometry)
dotNode.position = SCNVector3(hitResult.worldTransform.columns.3.x,
                             hitResult.worldTransform.columns.3.y,
                             hitResult.worldTransform.columns.3.z)

sceneView.scene.rootNode.addChildNode(dotNode)

You may have observed that we have added only one dot. So let’s create a global variable of an array of SCNNode to contain our added dot node.

var dotNodes = [SCNNode]()

Now we have the array of dot nodes, we should add our node in it right after adding the dot node in the scene view.

dotNodes.append(dotNode)

Calculating Length

If we test the app right now, we can add multiple dots in our scene view. We need it to limit only to two so we can measure the length from the first point to the second point.

if dotNodes.count >= 2 {
   calculate()
}

We don’t have the calculate() method yet so let’s create one now, using func calculate(). Inside our calculate() method, our first node in the array of SCNNode will be the starting point and the second will be the end point.

let start = dotNodes[0]
let end = dotNodes[1]

To measure the length of the two points, we will be using the Pythagorean Theorem. The formula will be d = √(a2 + b2 + c2).

let a = end.position.x - start.position.x
let b = end.position.y - start.position.y
let c = end.position.z - start.position.z
let distance = sqrt(pow(a, 2) + pow(b, 2) + pow(c, 2))

The result will be in meters, so let’s convert this to inches so that it will be more readable for short distances.

let inches = distance * 39.370079

Placing 3D Text

Now that we got our computed length, let’s create an updateText(distance:atPosition:) method that places the value in our scene view. Inside our updateText(distance:atPosition:) method, we will create an SCNText geometry containing a material of color red.

let textGeometry = SCNText(string: "\(distance) in.",
                          extrusionDepth: 1.0)
textGeometry.firstMaterial?.diffuse.contents = UIColor.red

Then let’s create a global variable of SCNNode by adding var textNode = SCNNode(). Our SCNNode will having a position just above of the end point and a 0.1 scale.

textNode = SCNNode(geometry: textGeometry)
textNode.position = SCNVector3(position.x, position.y + 0.01, position.z)
textNode.scale = SCNVector3(0.01, 0.01, 0.01)

Once we have finished creating our text, we will add the text node into our scene view.

sceneView.scene.rootNode.addChildNode(textNode)

Resetting Measurement

If you have tested the app so far, you can notice that if we measure more than two lengths, they can overlap with each other and that can be confusing. Let’s fix that.

In our touchesBegan(_:with:) method and before we locate the touch, we will check if our dotNodes has already more than two items and then remove the dotNodes and clear out our dotNode array.

if dotNodes.count >= 2 {
   for dot in dotNodes {
       dot.removeFromParentNode()
   }   
   dotNodes = [SCNNode]()
}

Also, inside the checking of more than 2 dotNodes, we will remove the textNode by adding textNode.removeFromParentNode()

It should look something like this!

Now go ahead and try it!

Primary Photo by patricia serna on Unsplash

Blog Posts by Lorence Lim


Related Entries