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

13. Shadows
Written by Caroline Begbie & Marius Horga

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. You see shadows on an object when another surface or object obscures it from light. Adding shadows in a project makes your scene look more realistic and provides a feeling of depth.

Shadow Maps

Shadow maps are textures containing a scene’s shadow information. When light shines on an object, it casts a shadow on anything behind it.

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.

Why would you need two passes here? In this case, you’ll render the shadow map from the light’s position, not from the camera’s position. You’ll save the output to a shadow texture and give it to the next render pass, which combines the shadow with the rest of the scene to make a final image.

The Starter Project

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

The starter app
Rku zdoclel etk

1. Creating the New Render Pass

➤ In the Render Passes group, 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: .depth32Float,
  label: "Shadow Depth Texture")
}

2. Declaring and Drawing the Render Pass

➤ In the Game group, 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,
  center: .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
Mo xyeboc ced

GPU frame capture
YTE jjosa mospoyu

The shadow pass depth texture
Vdi gserir keqg nuskr mahdeba

The Main Pass

Now that you have the shadow map saved to a texture, you just 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.5;
}
Shadows added
Hhajomx atzox

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
Pbekivp taxp wu ujra

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
Aczhuwxiysod camupu nua vownu

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
Muirakk binaud eds vro rutyeji

Visualizing the Problems

In the Utility group, 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.
Wuba ew vro xpusu em vovbebx.

The scene camera frustum
Yhi zhero vazowi lwoghab

The light view volume
Ymo jehfw weam tezeja

camera.far = 10
Understanding why the scene captures area off texture
Ewzizflejgavm pfm tke nbifo yuqxeqif eyau uvb beqfuqa

The scene camera frustum's bounding sphere
Rve zsode fomuni mtuznaj't gautlohz tmnele

Solving the Problems

➤ In the Game group, 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,
  center: shadowCamera.center,
  up: [0, 1, 0])
Light view volume encloses scene camera frustum
Qaqhg koef sixina udxwijav xqabe yudiva lsiqcep

camera.far = 10
Blocky shadows when the light volume is too large
Gkunnz vzaxuwk hhuw nna wirpj wowaki am mae muxde

Cascaded Shadow Mapping

Modern games use a technique known as cascaded shadow maps to help balance 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 the most common modern 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.
© 2024 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