Chapters

Hide chapters

Metal by Tutorials

Fourth Edition · macOS 14, iOS 17 · Swift 5.9 · Xcode 15

Section I: Beginning Metal

Section 1: 10 chapters
Show chapters Hide chapters

Section II: Intermediate Metal

Section 2: 8 chapters
Show chapters Hide chapters

Section III: Advanced Metal

Section 3: 8 chapters
Show chapters Hide chapters

28. Advanced Shadows
Written by Caroline Begbie & Marius Horga

Heads up... You’re accessing parts of this content for free, with some sections shown as xgdoltbov text.

Heads up... You’re accessing parts of this content for free, with some sections shown as ghkugbkym text.

Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now

Shadows and lighting are important topics in Computer Graphics. In Chapter 13, “Shadows”, you learned how to render basic shadows in two passes: one to render from the light source location to get a shadow map of the scene, and one to render from the camera location to incorporate the shadow map into the rendered scene.

Rasterization does not excel at rendering shadows and light because there’s no geometry that a vertex shader could precisely process. So now you’ll learn how to do it differently.

Time to conjure up your raymarching skills from the previous chapter, and use them to create shadows.

By the end of this chapter, you’ll be able to create various shadow types using raymarching in compute shaders:

  • Hard shadows.
  • Soft shadows.
  • Ambient Occlusion.

Hard Shadows

When creating shadows in a rasterized render pass, you create a shadow map, which requires you to bake the shadows.

With raymarching, you make use of signed distance fields (SDF). An SDF is a real-time tool that provides you with the precise distance to a boundary. This makes calculating shadows easy as they come for “free”, meaning that all of the information you need to compute shadows already exists and is available because of the SDF.

The principle is common to both rendering methods: If there’s an occluder between the light source and the object, the object is in the shadow. Otherwise, it’s lit.

Great! Time to put that wisdom down in code.

The Starter App

➤ In Xcode, open the starter app included with this chapter and build and run (or set up the SwiftUI Canvas preview).

Heads up... You’re accessing parts of this content for free, with some sections shown as qszuzflil text.

Heads up... You’re accessing parts of this content for free, with some sections shown as cxtiwwtuv text.

Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now
The starter app
Yhi bbivzed ihd

struct Rectangle {
  float2 center;
  float2 size;
};
float distanceToRectangle(float2 point, Rectangle rectangle) {
  // 1
  float2 distances =
      abs(point - rectangle.center) - rectangle.size / 2;
  return
    // 2
    all(sign(distances) > 0)
    ? length(distances)
    // 3
    : max(distances.x, distances.y);
}

Heads up... You’re accessing parts of this content for free, with some sections shown as lgjawbnil text.

Heads up... You’re accessing parts of this content for free, with some sections shown as szbaxmsyw text.

Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now
Shape subtraction
Ylore geqjxirrieq

float differenceOperator(float d0, float d1) {
  return max(d0, -d1);
}
float distanceToScene(float2 point) {
  // 1
  Rectangle r1 = Rectangle{float2(0.0), float2(0.3)};
  float d2r1 = distanceToRectangle(point, r1);
  // 2
  Rectangle r2 = Rectangle{float2(0.05), float2(0.04)};
  float2 mod = point - 0.1 * floor(point / 0.1);
  float d2r2 = distanceToRectangle(mod, r2);
  // 3
  float diff = differenceOperator(d2r1, d2r2);
  return diff;
}

Heads up... You’re accessing parts of this content for free, with some sections shown as spruwjvyw text.

Heads up... You’re accessing parts of this content for free, with some sections shown as fswyxtliz text.

Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now
float d2scene = distanceToScene(uv);
bool inside = d2scene < 0.0;
color = inside ? float4(0.8,0.5,0.5,1.0) :
  float4(0.9,0.9,0.8,1.0);
The initial scene
Fxi uzorued rzadu

float2 lightPos = 2.8 * float2(sin(time), cos(time));
float dist2light = length(lightPos - uv);
color *= max(0.3, 2.0 - dist2light);
A moving light
E jacofm cegqv

Heads up... You’re accessing parts of this content for free, with some sections shown as swvytlkaf text.

Heads up... You’re accessing parts of this content for free, with some sections shown as gksoqwpoj text.

Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now
float getShadow(float2 point, float2 lightPos) {
  // 1
  float2 lightDir = lightPos - point;
  // 2
  for (float lerp = 0; lerp < 1; lerp += 1 / 300.0) {
    // 3
    float2 currentPoint = point + lightDir * lerp;
    // 4
    float d2scene = distanceToScene(currentPoint);
    if (d2scene <= 0.0) { return 0.0; }
  }
  return 1.0;
}
float shadow = getShadow(uv, lightPos);
color *= 2;
color *= shadow * .5 + .5;
A moving shadow
O gedefg ndizix

Heads up... You’re accessing parts of this content for free, with some sections shown as njjewrpik text.

Heads up... You’re accessing parts of this content for free, with some sections shown as pdgaktpyb text.

Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now
Sphere intersection
Mllaqa arwispetniud

float2 lightDir = normalize(lightPos - point);
float shadowDistance = 0.75;
float distAlongRay = 0.0;
for (float i = 0; i < 80; i++) {
  float2 currentPoint = point + lightDir * distAlongRay;
  float d2scene = distanceToScene(currentPoint);
  if (d2scene <= 0.001) { return 0.0; }
  distAlongRay += d2scene;
  if (distAlongRay > shadowDistance) { break; }
}
return 1.0;
A more accurate shadow
E luto emgulona qfoxan

Soft Shadows

Shadows are not only black or white, and objects aren’t just in shadow or not. Often times, there are smooth transitions between the shadowed areas and the lit ones.

struct Ray {
  float3 origin;
  float3 direction;
};

struct Sphere {
  float3 center;
  float radius;
};

struct Plane {
  float yCoord;
};

struct Light {
  float3 position;
};

Heads up... You’re accessing parts of this content for free, with some sections shown as llqaskmil text.

Heads up... You’re accessing parts of this content for free, with some sections shown as kwcifcjum text.

Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now
float distToSphere(Ray ray, Sphere s) {
  return length(ray.origin - s.center) - s.radius;
}

float distToPlane(Ray ray, Plane plane) {
  return ray.origin.y - plane.yCoord;
}

float differenceOp(float d0, float d1) {
  return max(d0, -d1);
}

float unionOp(float d0, float d1) {
  return min(d0, d1);
}
The union operation
Khu ovooc olayemiup

float distToScene(Ray r) {
  // 1
  Plane p = Plane{0.0};
  float d2p = distToPlane(r, p);
  // 2
  Sphere s1 = Sphere{float3(2.0), 2.0};
  Sphere s2 = Sphere{float3(0.0, 4.0, 0.0), 4.0};
  Sphere s3 = Sphere{float3(0.0, 4.0, 0.0), 3.9};
  // 3
  Ray repeatRay = r;
  repeatRay.origin = fract(r.origin / 4.0) * 4.0;
  // 4
  float d2s1 = distToSphere(repeatRay, s1);
  float d2s2 = distToSphere(r, s2);
  float d2s3 = distToSphere(r, s3);
  // 5
  float dist = differenceOp(d2s2, d2s3);
  dist = differenceOp(dist, d2s1);
  dist = unionOp(d2p, dist);
  return dist;
}

Heads up... You’re accessing parts of this content for free, with some sections shown as lfbazszab text.

Heads up... You’re accessing parts of this content for free, with some sections shown as lfbuqzcam text.

Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now
float3 getNormal(Ray ray) {
  float2 eps = float2(0.001, 0.0);
  float3 n = float3(
    distToScene(Ray{ray.origin + eps.xyy, ray.direction}) -
    distToScene(Ray{ray.origin - eps.xyy, ray.direction}),
    distToScene(Ray{ray.origin + eps.yxy, ray.direction}) -
    distToScene(Ray{ray.origin - eps.yxy, ray.direction}),
    distToScene(Ray{ray.origin + eps.yyx, ray.direction}) -
    distToScene(Ray{ray.origin - eps.yyx, ray.direction}));
  return normalize(n);
}
color = 0;
uv.y = -uv.y;
// 1
Ray ray = Ray{float3(0., 4., -12), normalize(float3(uv, 1.))};
// 2
for (int i = 0; i < 100; i++) {
  // 3
  float dist = distToScene(ray);
  // 4
  if (dist < 0.001) {
    color = 1.0;
    break;
  }
  // 5
  ray.origin += ray.direction * dist;
}
// 6
float3 n = getNormal(ray);
color = float4(color.xyz * n, 1);
Colors representing normals
Pudacc cepborupxikc peqqebg

Heads up... You’re accessing parts of this content for free, with some sections shown as nlpysnwut text.

Heads up... You’re accessing parts of this content for free, with some sections shown as nbkihsrew text.

Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now
float lighting(Ray ray, float3 normal, Light light) {
  // 1
  float3 lightRay = normalize(light.position - ray.origin);
  // 2
  float diffuse = max(0.0, dot(normal, lightRay));
  // 3
  float3 reflectedRay = reflect(ray.direction, normal);
  float specular = max(0.0, dot(reflectedRay, lightRay));
  // 4
  specular = pow(specular, 200.0);
  return diffuse + specular;
}
Light light = Light{float3(sin(time) * 10.0, 5.0,
                           cos(time) * 10.0)};
float l = lighting(ray, n, light);
color = float4(color.xyz * l, 1.0);

Heads up... You’re accessing parts of this content for free, with some sections shown as gjnuhgnoj text.

Heads up... You’re accessing parts of this content for free, with some sections shown as kndyvwfik text.

Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now
A light circling the sphere
O caqym juslmerp hro qsruce

float shadow(Ray ray, Light light) {
  float3 lightDir = light.position - ray.origin;
  float lightDist = length(lightDir);
  lightDir = normalize(lightDir);
  float distAlongRay = 0.01;
  for (int i = 0; i < 100; i++) {
    Ray lightRay = Ray{ray.origin + lightDir * distAlongRay,
                       lightDir};
    float dist = distToScene(lightRay);
    if (dist < 0.001) { return 0.0; }
    distAlongRay += dist;
    if (distAlongRay > lightDist) { break; }
  }
  return 1.0;
}
float s = shadow(ray, light);
color = float4(color.xyz * l * s, 1.0);
Light casting shadows
Poqgz poygigy whiwufy

Heads up... You’re accessing parts of this content for free, with some sections shown as wvjyfmfeg text.

Heads up... You’re accessing parts of this content for free, with some sections shown as ggzutvxun text.

Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now
// 1
float shadow(Ray ray, float k, Light l) {
  float3 lightDir = l.position - ray.origin;
  float lightDist = length(lightDir);
  lightDir = normalize(lightDir);
  // 2
  float light = 1.0;
  float eps = 0.1;
  // 3
  float distAlongRay = eps * 2.0;
  for (int i=0; i<100; i++) {
    Ray lightRay = Ray{ray.origin + lightDir * distAlongRay,
                       lightDir};
    float dist = distToScene(lightRay);
    // 4
    light = min(light, 1.0 - (eps - dist) / eps);
    // 5
    distAlongRay += dist * 0.5;
    eps += dist * k;
    // 6
    if (distAlongRay > lightDist) { break; }
  }
  return max(light, 0.0);
}

Heads up... You’re accessing parts of this content for free, with some sections shown as mgxetbhuw text.

Heads up... You’re accessing parts of this content for free, with some sections shown as bcnaslmop text.

Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now
// 1
bool hit = false;
for (int i = 0; i < 200; i++) {
  float dist = distToScene(ray);
  if (dist < 0.001) {
    hit = true;
    break;
  }
  ray.origin += ray.direction * dist;
}
// 2
float3 col = 1.0;
// 3
if (!hit) {
  col = float3(0.8, 0.5, 0.5);
} else {
  float3 n = getNormal(ray);
  Light light = Light{float3(sin(time) * 10.0, 5.0,
                             cos(time) * 10.0)};
  float l = lighting(ray, n, light);
  float s = shadow(ray, 0.3, light);
  col = col * l * s;
}
// 4
Light light2 = Light{float3(0.0, 5.0, -15.0)};
float3 lightRay = normalize(light2.position - ray.origin);
float fl = max(0.0, dot(getNormal(ray), lightRay) / 2.0);
col = col + fl;
color = float4(col, 1.0);
Shadow Tones
Bjojiq Depag

Ambient Occlusion

Ambient occlusion (AO) is a global shading technique, unlike the Phong local shading technique you learned about in Chapter 10, “Lighting Fundamentals”. AO is used to calculate how exposed each point in a scene is to ambient lighting which is determined by the neighboring geometry in the scene.

Plane p{0.0};
float d2p = distToPlane(r, p);
return d2p;

Heads up... You’re accessing parts of this content for free, with some sections shown as bpciwkgep text.

Heads up... You’re accessing parts of this content for free, with some sections shown as mjgyzqvab text.

Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now
Ambient occlusion starter scene
Ipmioyg epzriwuor qdexzix pbuha

struct Box {
  float3 center;
  float size;
};
float distToBox(Ray r, Box b) {
  // 1
  float3 d = abs(r.origin - b.center) - float3(b.size);
  // 2
  return min(max(d.x, max(d.y, d.z)), 0.0)
              + length(max(d, 0.0));
}
// 1
Sphere s1 = Sphere{float3(0.0, 0.5, 0.0), 8.0};
Sphere s2 = Sphere{float3(0.0, 0.5, 0.0), 6.0};
Sphere s3 = Sphere{float3(10., -5., -10.), 15.0};
float d2s1 = distToSphere(r, s1);
float d2s2 = distToSphere(r, s2);
float d2s3 = distToSphere(r, s3);
// 2
float dist = differenceOp(d2s1, d2s2);
dist = differenceOp(dist, d2s3);
// 3
Box b = Box{float3(1., 1., -4.), 1.};
float dtb = distToBox(r, b);
dist = unionOp(dist, dtb);
dist = unionOp(d2p, dist);
return dist;

Heads up... You’re accessing parts of this content for free, with some sections shown as zlfajfcyb text.

Heads up... You’re accessing parts of this content for free, with some sections shown as sntihzfyc text.

Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now
The ambient occlusion scene
Sgi olriuvh ahnluxaew zteli

float ao(float3 pos, float3 n) {
    return n.y * 0.5 + 0.5;
}
col = col * l * s;
float o = ao(ray.origin, n);
col = col * o;
col = col + fl;

Heads up... You’re accessing parts of this content for free, with some sections shown as wnxuztquw text.

Heads up... You’re accessing parts of this content for free, with some sections shown as cgheqhcam text.

Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now
// 1
float eps = 0.01;
// 2
pos += n * eps * 2.0;
// 3
float occlusion = 0.0;
for (float i = 1.0; i < 10.0; i++) {
  // 4
  float d = distToScene(Ray{pos, float3(0)});
  float coneWidth = 2.0 * eps;
  // 5
  float occlusionAmount = max(coneWidth - d, 0.);
  // 6
  float occlusionFactor = occlusionAmount / coneWidth;
  // 7
  occlusionFactor *= 1.0 - (i / 10.0);
  // 8
  occlusion = max(occlusion, occlusionFactor);
  // 9
  eps *= 2.0;
  pos += n * eps;
}
// 10
return max(0.0, 1.0 - occlusion);

Heads up... You’re accessing parts of this content for free, with some sections shown as nhdopvfob text.

Heads up... You’re accessing parts of this content for free, with some sections shown as xwnynklyb text.

Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now
Ambient occlusion
Ujxiiwf izmbipoot

Heads up... You’re accessing parts of this content for free, with some sections shown as vsvoshmer text.

Heads up... You’re accessing parts of this content for free, with some sections shown as gvkecjkup text.

Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now
struct Camera {
  float3 position;
  Ray ray{float3(0), float3(0)};
  float rayDivergence;
};
Camera setupCam(float3 pos, float3 target,
                float fov, float2 uv, int x) {
  // 1
  uv *= fov;
  // 2
  float3 cw = normalize(target - pos);
  // 3
  float3 cp = float3(0.0, 1.0, 0.0);
  // 4
  float3 cu = normalize(cross(cw, cp));
  // 5
  float3 cv = normalize(cross(cu, cw));
  // 6
  Ray ray = Ray{pos,
                normalize(uv.x * cu + uv.y * cv + 0.5 * cw)};
  // 7
  Camera cam = Camera{pos, ray, fov / float(x)};
  return cam;
}

Heads up... You’re accessing parts of this content for free, with some sections shown as scpufxqex text.

Heads up... You’re accessing parts of this content for free, with some sections shown as gvxadxriw text.

Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now
Ray ray = Ray{float3(0., 4., -12), normalize(float3(uv, 1.))};
float3 camPos = float3(sin(time) * 10., 3., cos(time) * 10.);
Camera cam = setupCam(camPos, float3(0), 1.25, uv, width);
Ray ray = cam.ray;
Camera circling the scene
Mijeja qezzzulg qri rdofo

Key Points

  • Raymarching produces better quality shadows than rasterized shadows.
  • Hard shadows are not realistic, as there are generally multiple light sources in the real world.
  • Soft shadows give better transitions between areas in shadow and not.
  • Ambient occlusion does not depend on scene lighting, but on neighboring geometry. The closer geometry is to an area, the darker the area is.

Where to Go From Here?

In addition to the shadow types you learned in this chapter, there are other shadow techniques such as Screen Space Ambient Occlusion and Shadow Volumes. If you’re interested in learning about these, review references.markdown in the resources folder for this chapter.

Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.
© 2025 Kodeco Inc.

You’re accessing parts of this content for free, with some sections shown as xcgyqdgot text. Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now