A quite important topic in Computer Graphics
is lighting and shadows. This will be a first episode from a multi-part series about Shadows in Metal
. We are going to work on the playground we used in Using metal part 15 and build up on that. Let’s set up a basic scene:
float differenceOp(float d0, float d1) {
return max(d0, -d1);
}
float distanceToRect( float2 point, float2 center, float2 size ) {
point -= center;
point = abs(point);
point -= size / 2.;
return max(point.x, point.y);
}
float distanceToScene( float2 point ) {
float d2r1 = distanceToRect( point, float2(0.), float2(0.45, 0.85) );
float2 mod = point - 0.1 * floor(point / 0.1);
float d2r2 = distanceToRect( mod, float2( 0.05 ), float2(0.02, 0.04) );
float diff = differenceOp(d2r1, d2r2);
return diff;
}
We first created the differenceOp() function which returns the difference between two signed distances. This comes in handy when we want to carve shapes out of other shapes. Next, we created the distanceToRect() function which determines if a given point is either inside or outside a rectangle. On the 1st
line we offset the current coordinates by the given center. On the 2nd
line we get the symmetrical coordinates of the given point. On the 3rd
line we get the distance to any edge. Then, we created the distanceToScene() function which gives us the closest distance to any object in the scene. Note that the fmod()
function in MSL
uses trunc()
instead of floor()
so we need to create a custom mod operator here because we also want to use the negative values, so we use the GLSL
specification for mod()
which is x - y * floor(x/y)
. We need the modulus
operator to draw many small rectangles mirrored on a distance of 0.1 from each other. Finally, we use these functions to generate a shape that looks a bit like a tall building with windows:
kernel void compute(texture2d<float, access::write> output [[texture(0)]],
constant float &timer [[buffer(0)]],
uint2 gid [[thread_position_in_grid]])
{
int width = output.get_width();
int height = output.get_height();
float2 uv = float2(gid) / float2(width, height);
uv = uv * 2.0 - 1.0;
float d2scene = distanceToScene(uv);
bool i = d2scene < 0.0;
float4 color = i ? float4( .1, .5, .5, 1. ) : float4( .7, .8, .8, 1. );
output.write(color, gid);
}
If you run the playground now you should see a similar image:

For shadows to work we need to first – get the distance to the light, second – get the direction to the light, and third – step in that direction until we either reach the light or hit an object. So let’s create a light at position lightPos which we will animate for fun. We use that good old timer uniform that we have it handy passed from the host (API
) code. Then, we get the distance from any given point to lightPos
and then just color the pixel based on the distance from the light – if not inside an object. We want the color to be lighter closer to the light and darker when further away. We use the max()
function to avoid negative values for the brightness of the light. Replace the last line in the kernel with the lines below:
float2 lightPos = float2(1.3 * sin(timer), 1.3 * cos(timer));
float dist2light = length(lightPos - uv);
color *= max(0.0, 2. - dist2light );
output.write(color, gid);
If you run the playground now you should see a similar image:

We did the first two steps (light position and direction) so let’s proceed to doing the third one – the actual shadow function:
float getShadow(float2 point, float2 lightPos) {
float2 lightDir = lightPos - point;
float dist2light = length(lightDir);
for (float i=0.; i < 300.; i++) {
float distAlongRay = dist2light * (i / 300.);
float2 currentPoint = point + lightDir * distAlongRay;
float d2scene = distanceToScene(currentPoint);
if (d2scene <= 0.) { return 0.; }
}
return 1.;
}
Let’s go over the code, line by line. We first get the direction from the point to the light. Next, we find the distance to the light so we know how far we need to move along this light ray. Then, we use a loop to divide the ray into many smaller steps. If we don’t use enough steps, we might jump past our object and that would leave “holes” in the shadow. Next, we calculate how far along the ray we are currently and move along the ray by this distance to find the point in space we’re sampling at. Then, we see how far we are from the surface at that point and then test if we are inside an object. If we are, return 0because we are in the shadow, otherwise return 1 as the ray did not hit any object. It is finally time to see some shadows! In the kernel, replace the last line with the lines below:
float shadow = getShadow(uv, lightPos);
shadow = shadow * 0.5 + 0.5;
color *= shadow;
output.write(color, gid);
We use the value 0.5 to attenuate the effect of the shadow, however, feel free to play with various values and notice how it affects itc. If you run the playground now you should see a similar image:

Right now the loop goes in one-pixel steps which is not good performance-wise. We can improve that a little by accelerating the steps along the ray. We don’t need to move in really small steps. We can move in big steps so long as we don’t step past our object. We can safely step in any direction by the distance to the scene instead of a fixed step size, and this way we skip over empty areas really fast! When finding the distance to the nearest surface, we don’t know what direction the surface is in so in fact we have the radius of a circle that intersects with the nearest part of the scene. We can trace along the ray, always stepping to the edge of the circle, until the circle radius becomes 0 which means it intersected a surface. Oh, right, this is the raymarching technique we learned about last time! Simply replace the content of the getShadow() function with the lines below:
float2 lightDir = normalize(lightPos - point);
float dist2light = length(lightDir);
float distAlongRay = 0.0;
for (float i=0.0; i < 80.; i++) {
float2 currentPoint = point + lightDir * distAlongRay;
float d2scene = distanceToScene(currentPoint);
if (d2scene <= 0.001) { return 0.0; }
distAlongRay += d2scene;
if (distAlongRay > dist2light) { break; }
}
return 1.;
In raymarching
the size of the step depends on the distance from the surface. In empty areas it jumps big distances and it can travel a long way. But if it’s parallel to the object and close to it, the distance is always small so the jump size is also small. That means the ray travels very slowly. With a fixed number of steps, it doesn’t travel far. With 80 or more steps we should be safe from getting “holes” in the shadow. If you run the playground again the output looks similar except the shadow is faster now. To see an animated version of this code, use the Shadertoy
embedded player below. Just hover over it and click the play button to watch it in action:
https://www.shadertoy.com/embed/lt3SzB
This type of shadows is called hard shadows
. Next time we will be looking into soft shadows
which are more realistic and better looking. The source code is posted on Github
as usual.
Until next time!