Skip to content

Using MetalKit part 11

Let’s continue our journey into the wonderful world of shaders using the Metal Shading Language (MSL) by picking up where we left off in Part 10. Using the same playground we worked on last time, we will next try to get close to making art using MSL math functions such as sincospowabsfmodclampmixstep and smoothstep.

First, let’s look at our “sun eclipse” code from last time. Strangely enough, we start from the end of the functions list above because smoothstep is the function we need to fix an issue we had last time and we did not pay attention to it – our output image has jaggies (is aliased) as you can see below if we zoom in enough to make it visible:

alt text

The smoothstep function depends on a left edge being smaller than a right edge. The function takes a real number x as input and outputs 0 if x is less than or equal to the left edge, 1 if x is greater than or equal to the right edge, and smoothly interpolates between 0 and 1 otherwise. The difference between the step and the smoothstep function is that step makes a sudden jump from 0 to 1 at the edge. The smoothstep function implements cubic Hermite interpolation after doing a clamp. An improved version, named smootherstep, has zero 1st and 2nd order derivatives at x=0 and x=1:

smoothstep(X) = 3X^2 - 2X^3
smootherstep(X) = 6X^5 - 15X^4 + 10X^3

Let’s implement the smootherstep() function:

float smootherstep(float e1, float e2, float x)
    x = clamp((x - e1) / (e2 - e1), 0.0, 1.0);
    return x * x * x * (x * (x * 6 - 15) + 10);

The clamp() function moves the point to the nearest available value, given a min and max value. The input takes the value of min if less than it, the value of max if greater than it, and keeps its value if in between. Our compute kernel should now look like this:

int width = output.get_width();
int height = output.get_height();
float2 uv = float2(gid) / float2(width, height);
uv = uv * 2.0 - 1.0;
float distance = distToCircle(uv, float2(0), 0.5);
float xMax = width/height;
float4 sun = float4(1, 0.7, 0, 1) * (1 - distance);
float4 planet = float4(0);
float radius = 0.5;
float m = smootherstep(radius - 0.005, radius + 0.005, length(uv - float2(xMax-1, 0)));
float4 pixel = mix(planet, sun, m);
output.write(pixel, gid);

The last function we look at before moving on, is mix. The mix() function performs a linear interpolation between x and y using a to weight between them. The return value is computed as x * (1 - w) + y * w. In this case, the planet color and sun color are interpolated using smootherstep as weight. If you execute the playground, the output image now has anti-aliasing and the jaggies are all gone:

alt text

The next functions we look at are abs and fmod. The abs() function simply returns the absolute value, or the distance of a number from 0. In other words, any value loses its sign and always returns a non-negative value. The fmod() function returns the remainder fractional part of a float (the equivalent of the modulo operator % for integers). Let’s apply these two functions to some values and see what we can get:

float3 color = float3(0.7);
if(fmod(uv.x, 0.1) < 0.005 || fmod(uv.y, 0.1) < 0.005) color = float3(0,0,1);
float2 uv_ext = uv * 2.0 - 1.0;
if(abs(uv_ext.x) < 0.02 || abs(uv_ext.y) < 0.02) color = float3(1, 0, 0);
if(abs(uv_ext.x - uv_ext.y) < 0.02 || abs(uv_ext.x + uv_ext.y) < 0.02) color = float3(0, 1, 0);
output.write(float4(color, 1), gid);

The output image should look like this:

alt text

First, we drew a grid of blue lines spaced out at 0.1 between them and with a thickness of 0.005. Next, we normalized the screen coordinates so we can work with the [-1, 1] interval, and then drew the Xand Y axes in red with a thickness of 0.02. Finally, we drew the two diagonals in green with the same thickness, keeping in mind that x – y gives us the decreasing slope (diagonal) while x + y gives us the increasing one.

Finally, let’s use sin()cos()fract()dot() and pow() together with other functions we already discussed:

float2 cc = 1.1 * float2(0.5 * cos(0.1) - 0.25 * cos(0.2), 0.5 * sin(0.1) - 0.25 * sin(0.2) );
float4 dmin = float4(1000.0);
float2 z = (-1.0 + 2.0*uv) * float2(1.7, 1.0);
for(int i=0; i<64; i++) {
    z = cc + float2(z.x * z.x - z.y * z.y, 2.0 * z.x * z.y);
    dmin=min(dmin, float4(abs(0.0 + z.y + 0.5 * sin(z.x)), abs(1.0 + z.x + 0.5 * sin(z.y)), dot(z, z), length(fract(z) - 0.5)));
float3 color = float3(dmin.w);
color = mix(color, float3(1.00, 0.80, 0.60), min(1.0, pow(dmin.x * 0.25, 0.20)));
color = mix(color, float3(0.72, 0.70, 0.60), min(1.0, pow(dmin.y * 0.50, 0.50)));
color = mix(color, float3(1.00, 1.00, 1.00), 1.0 - min(1.0, pow(dmin.z * 1.00, 0.15)));
color = 1.25 * color * color;
color *= 0.5 + 0.5 * pow(16.0 * uv.x * (1.0 - uv.x) * uv.y * (1.0 - uv.y), 0.15);
output.write(float4(color, 1), gid);

The sin() function is just the sine of an angle, the cos() function is obviously the cosine of an angle, the fract() function returns the fractional part of a value, the dot() function returns the scalar product of two vectors and finally, the pow() function returns the value of a number raised to the power of another number. This code generates a beautiful fractal, a true piece of art courtesy of Inigo Quilez. The output image should look like this:

alt text

Your assignment is to try to understand how the magic works here. The source code is posted on Github as usual.

Until next time!

Leave a Reply

Your email address will not be published. Required fields are marked *