Chapters

Hide chapters

Metal by Tutorials

Fifth Edition · macOS 26, iOS 26 · Swift 6, Metal 3 · Xcode 26

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

13. Shadows
Written by Marius Horga & Caroline Begbie

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

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

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

Unlock now

In this chapter, you’ll learn about shadows. A shadow represents the absence of light on a surface. When light shines on an object, it casts a shadow on anything behind it. Adding shadows in a project makes your scene look more realistic and provides a feeling of depth.

Shadow Maps

Shadow maps are textures containing the information you need to create shadows for a scene. Typically, you render the scene from your camera’s location. However, to build a shadow map, you need to render your scene from the light source’s location - in this case, the sun.

A scene render
A scene render

The image on the left shows a render from the camera’s position with the directional light pointing down. The image on the right shows a render from the directional light’s position. The eye shows the camera’s position in the first image.

You’ll do two render passes:

Two render passes are needed
Two render passes are needed

  • First pass: You’ll render from the light’s point of view. Since the sun is directional, you’ll use an orthographic camera rather than a perspective camera. You’re only interested in the depth of objects that the sun can see, so you won’t render a color texture. In this pass, you’ll only render the shadow map as a depth texture. This is a grayscale texture, with the gray value indicating depth. Black is close to the light, and white is farther away.

  • Second pass: You’ll render using the scene camera as usual, but you’ll compare the camera fragment with each shadow map fragment. If the camera fragment’s depth is less than the shadow map fragment at that position, the fragment is in the shadow. The light can see the blue x in the above image, so it isn’t in shadow. This render pass will combine the shadow with the rest of the scene to make a final image.

Why would you need two passes here? Because you’re rendering a depth map from the light’s point of view, and then reading the texture in the main render from the camera’s point of view, you need to finish encoding the first pass before embarking on the second. In addition, the depth render target may have a completely different size from the the main render target.

The Starter Project

➤ In Xcode, open this chapter’s starter project.

The starter app
Byo vheflex ejy

1. Creating the New Render Pass

➤ In the Render Passes folder, create a new Swift file named ShadowRenderPass.swift, and replace the code with:

import MetalKit

struct ShadowRenderPass: RenderPass {
  let label: String = "Shadow Render Pass"
  var descriptor: MTLRenderPassDescriptor?
    = MTLRenderPassDescriptor()
  var depthStencilState: MTLDepthStencilState?
    = Self.buildDepthStencilState()
  var pipelineState: MTLRenderPipelineState
  var shadowTexture: MTLTexture?

  mutating func resize(view: MTKView, size: CGSize) {
  }

  func draw(
    commandBuffer: MTLCommandBuffer,
    scene: GameScene,
    uniforms: Uniforms,
    params: Params
  ) {
  }
}
static func createShadowPSO() -> MTLRenderPipelineState {
  let vertexFunction =
    Renderer.library?.makeFunction(name: "vertex_depth")
  let pipelineDescriptor = MTLRenderPipelineDescriptor()
  pipelineDescriptor.vertexFunction = vertexFunction
  pipelineDescriptor.colorAttachments[0].pixelFormat = .invalid
  pipelineDescriptor.depthAttachmentPixelFormat = .depth32Float
  pipelineDescriptor.vertexDescriptor = .defaultLayout
  return createPSO(descriptor: pipelineDescriptor)
}
init() {
  pipelineState =
    PipelineStates.createShadowPSO()
  shadowTexture = Self.makeTexture(
    size: CGSize(
    width: 2048,
    height: 2048),
    pixelFormat: Renderer.viewDepthPixelFormat,
  label: "Shadow Depth Texture")
}

2. Declaring and Drawing the Render Pass

➤ In the Renderer folder, open Renderer.swift, and add the new render pass property to Renderer:

var shadowRenderPass: ShadowRenderPass
shadowRenderPass = ShadowRenderPass()
shadowRenderPass.resize(view: view, size: size)
shadowRenderPass.draw(
  commandBuffer: commandBuffer,
  scene: scene,
  uniforms: uniforms,
  params: params)

3. Setting up the Render Pass Drawing Code

➤ Open ShadowRenderPass.swift, and add the following code to draw(commandBuffer:scene:uniforms:params:):

guard let descriptor = descriptor else { return }
descriptor.depthAttachment.texture = shadowTexture
descriptor.depthAttachment.loadAction = .clear
descriptor.depthAttachment.storeAction = .store

guard let renderEncoder =
  commandBuffer.makeRenderCommandEncoder(descriptor: descriptor) else {
  return
}
renderEncoder.label = "Shadow Encoder"
renderEncoder.setDepthStencilState(depthStencilState)
renderEncoder.setRenderPipelineState(pipelineState)
for model in scene.models {
  renderEncoder.pushDebugGroup(model.name)
  model.render(
    encoder: renderEncoder,
    uniforms: uniforms,
    params: params)
  renderEncoder.popDebugGroup()
}
renderEncoder.endEncoding()

4. Setting up the Light Camera

During the shadow pass, you’ll render from the point of view of the sun, so you’ll need a new camera and some new shader matrices.

matrix_float4x4 shadowProjectionMatrix;
matrix_float4x4 shadowViewMatrix;
var shadowCamera = OrthographicCamera()
shadowCamera.viewSize = 16
shadowCamera.far = 16
let sun = scene.lighting.lights[0]
shadowCamera.position = sun.position
uniforms.shadowProjectionMatrix = shadowCamera.projectionMatrix
uniforms.shadowViewMatrix = float4x4(
  eye: sun.position,
  target: .zero,
  up: [0, 1, 0])

5. Creating the Shader Function

As you may have noticed when you set up the shadow pipeline state object in Pipelines.swift, it references a shader function named vertex_depth, which doesn’t exist yet.

#import "Common.h"

struct VertexIn {
  float4 position [[attribute(0)]];
};

vertex float4
  vertex_depth(const VertexIn in [[stage_in]],
  constant Uniforms &uniforms [[buffer(UniformsBuffer)]])
{
  matrix_float4x4 mvp =
    uniforms.shadowProjectionMatrix * uniforms.shadowViewMatrix
    * uniforms.modelMatrix;
  return mvp * in.position;
}
No shadow yet
Fa kbuzec mig

GPU frame capture
HME hbaye herzuqi

The shadow pass depth texture
Wpu sfabif lenv dakwn yufpeli

The Main Pass

Now that you have the shadow map saved to a texture, you need to send it to the main pass to use the texture in lighting calculations in the fragment function.

weak var shadowTexture: MTLTexture?
renderEncoder.setFragmentTexture(shadowTexture, index: 15)
forwardRenderPass.shadowTexture = shadowRenderPass.shadowTexture
float4 shadowPosition;
.shadowPosition =
  uniforms.shadowProjectionMatrix * uniforms.shadowViewMatrix
  * uniforms.modelMatrix * in.position
depth2d<float> shadowTexture [[texture(15)]]
// shadow calculation
// 1
float3 shadowPosition
  = in.shadowPosition.xyz / in.shadowPosition.w;
// 2
float2 xy = shadowPosition.xy;
xy = xy * 0.5 + 0.5;
xy.y = 1 - xy.y;
xy = saturate(xy);
// 3
constexpr sampler s(
  coord::normalized, filter::linear,
  address::clamp_to_edge,
  compare_func:: less);
float shadow_sample = shadowTexture.sample(s, xy);
// 4
if (shadowPosition.z > shadow_sample) {
  diffuseColor *= 0.1;
}
Shadows added
Zsoyozg oqbuf

Shadow Acne

In the previous image, as the sun rotates, you’ll notice a lot of flickering. This is called shadow acne or surface acne. The surface is self-shadowing because of a lack of float precision where the sampled texel doesn’t match the calculated value.

if (shadowPosition.z > shadow_sample + 0.001) {
Shadows with no acne
Twuzalb nagw ro akze

Identifying Problems

Take a look at the previous render, and you’ll see a problem. Actually, there are two problems. A large dark gray area on the plane appears to be in shadow but shouldn’t be.

Orthographic camera too large
Otcnotyadluz raveke bua riyna

if (xy.x < 0.0 || xy.x > 1.0 || xy.y < 0.0 || xy.y > 1.0) {
  return float4(1, 0, 0, 1);
}
Reading values off the texture
Yeirahp wiciah izb yvi hafceme

Visualizing the Problems

In the Utility folder, DebugCameraFrustum.swift will help you visualize this problem by rendering wireframes for the various camera frustums. When running the app, you can press various keys for debugging purposes:

DebugCameraFrustum.draw(
  encoder: renderEncoder,
  scene: scene,
  uniforms: uniforms)
camera.far = 5
Some of the scene is missing.
Hepo uy vqu zjata eg zuzrigf.

The scene camera frustum
Tqi clihi rozema rjegfes

The light view volume
Ypa wiczx cued wuyafe

camera.far = 10
Understanding why the scene captures area off texture
Exbabwnukbowr znt mqe kroce zewdahux ique otn nefluwu

The scene camera frustum's bounding sphere
Kja thitu mawowi bfefgaj'j haixyejs cgzera

Solving the Problems

➤ In the Cameras folder, open ShadowCamera.swift. This file contains various methods to calculate the corners of the camera frustum. createShadowCamera(using:lightPosition:) creates an orthographic camera that encloses the specified camera.

let sun = scene.lighting.lights[0]
shadowCamera = OrthographicCamera.createShadowCamera(
  using: scene.camera,
  lightPosition: sun.position)
uniforms.shadowProjectionMatrix = shadowCamera.projectionMatrix
uniforms.shadowViewMatrix = float4x4(
  eye: shadowCamera.position,
  target: shadowCamera.center,
  up: [0, 1, 0])
Light view volume encloses scene camera frustum
Zintt feoh tiqovu emnlofof fbobe salixo kvujqed

camera.far = 10
Blocky shadows when the light volume is too large
Dzenxz mkatehg bpij kke rukkk xodici af hui hezbe

Cascaded Shadow Mapping

A better method is cascaded shadow maps which balances performance and shadow depth. In Chapter 8, “Textures”, you learned about mip maps, textures of varying sizes used by the GPU depending on the distance from the camera. Cascaded shadow maps employ a similar idea.

Key Points

  • A shadow map is a render taken from the point of the light casting the shadow.
  • You capture a depth map from the perspective of the light in a first render pass.
  • A second render pass then compares the depth of the rendered fragment with the stored depth map fragment. If the fragment is in shadow, you shade the diffuse color accordingly.
  • The best shadows are where the light view volume exactly encases the scene camera’s frustum. However, you have to know how much of the scene is being captured. If the area is large, shadows will be blocky.
  • Shadows are expensive. A lot of research has gone into rendering shadows, and there are many different methods of improvements and techniques. Cascaded shadow mapping is a common technique.
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 scrambled text. Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now