ARKit 911 — How to create a custom ARWorldAnchor
Seasoned Augmented Reality developers know that anchors are a crucial part of any AR app. Without anchors 3D models may drift and this unpleasant issue will dramatically impact your app’s realism and user experience. Thus you always need to tether your models with anchors. ARKit has 10 specific types of anchors that help you build a robust AR experience. Those anchor types are:
• ARAppClipCodeAnchor
• ARBodyAnchor
• AREnvironmentProbeAnchor
• ARFaceAnchor
• ARGeoAnchor
• ARImageAnchor
• ARMeshAnchor
• ARObjectAnchor
• ARParticipantAnchor
• ARPlaneAnchor
All these anchor types inherit from ARAnchor parent class. And, as you can see, in ARKit there’s still no out-of-the-box anchor named “World” like RealityKit’s AnchorEntity(world: SIMD3<Float>). Even a notorious Xcode’s template called Augmented Reality App doesn’t give a developer any hint on how to tether a SceneKit’s fighter (ship.scn) using world anchor.
A code for Augmented Reality App template can be as simple as that:
class ViewController: UIViewController { @IBOutlet var sceneView: ARSCNView! override func viewDidLoad() {
super.viewDidLoad() sceneView.scene = SCNScene(named: "art.scnassets/ship.scn")!
sceneView.session.run(ARWorldTrackingConfiguration())
}
}
As I said, in ship.scn scene there’s no anchor for ship model. As well as in code. Nevertheless, we can easily create a new anchor type programmatically to tether any desired model in a scene but at first we should dive into transform matrices topic.
ARAnchor class has init(transform: simd_float4x4) for positioning, rotating and scaling anchors in world space. So if we want our model to appear in front of us, 4 meters away, we have to change a translate Z value. It is located in the last column of matrix (column with index 3). Three upper diagonal `1` are default XYZ scale values.
If you need a more detailed info about matrices you can find it here.
In ARKit, SceneKit and RealityKit each column of the Identity 4x4 matrix is represented by SIMD4<Float> type. With this sacred knowledge we can create our ARWorldAnchor custom class.
import UIKit
import ARKit
import SceneKitclass ARWorldAnchor: ARAnchor { init(column0: SIMD4<Float> = [1, 0, 0, 0],
column1: SIMD4<Float> = [0, 1, 0, 0],
column2: SIMD4<Float> = [0, 0, 1, 0],
column3: SIMD4<Float> = [0, 0, 0, 1]) { let transform = simd_float4x4(columns: (column0,
column1,
column2,
column3)) let worldAnchor = ARAnchor(name: "World Anchor",
transform: transform) super.init(anchor: worldAnchor)
} required init(anchor: ARAnchor) {
super.init(anchor: anchor)
} required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
fatalError("Hasn't been implemented yet...")
}
}
In class initializer we’ve put default values in each matrix column, so translation and rotation for all axis are zeros, and scale is ones (100%). The name of our anchor is “World Anchor” respectively.
Now we are able to use the new ARWorldAnchor type in our ViewController’s life cycle. And please, don’t forget that we have to add worldAnchor object to the anchors’ collection in the current session via add(anchor:) method.
class ViewController: UIViewController: ARSCNViewDelegate { @IBOutlet weak var sceneView: ARSCNView! override func viewDidLoad() {
super.viewDidLoad() sceneView.delegate = self let config = ARWorldTrackingConfiguration()
sceneView.session.run(config, options: []) let worldAnchor = ARWorldAnchor(column3: [0, 0,-4, 1])
sceneView.session.add(anchor: worldAnchor)
}
}
In ViewController we’ve implemented ARSCNViewDelegate protocol that provides us with a renderer(_:didAdd:for:) optional instance method. A delegate instance property mediates between running ARSession and 3D content in ARSCNView.
weak var delegate: ARSCNViewDelegate? { get set }
A renderer(_:didAdd:for:) method has three arguments. One of these, a local argument node, is an object of type SCNNode.
optional func renderer(_ renderer: SCNSceneRenderer,
didAdd node: SCNNode,
for anchor: ARAnchor)
We do not need to add this node to a scene because it’s already a child of a root scene node. And this node is automatically tethered by our brand-new ARWorldAnchor because anchor is already inside anchors’ array. To filter out anchors of a needed type just use if let or guard let optional binding statements.
Now, let’s create an extension and use a delegate’s method there.
extension ViewController { func renderer(_ renderer: SCNSceneRenderer,
didAdd node: SCNNode,
for anchor: ARAnchor) { guard let worldAnchor = anchor as? ARWorldAnchor
else { return } print(worldAnchor.isKind(of: ARWorldAnchor.self))
// it prints "true" let earthNode = SCNNode()
earthNode.geometry = SCNSphere(radius: 1.0) let path = "art.scnassets/earthAlbedo.jpg"
earthNode.geometry?.firstMaterial?.diffuse.contents = UIImage(named: path) let action = SCNAction.repeatForever(SCNAction.rotate(by: CGFloat.pi, around: SCNVector3(0, 1, 0), duration: 3))
earthNode.runAction(action) node.addChildNode(earthNode)
}
}
We’ve built a planet with a texture and added it to node object as a child. Also we’ve created a simple transform animation that allows our planet to be rotated forever.
Run your app and guess what you see.
Voila!
Now we should check whether anchors’ array contains our ARWorldAnchor or not. In ARKit each ARFrame keeps info about anchors, so we need to ask currentFrame property about it.
print(sceneView.session.currentFrame!.anchors.first!.name! as Any)
print(sceneView.session.currentFrame!.anchors.first!.transform)
When we print a name and transform instance properties we’ll get this:
That’s all for now.
If this post is useful for you, please press the Clap button and hold it.
¡Hasta la vista!