Yes, as the title suggests, we’re going to have another of those posts with math (and fun) in it. I was thinking the other day, what can we do while commuting for an hour or so, without internet and possibly without a laptop as well, just carrying an iPad with us. Luckily, the iPad now has the awesome Swift Playgrounds app.

Let’s start with a new playground that runs a basic compute kernel. Since the current version of the Swift Playgrounds app does not yet let us edit the Auxiliary Source Files, where all our Swift and Metal files usually reside, we will have to write our code in the main playground page, but it is not that complicated. All we have to do is modify our MetalView initializer and let it take in an extra argument – our shader/kernel code. Then we start building our code by adding more lines to this long string.

Let’s start with a light blue sky background color:

let shader =
"#include <metal_stdlib>\n" +
"using namespace metal;" +
"kernel void k(texture2d<float,access::write> o[[texture(0)]]," +
"              uint2 gid[[thread_position_in_grid]]) {" +
"   float3 color = float3(0.5, 0.8, 1.0);" +
"   o.write(float4(color, 1.0), gid);" +
"}"

If you run the playground now, the output image should look like this:

alt text

Next, let’s draw a gradient. we divide the current pixel coordinates to the screen dimensions and we get UV – a pair of floats between (0-1). we then multiply the fixed color with Y – the vertical component of UV which gives us the gradient:

"   int width = o.get_width();" +
"   int height = o.get_height();" +
"   float2 uv = float2(gid) / float2(width, height);" +
"   color *= uv.y;" +

The output image should look like this:

alt text

Let’s work on a nicer background next. A smooth gradient would look like a great sunset. We can use mix to blend colors. We tell the function to blend vertically, and take the complement of Y to switch the colors:

"   float3 color = mix(float3(1.0, 0.6, 0.1), float3(0.5, 0.8, 1.0), sqrt(1 - uv.y));" +

The output image should look like this:

alt text

From here we could go to drawing a black hole. We would achieve that by using a distance function (length) to draw black in the middle of the screen (0.5, 0.5) and add more and more background color outside of it, until we reach the maximum value in the screen corners. Replace the last line with:

"   float2 q = uv - float2(0.5);" +
"   color *= length(q);" +

The output image should look like this:

alt text

Next we use smootstep to draw a round shape that is black inside, blue outside and a blended color between r and (r + 0.01). Replace the last line with:

"   float r = 0.2;" +
"   color *= smoothstep(r, r + 0.01, length(q));" +

The output image should look like this:

alt text

If we’re not satisfied with a circular perimeter, we could make it bumpy by using math functions such as cos and atan2. We generate here 9 spikes (frequency) with a spike length (amplitude) of 0.1:

"   float r = 0.2 + 0.1 * cos(atan2(q.x, q.y) * 9.0);" +

The output image should look like this:

alt text

Adding the X coordinate to the cosine phase introduces a spike bend-like effect:

"   float r = 0.2 + 0.1 * cos(atan2(q.x, q.y) * 9.0 + 20.0 * q.x);" +

The output image should look like this:

alt text

You can rotate them by adding a small number such as 1.0 to the cosine value:

"   float r = 0.2 + 0.1 * cos(atan2(q.x, q.y) * 9.0 + 20.0 * q.x + 1.0);" +

The output image should look like this:

alt text

If you think this is starting to look like a palm tree canopy, I see it too! We can draw its trunk using abs which gives us horizontal/vertical distances instead of euclidian distances (to a given point) like length did, so let’s take the X distance and add these lines (we are reusing both r and color) after the existing ones:

"   r = 0.015;" +
"   color *= smoothstep(r, r + 0.002, abs(q.x));" +

The output image should look like this:

alt text

We can remove the unneeded part of the trunk by using another smoothstep on the Y coordinate:

"   color *= 1.0 - (1.0 - smoothstep(r, r + 0.002, abs(q.x))) * smoothstep(0.0, 0.1, q.y);" +

The output image should look like this:

alt text

Since both the canopy and trunk are using q, modifying its value will move both:

"   float2 q = uv - float2(0.67, 0.29);" +

The output image should look like this:

alt text

By introducing a sin function we can bend the trunk. A too small frequency does not bend it enough while a too high frequency bends it too much so 2.0 seems right. An amplitude of 0.25 also moves the base of the trunk towards the edge of the screen so it looks right (as an aside, changing the sign from + to will shift the base to the other side):

"   color *= 1.0 - (1.0 - smoothstep(r, r + 0.002, abs(q.x - 0.25 * sin(2.0 * q.y)))) * smoothstep(0.0, 0.1, q.y);" +

The output image should look like this:

alt text

The trunk, however, is too smooth. To add irregularities to its surface we use cos again. A high frequency and low amplitude seem to be what we need to make it look right:

"   r = 0.015 + 0.002 * cos (120.0 * q.y);" +

The output image should look like this:

alt text

Also, trunks are usually shaping the ground a little around their base, so an exp function is what we need here because it grows slowly in the beginning and then it soars to the skies. We use an attenuation factor of -50.0:

"   r = 0.015 + 0.002 * cos (120.0 * q.y) + exp(-50.0 * (1.0 - uv.y));" +

The output image should look like this:

alt text

We can increase the presence of the second color by using sqrt which gives us bigger numbers (when used on sub-unitary numbers) to work with. The sunset is about to end soon:

"   float3 color = mix(float3(1.0, 0.6, 0.1), float3(0.5, 0.8, 1.0), sqrt(1 - uv.y));" +

The final image on the iPad should look like this:

alt text

In conclusion, we saw how to use sqrt to shape transitions, then cos to create variations of ups and downs in shapes, then exp that allows us to create curves, then smoothstep for thresholding, then abs for symmetry and mix for blending. Still commuting? Why don’t you take a look at how this nice clover was created:

alt text

I want to say thanks to Inigo Quilez again, for keep inspiring me to write more and more about drawing with math. All the math in this tutorial belongs to him. The source code is posted on Github as usual.

Until next time!