ARKit CocoaPod — Portals

Max Cobb
4 min readNov 18, 2018

I believe the biggest missing piece from the ARKit developer ecosystem right now is reusable code. There are so many amazing pods in use for iOS development in general, but not many specifically for using with ARKit yet.

Here’s the CocoaPod I’ll be explaining in this post, please feel free to report issues, fork, and contribute!

SceneKit-PortalMask

When I started my career in Augmented Reality I was mostly working with marker based AR, using something similar to Vuforia. There was one trend that a lot of clients wanted, and I can see this being a trend going forward in ARKit. The idea is to mask everything outside of the tracking image itself, but allow you to look in through a hole where the image is.

Here’s two simple examples that can be achieved with this Pod, one with just a few lines of code:

Here’s a code sample to create something similar to the example on the left.

The Portal is just fed the physicalSize and it creates the mask around the object.

func renderer(_ renderer: SCNSceneRenderer, didAdd node: SCNNode, for anchor: ARAnchor) {
if let imageAnchor = anchor as? ARImageAnchor {
let nodeRotated = SCNNode()
// nodes on ARImageAnchors need to be rotated -90 degrees
nodeRotated.eulerAngles.x = -.pi / 2
let imageSize = imageAnchor.referenceImage.physicalSize
// the next 2 lines add the portal
let portal = PortalMask(frameSize: imageSize)
nodeRotated.addChildNode(portal)
// the next 5 lines add a cube inside the image area
let width = imageAnchor.referenceImage.physicalSize.width
let boxNode = SCNNode(geometry: SCNBox(width: width, height: width, length: width, chamferRadius: 0))
// push the box behind the marker
boxNode.position.z = -boxNode.width
nodeRotated.addChildNode(boxNode)
node.addChild(nodeRotated)
}
}

This can also be created without a tracking image and placed on any surface, anchor point, or otherwise location in the scene graph.

If you have a tracking image that is instead spherical there are two different ways you can do that. One is using PortalMask.tube(radius:) or PortalMask(radius:). The first of which creates a circular inside hole with a circular mask around it, the latter creates effectively a cuboid with a circular hole into it. The latter would be preferred for most use-cases as it uses much less vertices. There are other parameters on the function documented in-line here.

Here’s a simple example adding a circular portal in mid air with a few objects behind it creating a scene.

let holeNode = PortalMask(radius: 0.5)
holeNode.position = SCNVector3(0, 0, -2)
// Just for a nicer effect, I've added a border around the portal
let holeEdge = SCNNode(geometry:
SCNTube(innerRadius: 0.5, outerRadius: 0.55, height: 0.05)
)
holeEdge.eulerAngles.x = .pi / 2
// Add the nodes that make up your scene inside the portal
let insideChild = BeachScene()
self.sceneView.scene.rootNode.addChildNode(holeNode)
holeNode.addChildNode(holeEdge)
holeNode.addChildNode(insideChild)

The beach scene in this case is just 2 planes, a sphere and a light with one parent node inside the portal area.

And finally, I think the most useful initializer for the PortalMask class lets you create a portal using just a collection of CGPoints. This allows you to create portals of any shape and size, the best example I could think of creates a portal using points collected from surface detection, as in the video below:

I think in this video I set the location to the centre of the ARPlaneAnchor, don’t do that!

The function to initialize this portal is init(path: [CGPoint]), and then updating the geometry as I’ll demonstrate below.

In the code below, planeAnchor is of type ARPlaneAnchor and node is the respective root node for that anchor, taken from renderer(_:didAdd:for:)

let points = planeAnchor.geometry.boundaryVertices
.map { (point) -> CGPoint in
return
CGPoint(x: CGFloat(point.x), y: CGFloat(point.z))
}
let portalNode = PortalMask(path: points)
// The portal will be by default horizontal, to make it vertical
portalNode.eulerAngles.x = -.pi / 2
// add things inside the portal
portalNode.addChildNode(BeachScene())
// add the portal to the anchor node
node.addChildNode(portalNode)

The portal is then added to the wall that’s being scanned, we just need to update that using renderer(_:didUpdate:for:) as follows:

let updatedPoints = planeAnchor.geometry.boundaryVertices.map {
(point) -> CGPoint in
return
CGPoint(x: CGFloat(point.x), y: CGFloat(point.z))
}
portalNode.updateGeometry(with: updatedPoints)

Before doing this you just need to find the node of class PortalMask that is in the ARAnchor node.

You can do this by simply node.childNodes.first as? PortalMask, or looping through the children if there’s more than one, or assigning the anchor’s root node a name and looking it up in a hash map.

That covers most of what I’ve done so far with the repository, I’m hoping that other people will find it useful and I’ll continue to contribute to it when there’s any relevant updates to ARKit or any other ideas I’ve had. Please check out my GitHub for this and some other libraries I’m working on.

--

--

Max Cobb

spatial computing at apple. won’t be posting new content for now.