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:

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:

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:

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:

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:

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:

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:

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:

If you think this is starting to look like a palm tree canopy, I see it too! We can draw its trunk using abswhich gives us horizontal/vertical distances instead of euclidian distances (to a given point) like lengthdid, 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:

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:

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:

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:

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:

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:

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:

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 absfor symmetry and mix for blending. Still commuting? Why don’t you take a look at how this nice clover was created:

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!