In Part 1 of this series, we went through a workflow where we processed a 3D model, created an AR project in Xcode, started an AR session and put our model in our augmented scene.
In this post, we’ll begin putting our model in action, using a variety of SceneKit methods, and begin interacting with the objects in our world.
The project for this post can be found at https://github.com/AbovegroundDan/ARTutorial_Part2
SceneKit provides a set of actions that can be applied to a node. These actions can be used to animate movement, rotation, scaling and other node properties. They can be grouped to run at the same time, sequenced to run one after another, and repeated or reversed. The full list can be found at https://developer.apple.com/documentation/scenekit/scnaction
We’re going to continue modifying the current project, and start adding some actions to our object. Let’s begin by adding a rotation to our sphere.
After the addSphere method in our HoverScene, add the following method:
In this code, we create an action that describes a 180 degree rotation around the Y axis. This rotation should take 5 seconds to complete. Then we take this action and add it to the node passed in. Now, we need to call this method from our addSphere method. At the end of the method, after the line where we add the child node, add the following line:
Now, if we run this code, we will see our sphere rotate to face the other way and then stop. What we want to do now is make this animation repeat, so it keeps rotating.
Let’s change our addAnimation code to the following:
Here, we added a repeat forever action, which takes in the rotation action as the input. Then instead of the rotation action, we add the repeatForever action to our node. Run the code again, and we will see that our sphere now continuously rotates.
Let’s add a little bit of pizzazz to the sphere. Let’s also make it hover up and down a bit. We’re going to add a hover up action, a hover down action, sequence those two actions, and then group them with our existing rotate action. This is what the addAnimation method should look like:
Now we have a rotating, hovering sphere that we can place anywhere in our world.
HitTests and Abstractions
Let’s add some interactivity to the scene. Let’s set a goal to start the scene out by having the user place the spheres in the world, and then tap to activate them. Since we’re getting a bit more complex with our code, its time to start abstracting the way we deal with our scene objects. Create a new group in our project called Objects. In this folder, we’ll create a new Swift file called SceneObject.swift.
We’re going to create a base class, SceneObject, that derives from SCNNode.
We want to abstract the loading of the object from the rest of the code, so lets create an init() method that takes in a filename. In this initializer we’ll move the code that we have for loading from file.
Now we can create a Sphere class that derives from SceneObject:
We now have a Sphere object class that can load itself. Since we’re going to animate the spheres when we tap on them, let’s remove the addAnimation call from the addSphere method in our HoverScene. Also, since we’ve now moved all the loading code into the Sphere class, we can just initialize Sphere and add it to the scene’s root node directly. Our greatly simplified method now looks like this:
Now, we’re going to take a look at how we can do a hit test. We already have a tap gesture recognizer in our view controller, so we can hook into that, but how will we know whether our taps are hitting a Sphere, another object, or nothing at all?
Luckily, our ARSCNView can help us with that. It has the following method:
We can feed it a location in the view, and it will give us back an array of nodes that are under that point, no matter what the z-value, or depth is.
Since we want to only grab Sphere objects, lets create a quick filter that checks to see if each node returned in the hitTest is a Sphere. To do that we need to grab the topmost parent node for each node we want to check. Let’s go back to our Node+Extensions.swift file and add the following method:
Since all the objects in our scene are a child of the scene’s root node, we want to stop when we reach that node and not check any further. Since its a recursive method, we stop and return when we find that the root node is our node’s parent.
With that behind us, let’s go back to our ViewController and modify our tap gesture recognizer delegate. We want to continue to add new spheres if we tap on an empty space, but if we tap on an existing sphere, we want to kick off its animation.
When we get a tap, we want to get the point in our scene view, pass that to the scene view’s hitTest method, and see what we get back. Since we only want to deal with one object at a time, we grab the first one (if there are hits), then use our topmost() extension to grab the topmost parent, and check if its a sphere. If it is, then we add our animation to it. If we didn’t get any hits from our test, then we do as before, adding a new Sphere in front of the camera.
Go ahead and run the app again. We can tap to add a sphere, and then tap any sphere to start it moving. However, we have a bug that we introduced. We can continue tapping on a sphere and the animations will keep getting added to the object each time. You can test it and see the sphere spinning faster and moving up and down faster each time you tap it.
Since the animation is specific to the sphere, let’s move the addAnimation code into the Sphere itself, and rename it to just animate(). Instead of node.addAnimation, we can just call addAnimation. We’ll also add a flag to the Sphere class, which we’ll check before adding the animation, and set it to true the first time its added:
All that’s left to do is change the code in our gesture callback to run this new call on the sphere itself.
We’re all set with this part now. We have a sphere that we can place in the world, added some interaction so that we can kick off the animation, and cleaned up the code a bit.
The way that we place a sphere in the world is pretty abrupt. We tap, and its suddenly there. Let’s add a little pizzazz to this function and animate the sphere when we place it.
In our addSphere method in HoverScene, let’s add a scale effect. When we add the sphere, we will animate its scale, and instead of using a standard linear scale, we will put in a bounce, or pop in, effect.
Let’s change our addSphere method to the following, and add in the easeOutElastic timing function, which will provide us with that bounce:
Now when we tap to place a sphere, we get a pretty cool animated effect.
Extra credit, part deux
We’ve been doing a lot of stuff with SceneKit, but have only grazed the surface of some of the things ARKit can do. As a quick teaser before we get to more ARKit functionality, let’s add a bit of fun to the scene, by making the sphere “look at” the camera. We’ve already got hooks into the renderer’s updateAtTime method, and we have a reference to the camera there as well. So let’s begin by adding a method to the Sphere class, to have it look towards a particular direction. The sphere’s “eye” is already facing negative Z, which is the object’s forward direction. What we’ll do is create a method that takes a vector marking a point in space that our “eye” will face.
In this code, we look at the distance between the sphere and the target (camera) position. If it is less than some amount, we’ll animate the eye to face the target. If the eye was facing the camera and the user moves further away than this set distance, then we’ll go into our “patrol” animation. One thing to note in this code is that since there is no handy SCNAction for applying a “LookAt” animation, we wrap our look(at:) call in an SCNTransaction, which allows us to animate the movement. Dedicated 3D game engines such as Unity or Unreal have convenience functions for these things, but SceneKit is not yet at that level.
You may notice there is a distance call on the targetPos SCNVector3 that’s passed in, but that method does not exist for SCNVector3. What we will do is add a new extension for this distance call.
I put that code into a new UtilityExtensions.swift file, but feel free to put that wherever you like.
Next, we’ll need to change our updateAtTime method to remove the flag check and call a method in our scene controller every frame. Our scene controller will be in charge of dispatching the message to all the Sphere objects in our scene.
In our HoverScene, we’ll create the makeUpdateCameraPos method, which will filter on Sphere objects only, and call the patrol method.
Let’s also change our placement method to put the spheres a bit further away. Let’s make the didTapScreen method place our sphere 5 meters away instead of 1:
In our Sphere class, let’s make our threshold to trigger the eye gaze to 4.85 meters:
Let’s also change our Sphere animation so that it looks around a bit, and not hover.
The Float.random is another extension which is simply:
Give that a run and place some spheres. They should animate on their own now, without needing to tap them. Walk closer to each one and you should see them turn their eye to you. Walk further away and they should go back to patrolling their area. Now we have a scene straight out of a dystopian future, with hovering eyes watching our every move.
Stay tuned for more ARKit fun!