Explore the different types of constraints and how to use them in SwiftUI-powered apps
Most would agree that augmented reality has yet to find its niche, with the argument most often cited: the absence of a killer app. A truism, though I suspect another reason is that augmented reality apps are hard to code. This is a challenge because the math behind working out the positions and angles of interacting nodes is at the core of computer science. Plus, the cryptography of graphics.
Thankfully some Apple engineers have already worked and encoded in the SceneKit framework, with some of their best work shown at the final SceneKit presentation at WWDC2017. Join me in this piece to take a look at the mind-boggling math behind the implications that he created in constructs called Constraints.
Before I dive in, though, I want to give credit to the draftsman/artist Bergman, who created the out-of-this-world collection of tanks that you can find Here, Tanks that I will use throughout this article to represent those constraints. I’ll start by explaining the tank construction in the animated GIF that is the title of this article.
Within the scene, I use a Bergmann tank model that comes in six pieces, of which I used five. The principal chassis consists of the track, turret, main gun, hatch and machine gun and a sixth piece which is optional [in my book]
I tried to fit the pieces together as best I could. in addition 5
SCNReferenceNodes, I created two base nodes. I connected the chassis to the first base node and everything else to the second. I did this because I needed the turret + guns to run separate from the chassis. I wanted to change the pivot points on several model components to make sure they pivot on the correct axis. I found the required pivot code on SO Here,
minimum = SIMD3
maximum = SIMD3
translation = (maximum + minimum) * 0.5
T1100m109a5turret19ReferenceNode?.pivot = SCNMatrix4MakeTranslation(translation.x, translation.y, translation.z)
I also needed to add a directional light to the scene as the model didn’t come out without it. Lastly, I used the Subsection Code Pattern within the Combine Framework to add some tweaks to the SwiftUI interface for the turret, main gun, and machine gun, just for fun.
turretSub = turretPass.debounce(for: .seconds(0.01), scheduler: RunLoop.main)
.sink(receiveValue: [self] direct in
SCNTransaction.animationDuration = 2.0
turretNode.simdEulerAngles = SIMD3(x: 0, y: Float(30).degrees2radians(), z: 0)
turretNode.simdEulerAngles = SIMD3(x: 0, y: Float(0).degrees2radians(), z: 0)
And — I should mention this detail for converting degrees to radians and radians to degrees — for good measure.
internal extension Float
func radians2degrees() -> Float
func degrees2radians() -> Float
OK – now, on to the main course – Sanctions – who have nothing to do with them painful Things of the same name in UIKit, although I must admit that their use is not easy.
Here’s a slide out from WWDC2017 of the said constraints; Gray boxes were already available in prior versions of SceneKit, and green boxes were new. Apple talked about most of this in terms of the camera. You’re adding them to the SCNNode though, so there’s no reason to use them with a camera; In principle, you can use them on any node.
You can add constraints to a node at any point in your code, although setup is the best place to do so. Rules that you can turn up or down, on or off, using a variable influencing factor, One means full-on, zero means off, values in the middle of the half house. As explained in WWDC2017, you want to use constraints in pairs or more.
Barriers in this context are somewhat like self-driving nodes. Self-driving, as they will manage angles or positions of nodes in your app. Controls that will take your settings as a cue and then decide where they should be. Weak signal that they will ignore if they feel they are out of bounds.
This is a good and a bad thing; A good thing because it takes the mind off the math of the problem, but a bad thing because it can become a real challenge to see what’s going on.
bon, well – now, although not new
SCNLookAtConstraint Probably the first one you’ll encounter, probably from the camera’s point of view. The purpose of this is to keep the nodes you linked within the viewport.
let lookAtConstraint = SCNLookAtConstraint(target: tankNode)
lookAtConstraint.influenceFactor = 0.5
lookAtConstraint.isGimbalLockEnabled = true
cameraNode.constraints = [lookAtConstraint]
Code that looks like this in our World of Tanks. Note that the tank is moving here, and the camera is stationary.
As you can see, the tank vanishes into infinity and almost beyond.
Now obviously, letting your game player go to infinity and beyond isn’t that useful. To fix this, you may be tempted to add a distance constraint; I was. A good solution – taking into account whatever min/max distance you’ve covered.
let distanceConstraint = SCNDistanceConstraint(target: tankNode)
distanceConstraint.influenceFactor = 1.0
distanceConstraint.minimumDistance = 6.0
distanceConstraint.maximumDistance = 8.0
cameraNode.constraints = [lookAtConstraint, distanceConstraint]distanceConstraint.influenceFactor = 1.0distanceConstraint.maximumDistance = 8.
Code that looks like this in our World of Tanks. Note that the tank and camera are moving here, however, in my code, I’m only moving the tank; The camera is in auto-driving mode.
Although it’s probably not ideal in this case, since the camera is so close to the tank, you don’t see the signals until they are almost upon you.
Assuming that’s not what you wanted, a Replicator barrier would be an option. Using this, with the right parameters, you can get your camera to follow the player, this time taking care to maintain the exact position and angle before moving the node. You can see here I am configuring the constraint with the condition of
let replicatorConstraint = SCNReplicatorConstraint(target: tankNode)
replicatorConstraint.positionOffset = SCNVector3(cameraNode.position.x,cameraNode.position.y,cameraNode.position.z)
replicatorConstraint.replicatesOrientation = false
cameraNode.constraints = [replicatorConstraint, lookAtConstraint]
Code that looks like this in our World of Tanks. Note that I moved the camera in and out as you see it. The movements that are noticed when I move the tank.
This restriction is even more subtle than the last one I showed you. This introduces a lag in camera movement, though I wonder if that does the video justice.
let accelerationConstraint = SCNAccelerationConstraint()
accelerationConstraint.maximumLinearAcceleration = 0.02
cameraNode.constraints = [replicatorConstraint, lookAtConstraint, accelerationConstraint]
Code that looks like this in our World of Tanks. It’s not that easy to see; But it looks different. Here the acceleration constraint is slowing down the camera response when following a tank.
But wait — because even in auto-driving mode, you may still want to add some limits to the mix. Boundaries that are not intended as indications, but are fixed red lines. In the previous example, I allowed the player to move the camera at any angle in all directions, and this works very well because if they move the camera too low, it goes below the ground . To prevent this from happening, I can set a transform constraint to limit the camera’s movement to a positive y-axis.
let transformConstraint = SCNTransformConstraint.positionConstraint(inWorldSpace: false, with: (node, position) -> SCNVector3 in
if node.position.y < 0 node.position.y = 0
I didn’t feel the need to include a GIF of this because there’s no point in showing how you can stop something from showing up.
SCN Billboard Constraints
Finally, I want to mention this constraint, which is an exception to the rule because it makes no sense to add it to cameras, only to nodes. It works by changing the angle of the node so that it always faces the camera.
let boardNodeConstraint = SCNBillboardConstraint()
boardNode.constraints = [boardNodeConstraint]
Code that looks like this in our World of Tanks. Note that I start by moving the camera, and then I move the tank. The colored boards, meanwhile, follow the camera, always remaining readable.
All this brings me to the end of this paper. it’s true i didn’t touch
SCNAvoidOccluderConstraintsWhich needs an article in its own right, besides this article was getting too long.
I hope you enjoy reading this as much as I enjoyed writing it. Please show your appreciation.