ARKit + SceneKit Intro to Shaders

8 min read Original article ↗

I talked before about how to create your own custom geometries in SceneKit and as a result some things you can do with them such as skew or animate.

See Part 1 of 2 here:

Towards the end of the second in that series I showed how you could animate the flag with what looks like just a string, which is one way of adding a shader to a geometry or material in SceneKit.

In many cases, a shader such as the one shown at the bottom of my previous post is a better choice for animating a geometry than the other examples I have shown. The main reason of this is that these functions are taken in by SceneKit, compiled into Metal and run on your device’s GPU, which is much more powerful than the CPU, where it would otherwise be run. This is, mostly, because a GPU is built specifically for processing graphics (hence the name, Graphics Processing Unit), so we prefer to use that as often as possible!

The method of applying shaders I want to mention in this article will mostly be by using the shaderModifiers property of SCNGeometry objects. To get started take a look here:

For a simple example, take a look at the following small snippet:

if (_geometry.position.y > 0) {
_geometry.position.x += 0.5;
}

Suppose we apply this to a geometry of type SCNBox, with dimensions 1x1x1.

_geometry represents the vertex, just as described in my previous posts, and its position is, of course, its position in 3D space, [x, y, z]. In this snippet we’re taking any vertex whose position is above the centre of the cube, and pushing it 0.5m to the side in the positive x direction.

Press enter or click to view image in full size

Here’s two red cubes before the shader is applied

Press enter or click to view image in full size

And those same red cubes with the top 4 points pushed in the positive x axis

We can quickly make this more interesting without even giving it much thought, as such:

if (_geometry.position.y > 0) {
_geometry.position.x += 0.5 * sin(u_time);
}

u_time is the time in seconds since SceneKit began rendering this shader. So this will animate those same 4 points across from +0.5 in the cube’s local x axis to -0.5 in a smooth fashion following a sine easing.

Press enter or click to view image in full size

And the last example with the cube I’ll demonstrate is changing the top four points from moving in a straight line to move in a circle using a unit circle equation. Click here to see a quick graph is Desmos in case you’re not sure how that’s put together.

if (_geometry.position.y > 0.0) {
_geometry.position.xz += vec2(
0.5 * sin(3.0 * u_time),
0.5 * cos(3.0 * u_time)
) * (u_time < 3.0 ? u_time / 3.0 : 1.0);
}

The language used for making shaders here has some nice features, like in the example above you can directly manipulate just the x and y axis of the position by adding a vec2 straight to it. A vec2 can also be multiplied by a scalar value. In the snippet above we’ve got the (sin, cos) circle equation multiplied by a value ranging from 0.0–1.0 in the first 3 seconds, this just makes the animation not be so abrupt at the beginning.

Press enter or click to view image in full size

Ok, I’m done looking at cubes wiggle. On to spheres!

For starters, let’s try the same shader on a SCNSphere, where only the top half of vertices are moving in a circle again:

Press enter or click to view image in full size

Not the prettiest sphere, but hopefully you see what’s going on.

We’re going to go through 2 more geometry shaders with spheres and 1 surface shader.

If we go for a sort of wiggly look, ripples moving up the sphere. For this we will only need the vertices to move in the horizontal (x, z) plane, since it’s a sphere we can make that a multiple of their initial position in the plane by a constant (5% sounds like enough to be visible), and as the ripple will be moving up the y axis we can use that for our sine function, along with time.

This is what I’m thinking so far:

_geometry.position.xz += _geometry.position.xz
* sin(30.0 * _geometry.position.y - 3.0 * u_time) * 0.05
* (u_time < 3.0 ? u_time / 3.0 : 1.0);

Press enter or click to view image in full size

This looks good enough, it’s what I had in my mind anyway!

Now for the last section, we’re going to turn a yellow sphere into something that resembles the sun!

I went away and messed around with Desmos a little bit and came up with this:

Press enter or click to view image in full size

Ok so this might take a minute to understand, especially if you’re not familiar with radial graphs. but knowing that r < 1 would make a filled in circle with a dotted line, everything else in the equation below is just added to that, and θ is the anti-clockwise angle from the positive x axis. One of the sine functions is moving with time, the other is fixed. I mostly just messed around Desmos and found something that works, this is what I ended up with.

Press enter or click to view image in full size

Theta can be calculated with the help of the atan function, just like this: float theta = atan(_geometry.position.x, _geometry.position.y);

Technically this is rotated 90 degrees because I put the x value first, but for all intents and purposes this is fine, I just like putting x before y.

I’m just going to try put that straight into the shader and see what happens:

float theta = atan(_geometry.position.x, _geometry.position.y);
float pi = 3.14159;
_geometry.position.xy += _geometry.position.xy
* sin(theta1 * pi - u_time * 2.0) * sin(6.0 * theta1) / 8;

Press enter or click to view image in full size

Looks interesting but not like the sun, I’m going to get rid of the light and change the background to blue. Because the sun can’t be effected by light, as it is a light source itself.

Press enter or click to view image in full size

Bit better, but now I want to make the edges blend to the background a little bit more, time for a surface shader.

When trying to make this blend out the edges, we need to see what’s similar with all the edges. I’d say the similarity is that the normal of the edges will be looking away from the view (camera view). The best way to find that is to take the dot product of those 2 vectors, it’ll give us a value from 1 to -1. If the value is close to 1, then the normal is facing the view (or camera), 0 means it is perpendicular to the camera’s view, and -1 would mean it’s the other side of the object.

// We want it to be transparent on the edges, so  we need these 2
#pragma transparent
#pragma body
float dotProduct = dot(_surface.view, _surface.normal);// I'm clamping it so all negative values are just 0
dotProduct = dotProduct < 0.0 ? 0.0 : dotProduct;
_surface.diffuse.rgb = vec3(1.0, 1.0, 0.0);float a = dotProduct;
_surface.diffuse = vec4(_surface.diffuse.rgb * a, a);

Press enter or click to view image in full size

Looks okay, but actually it would look better if the solid colour in the centre was a little bigger, easy fix, just make it so that it doesn’t start changing the colour until the dot product is small enough. Below is the final snippet for this shader, I’ve highlighted the lines that changed.

#pragma transparent
#pragma body
float dotProductEdge = 0.5;float dotProduct = dot(_surface.view, _surface.normal);
dotProduct = dotProduct < 0.0 ? 0.0 : dotProduct;
_surface.diffuse.rgb = vec3(1.0, 1.0, 0.0);if (dotProduct <= dotProductEdge) {
float a = dotProduct / dotProductEdge;
_surface.diffuse = vec4(_surface.diffuse.rgb * a, a);
}

Press enter or click to view image in full size

Here’s that sun next to the original graph we had

Since I’ve branded this as an “ARKit” shaders intro, I’ve created a repository showing how to use all of the above examples in ARKit, and I’ll show a couple of these examples in GIFs below.

use function cubeRotateSkew() in the repository for this
use function sphereRipple() in the repository for this object
use function sphereSun() in the repository for this one

Thanks for reading, I appreciate this post is assuming quite a few bits of knowledge with the equations, I’d recommend opening up the Desmos links and playing with the equations in there to try get a better understanding, especially of the radial graphs.

As ususal, find me on Twitter or LinkedIn if anything’s unclear or have any suggestions for this post or thoughts for future posts; also check out my GitHub for other public projects I’m working on.