Blog
March 25, 2019Visualizing Surfaces Detected by ARKit
Visualizing ARKit Planes
Apple has worked hard to make building apps with ARKit as easy as possible. If you’re new to ARKit, it's a framework that uses the measurement sensors on the device and the camera to detect details in the physical environment. When it detects something that's new or that changed in the environment, it will notify you. Then you can create visual elements with these physical details. One example of a physical detail that ARKit can detect planes or surfaces, like the floor or a table. To demonstrate this idea, in this article we will be visualizing planes detected by ARKit.
Depending on the UI in your ARKit app, you might not necessarily show the exact position and size of detected planes at all times. However, there are still situations when visualizing ARKit planes is useful. For example, maybe you’re debugging object placement and want to see how objects are interacting with planes or maybe you’re building an outdoor application where it’s difficult to know how much of a large area has been detected. In my experience, it’s helpful to use these visualizations so as to say to your users, “You can interact with this area.” Visualizing planes is also quick to implement, so it’s a good place to start if you’re giving ARKit a spin for the first time! If you’d like to follow along, here’s a link to the code on GitHub.
ARKit
Before we get started, let’s briefly talk about how ARKit works. ARKit uses a combination of image data captured from device camera and motion data captured from device measurement sensors. With this data, ARKit uses machine learning and computer vision algorithms to analyze the environment. This analysis defines feature points which depending on how you configure ARKit at launch will create objects of interest such as detected surfaces or detected 2D/3D images.
Rendering Frameworks
It’s important to note that ARKit does not by itself generate any visual components for the user. When developing with ARKit, we must choose a “Content Technology” in order to display something. Apple gives us these three options:
- SceneKit – A 3D rendering framework
- SpriteKit – A 2D rendering framework
- Metal – A 3D rendering framework for advanced users with extra functionality
Note that these content frameworks can also be used on their own in applications like games or simulations. However, when using them with ARKit, data about the environment is mixed with virtual content created with one of these content technologies.
For this blog, since we will be visualizing 3D planes, we will render them using SceneKit. Here’s how it will work. ARKit will let us know when it thinks it’s detected a plane. Then we will take the position and sizing information about the plane and create a SceneKit object with it.
SceneKit
Let’s quickly look at how SceneKit works at a high level.
-
SCNScene
has a tree structure ofSCNNodes
, with arootNode
as the top most parent. -
An
SCNNode
is by itself just a point in the coordinate system of theSCNScene
’s space. It carries the information of its position, orientation, and scale. -
To give an
SCNNode
a shape, we can add oneSCNGeometry
per node. For example, we could add anSCNBox
,SCNSphere
, or, in our case, anSCNPlane
. -
An
SCNMaterial
is how we can add color or texture to ourSCNGeometry
.
The Project
Now that we know the basics of SceneKit, let’s see how we will be using it with ARKit. In the project we have the following Storyboard file:
You’ll notice that the Storyboard has an ARSCNView
that
covers the entire screen. ARSCNView
is an object that spans
across three separate Apple frameworks: ARKit, SceneKit, and UIKit. From
Apple’s
Documentation
on ARSCNView
:
- The view automatically renders the live video feed from the device camera as the scene background.
- The world coordinate system of the view’s SceneKit scene directly responds to the AR world coordinate system established by the session configuration.
- The view automatically moves its SceneKit camera to match the real-world movement of the device.
Now let’s take a look at the view lifecycle of the view controller for this Storyboard:
override func viewDidLoad() {
super.viewDidLoad()
sceneView.delegate = self
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
let configuration = ARWorldTrackingConfiguration()
configuration.planeDetection = [.horizontal]
sceneView.session.run(configuration)
}
First, we set this view controller as the delegate for our scene view in the
viewDidLoad
method. We’ll go over this more when we take a
look at the delegate methods themselves. Next, we set the configuration for
the ARSession
. Take a look at this diagram from the Apple
docs:
An ARSCNView
has an ARSession
object which
coordinates all the components required to analyze the environment. These
components include the device’s motion sensor hardware, data from the
camera, as well as algorithms and libraries that compile this data.
An ARSession
requires an ARConfiguration
in order to
get started. ARKit is able to track the environment, track images, track
faces, and scan 3D objects. We have to decide what we want ARKit to do in our
application and we use an ARConfiguration
to decide which
functionality to focus on. We will be using an
ARWorldTrackingConfiguration
with horizontal plane detection for
this project since we’re interested in tracking the physical features of
the environment and visualizing horizontal planes.
Now we’re ready to implement the ARSCNViewDelegate
methods.
There are several methods in the protocol for this delegate, but here we only
need to implement two of them:
func renderer(_ renderer: SCNSceneRenderer, didAdd node: SCNNode, for anchor: ARAnchor) {
}
func renderer(_ renderer: SCNSceneRenderer, didUpdate node: SCNNode, for anchor: ARAnchor) {
}
This is possibly the most important part of understanding how to create visual
elements based off of results from ARKit. Let’s first talk about
ARAnchors
. From the Apple Developer Documentation an
ARAnchor
is “a real-world position and orientation that can
be used for placing objects in an AR scene.” There are several classes
that subclass ARAnchor
depending on the type of object detected,
for example ARImageAnchor
and ARObjectAnchor
.
Specifically, we will be interested in ARPlaneAnchor
s since
we’re trying to visualize planes.
I’d like to emphasize that there is no visual component of an
ARAnchor
on its own. ARAnchors
won’t show up
on the screen for the user by default as they’re detected. This is where
SceneKit comes in and this is why these ARSCNViewDelegate
methods
are so convenient. These methods will get called when ARKit recognizes a new
item in your scene, in this case a horizontal plane via an
ARPlaneAnchor
. Since an ARPlaneAnchor
has the
position, orientation, and size of the plane, we have everything we need to
create a virtual object to mimic its physical properties. We also have a
reference to an empty SCNNode
that’s created specifically
for this anchor.
func renderer(_ renderer: SCNSceneRenderer, didAdd node: SCNNode, for anchor: ARAnchor) {
// Cast ARAnchor as ARPlaneAnchor
guard let planeAnchor = anchor as? ARPlaneAnchor else { return }
// Create SCNGeometry from ARPlaneAnchor details
let width = CGFloat(planeAnchor.extent.x)
let height = CGFloat(planeAnchor.extent.z)
let planeGeometry = SCNPlane(width: width, height: height)
// Add material to geometry
let material = SCNMaterial()
material.diffuse.contents = UIColor.blue.withAlphaComponent(0.8)
planeGeometry.materials = [material]
// Create a SCNNode from geometry
let planeNode = SCNNode(geometry: planeGeometry)
planeNode.position = SCNVector3(planeAnchor.center.x, planeAnchor.center.y, planeAnchor.center.z)
planeNode.eulerAngles.x = -.pi / 2
// Add the newly created plane node as a child of the node created for the ARAnchor
node.addChildNode(planeNode)
}
First, we cast the added ARAnchor
as an
ARPlaneAnchor
. Then we take the physical size of the plane anchor
and use it to create a SCNPlane
of the same dimensions. Note that
we’re only using the x and z components of the plane anchor’s
extent. Since an ARPlaneAnchor
is a two-dimensional shape, its
y-component will always be zero. Then we add an SCNMaterial
to
the geometry. Here we’re choosing a semi-transparent, blue color so we
can see beneath our virtual surface. Next, we create an
SCNNode
using the plane geometry we just created and set its
position to match the center of the plane anchor. Finally, we add the new
plane node as a child node of the anchor’s node.
Next, we need to fix the fact that we’re currently only adding planes
when they’re first detected, but we’re not updating the visuals
over time to match the updates from ARKit. To fix this, we need to make
changes to our next ARSCNDelegate
method,
renderer(_:didUpdate:for:)
.
func renderer(_ renderer: SCNSceneRenderer, didUpdate node: SCNNode, for anchor: ARAnchor) {
// Cast ARAnchor as ARPlaneAnchor, get the child node of the anchor, and cast that node's geometry as an SCNPlane
guard
let planeAnchor = anchor as? ARPlaneAnchor,
let planeNode = node.childNodes.first,
let planeGeometry = planeNode.geometry as? SCNPlane
else { return }
// Update the dimensions of the plane geometry based on the plane anchor.
planeGeometry.width = CGFloat(planeAnchor.extent.x)
planeGeometry.height = CGFloat(planeAnchor.extent.z)
// Update the position of the plane node based on the plane anchor.
planeNode.position = SCNVector3(planeAnchor.center.x, planeAnchor.center.y, planeAnchor.center.z)
}
If you understood what was going on in the
renderer(_:didAdd:for:)
method, then you’ll probably
understand what going on in this method as well since it’s mostly the
same code. First, we need cast the ARAnchor
to an
ARPlaneAnchor
like before. Then, we pick the first child node of
the updated anchor node. This will be the plane node we added as a child node
in the renderer(_:didAdd:for:)
method. We’ll also cast the
geometry of the plane node as an SCNPlane
. Finally, we’ll
update the plane geometry’s dimensions and the plane node’s
position based off of the plane anchor’s updated information.
Let’s run the app and take a look.
Hm, well that doesn’t look right. Why are these SCNPlane
s
sticking straight up? From the Apple Developer
Documentation
on SCNPlane
:
“A plane defines a flat surface in the x- and y-axis dimensions of its local coordinate space according to its width and height properties. To orient a plane differently, adjust the transform property of the node containing the plane geometry.”
The issue is that each plane is created in the x- and y-axis dimensions, where we want it in the x- and z-axis dimensions. So, all we need to do is rotate the node to fix the issue:
// Create a SCNNode from geometry
let planeNode = SCNNode(geometry: planeGeometry)
planeNode.position = SCNVector3(planeAnchor.center.x, planeAnchor.center.y, planeAnchor.center.z)
planeNode.eulerAngles.x = -.pi / 2 // Add this line to fix the plane orientation issue
Let’s run to double-check if that worked.
Looks great! You might notice that sometimes it appears as if planes are
created in separate spots and then merge as they grow. How come we
didn’t have to delete the SceneKit nodes for those planes? The
ARSCNView
is deleting these SCNNode
s automatically
for us as needed. Since we’re creating these SceneKit plane objects as
child nodes of these plane nodes, they’re deleted when the parent node
is removed. (This is true as long as you don’t have some other strong
reference to either these parent nodes or child nodes.) There’s another
ARSCNDelegate
method that we won’t be implementing in this
article, but feel free to put a print
statement in the
renderer(_:didRemove:for:)
method to see when this event is
triggered in the scene.
Another Option
I’d like to show an additional way to visualize planes in this article.
If we take a look at the documentation for ARPlaneAnchor
,
you’ll notice a property that we haven’t used yet.
/**
Geometry of the plane in the anchor's coordinate space.
*/
@available(iOS 11.3, *)
open var geometry: ARPlaneGeometry { get }
We can use this property in conjunction with
ARSCNPlaneGeometry
to create a more precise visual representation
of the progress ARKit is making towards recognizing planes. The code in the
renderer(_:didAdd:for:)
method is replaced with the following:
func renderer(_ renderer: SCNSceneRenderer, didAdd node: SCNNode, for anchor: ARAnchor) {
// Cast ARAnchor as ARPlaneAnchor
guard let planeAnchor = anchor as? ARPlaneAnchor else { return }
let planeGeometry = ARSCNPlaneGeometry(device: MTLCreateSystemDefaultDevice()!)!
planeGeometry.update(from: planeAnchor.geometry)
// Add material to geometry
let material = SCNMaterial()
material.diffuse.contents = UIColor.blue.withAlphaComponent(0.8)
planeGeometry.materials = [material]
// Create a SCNNode from geometry
let planeNode = SCNNode(geometry: planeGeometry)
// Add the newly created plane node as a child of the node created for the ARAnchor
node.addChildNode(planeNode)
}
Note that we use the plane geometry’s update(from:)
method
to get the physical dimensions from the ARPlaneAnchor
’s
geometry property. Here again we are bridging the gap of the physical data
that ARKit provides and the visual output for the user with SceneKit.
ARSCNPlaneGeometry
handles the orientation and position of the
plane, so we don’t need to make any changes to the plane node with this
visualization option. Now let’s change our
renderer(_:didUpdate:for:)
method to work with
ARSCNPlaneGeometry
.
func renderer(_ renderer: SCNSceneRenderer, didUpdate node: SCNNode, for anchor: ARAnchor) {
// Cast ARAnchor as ARPlaneAnchor, get the child node of the anchor, and cast that node's geometry as an ARSCNPlaneGeometry
guard
let planeAnchor = anchor as? ARPlaneAnchor,
let planeNode = node.childNodes.first,
let planeGeometry = planeNode.geometry as? ARSCNPlaneGeometry
else { return }
planeGeometry.update(from: planeAnchor.geometry)
}
It looks even simpler! Here’s what we now have when we run the app:
Which Option Is Best?
With this method of plane visualization, we get a more detailed look at the
progress of ARKit’s plane detection. So, which plane visualization
option is best? Well, that depends on your app. For example, if you’re
just visualizing planes for debugging purposes then it’s really just
personal preference. However, if you’re visualizing planes in the final
production version of your app then it’s more of a question of UX. While
we used a transparent color for our plane material, it’s also possible
to use images as materials. If that image is rectangular, for example
something that looks like grid lines, then maybe it would make more sense to
use SCNPlane
in that case. I will say though that the additional
feedback you get from using ARSCNPlaneGeometry
about exactly
where the app has detected a surface can be very valuable. This extra
information and prompt the user to focus more on areas of a surface that still
need to be detected. But ultimately the choice is your’s.
Conclusion
I hope this article has shown how easy Apple has made it to get started working with ARKit. Here’s the link to the code of the final project on GitHub if you missed it before. Good luck!