Have you noticed the many utility ARKit apps on the App Store that allow you to measure the sizes of horizontal planes in the world? Guess what? After this tutorial, you'll be able to do this yourself!
In this tutorial, you'll learn how to make your augmented reality app for iPads and iPhones by using ARKit. Specifically, we'll go over how we can detect horizontal planes and their dimensions (width and length).
What Will You Learn?
We'll be learning how to detect horizontal planes and their sizes accurately using ARKit.
Minimum Requirements
- Mac running macOS 10.13.2 or later.
- Xcode 9.2 or above.
- A device with iOS 11+ on an A9 or higher processor. Basically, the iPhone 6S and up, the iPad Pro (9.7-inch, 10.5-inch, or 12.9-inch; first-generation and second-generation), and the 2017 iPad or later.
- Swift 4.0. Although Swift 3.2 will work on Xcode 9.2, I strongly recommend downloading the latest Xcode to stay up to date.
- An Apple Developer account. However, it should be noted that you don't need a paid Apple Developer account. Apple allows you to deploy apps on a test device using an unpaid Apple Developer account. That said, you will need a paid Developer account in order to put your app in the App Store. (See Apple's site to see how the program works before registering for your free Apple Developer account.)
Step 1: Download the Assets You Will Need
To make it easier to follow along with this tutorial, I've created a folder with the required 2D assets and Swift file needed for the project. These files will make sure you won't get lost in this guide, so download the zipped folder containing the assets and unzip it.
Step 2: Set Up the AR Project in Xcode
If you're not sure how to do this, follow Step 2 in our post on piloting a 3D plane using hitTest to set up your AR project in Xcode. Be sure to give your project a different name, such as NextReality_Tutorial4. Make sure to do a quick test run before continuing on with the tutorial below.
Step 3: Import Assets into Your Project
In the project navigator, click on the "Assets.xcassets" folder. We'll be adding our 2D images here. Then, right-click on the left pane of the area in the right side of the project navigator. Choose "Import" and add the "overlay_grid.png" file from the unzipped Assets folder.
Next, once again in the project navigator, right-click on the yellow folder for "NextReality_Tutorial4" (or whatever you named your project). Choose the "Add Files to 'NextReality_Tutorial4'" option.
Navigate to the unzipped "Assets" folder, and choose the "Grid.swift" file. Be sure to check "Copy items if needed" and leave everything else as is. Click on "Add."
"Grid.swift" should be added into your project, and your project navigator should look something like this:
This file will help render a grid for every horizontal plane ARKit detects.
Step 4: Place a Grid to Show Detected Horizontal Planes
To quickly go over ARKit's plane detection capabilities, take a look at our tutorial on horizontal plane detection.
Open the "ViewController.swift" class by double-clicking it. If you want to follow along with the final Step 4 code, just open that link to see it on GitHub.
In the "ViewController.swift" file, modify the scene creation line in the viewDidLoad() method. Change it from:
let scene = SCNScene(named: "art.scnassets/ship.scn")!
To the following, which ensures we are not creating a scene with the default ship model.
let scene = SCNScene()
Next, find this line at the top of the file:
@IBOutlet var sceneView: ARSCNView!
Under that line, add this line to create an array of "Grid's" for all horizontal planes detected:
var grids = [Grid]()
Copy and paste the following two methods as listed below to the end of the file before the last curly bracket ( } ) in the file. These methods will allow us to add our Grid on the horizontal planes detected by ARKit as a visual indicator.
// 1.
func renderer(_ renderer: SCNSceneRenderer, didAdd node: SCNNode, for anchor: ARAnchor) {
let grid = Grid(anchor: anchor as! ARPlaneAnchor)
self.grids.append(grid)
node.addChildNode(grid)
}
// 2.
func renderer(_ renderer: SCNSceneRenderer, didUpdate node: SCNNode, for anchor: ARAnchor) {
let grid = self.grids.filter { grid in
return grid.anchor.identifier == anchor.identifier
}.first
guard let foundGrid = grid else {
return
}
foundGrid.update(anchor: anchor as! ARPlaneAnchor)
}
Let's quickly go over what's happening in these two methods:
- The didAdd() is called whenever a new node is added to the ARSCNView. Here, we take the detected ARPlaneAnchor and add it as our Grid object, which adds the grid image we imported to any plane detected.
- The didUpdate() is called whenever newer ARPlaneAnchor nodes are detected or when the plane is expanded. In that case, we want to update and expand our grid as well. We do that here by calling update() on that specific Grid.
Now, let's enable feature points. Under this line in viewDidLoad():
sceneView.showsStatistics = true
Add:
sceneView.debugOptions = ARSCNDebugOptions.showFeaturePoints
Next, let's turn on horizontal plane detection. Under this line in viewWillAppear():
let configuration = ARWorldTrackingConfiguration()
Add:
configuration.planeDetection = .horizontal
This is very important! It will ensure that ARKit is able to detect horizontal, flat geometric planes in the real world. The feature points will allow us to see all the 3D points ARKit is able to detect.
Now, run your app on your phone and walk around. Focus on a well lit area on the ground, you should be able to see blue grids appear whenever a horizontal plane is detected:
Checkpoint: Your entire project at the conclusion of this step should look like the final Step 4 code on my GitHub.
Step 5: Measure the Detected Horizontal Planes
Now, we'll use the "extent" property of the detected planes in order to acquire its dimensions and print them out with the help of SceneKit. All of the code changes below will be done on the "Grid.swift" file. Make sure to open that and follow the steps below. Feel free to walk along the finished version as you go through the steps.
Open the "Grid.swift" class and under this line:
var planeGeometry: SCNPlane!
Add a new geometry for the text that will display the dimensions of the detected planes:
var textGeometry: SCNText!
Next, find the line that imports ARKit:
import ARKit
Under this line, and above the line that creates the Grid class, add this code:
extension ARPlaneAnchor {
// Inches
var width: Float { return self.extent.x * 39.3701}
var length: Float { return self.extent.z * 39.3701}
}
Here, we extend the functionality of ARPlaneAnchor. By default, the size is returned in meters. We create two properties: "width" and "length" that convert the meters to inches. We use the "extent" property of the ARPlaneAnchor and get its "x" and "z" value. Can you guess why we don't get the "y" value? That's because geometric planes are flat and don't have any height!
If you'd like to read up more on Extensions in Swift, take a look at the Apple documentation.
Next, find the setup() method. Delete this method and replace it with an updated version of the method as shown below:
private func setup() {
planeGeometry = SCNPlane(width: CGFloat(anchor.width), height: CGFloat(anchor.length))
let material = SCNMaterial()
material.diffuse.contents = UIImage(named:"overlay_grid.png")
planeGeometry.materials = [material]
let planeNode = SCNNode(geometry: self.planeGeometry)
planeNode.physicsBody = SCNPhysicsBody(type: .static, shape: SCNPhysicsShape(geometry: planeGeometry, options: nil))
planeNode.physicsBody?.categoryBitMask = 2
planeNode.position = SCNVector3Make(anchor.center.x, 0, anchor.center.z);
planeNode.transform = SCNMatrix4MakeRotation(Float(-Double.pi / 2.0), 1.0, 0.0, 0.0);
// 1.
let textNodeMaterial = SCNMaterial()
textNodeMaterial.diffuse.contents = UIColor.black
// Set up text geometry
textGeometry = SCNText(string: String(format: "%.1f\"", anchor.width) + " x " + String(format: "%.1f\"", anchor.length), extrusionDepth: 1)
textGeometry.font = UIFont.systemFont(ofSize: 10)
textGeometry.materials = [textNodeMaterial]
// Integrate text node with text geometry
// 2.
let textNode = SCNNode(geometry: textGeometry)
textNode.name = "textNode"
textNode.position = SCNVector3Make(anchor.center.x, 0, anchor.center.z);
textNode.scale = SCNVector3Make(0.005, 0.005, 0.005)
addChildNode(textNode)
addChildNode(planeNode)
}
We're keeping the setup() method the same as it was in order to set up the logic of the grid. However, we've supplemented it with logic that adds the size of the grid on top of the grid. Let's examine this:
- We've set up a geometry for the text using the SCNText class. We then set the text to be the plane's width and height, calculated using our custom extension. We then modified the font size and set our custom material (black colored text) to the "materials" property of the SCNText geometry.
- Here, we've created a new SCNNode with the SCNText geometry from the above step and updated its position and scale appropriately. We then added both the text and plane nodes to our Grid node.
This allows us to show the size of the plane when it is first set up. Additionally, we should keep updating the size of the plane as it keeps getting larger. Let's do that in the next code changes.
Find the update(anchor: ARPlaneAnchor) method. At the end of the method, before its last curly bracket (}), add the following code:
if let textGeometry = self.childNode(withName: "textNode", recursively: true)?.geometry as? SCNText {
// Update text to new size
textGeometry.string = String(format: "%.1f\"", anchor.width) + " x " + String(format: "%.1f\"", anchor.length)
}
We've also updated the text of the SCNText geometry we added previously to ensure that the dimensions will continue to be updated visually as the plane expands.
Save and run the app. Walk around in a well lit, textured area and focus on the ground (or any horizontal plane, like a table). You should start to see blue grids appear along with the size of the planes being updated as the detected planes keep getting larger.
Checkpoint: Your entire project at the conclusion of this step should look like the final Step 5 code on my GitHub.
What We've Accomplished
Good job! You were successfully able to measure the ground with precision using ARKit! Let's go over what you learned from this tutorial: placing a grid over detected horizontal planes, using the extent property of ARPlaneAnchor to get the plane's size, and visually rendering the size using SceneKit.
If you need the full code for this project, you can find it in my GitHub repo. I hope you enjoyed this tutorial on ARKit. If you have any comments or feedback, please feel free to leave it in the comments section. Happy coding!
Just updated your iPhone to iOS 18? You'll find a ton of hot new features for some of your most-used Apple apps. Dive in and see for yourself:
Be the First to Comment
Share Your Thoughts