# Soft Shadow Mapping

Shadow mapping is one of those things that a lot of people struggle with. It is also a very old shadowing technique that has been improved in a variety of ways. I'd like to make a brief trip trough the history of shadow mapping hopefully shedding some light on the topic and introduce you to some very nice techniques.

## Contents

- Preface
- Test scene without shadow
- Hard Shadow Mapping
- Interpolated shadowing
- Percentage Closer Filtering (PCF)
- PCF and Interpolation
- Variance Shadow Mapping (VSM)
- Antialiased and Filtered VSM

## Preface

The examples in this article use WebGL and a set of specific capabilities such as **floating point texture render targets**. You might not have support for these features. In that case the illustrations are non-interactive screenshots.

### Scope

This blog post can't cover all shadowing topics, or even most optimizations you can apply. It will cover the basics of several shadow mapping techniques in a very simple and not optimized setup.

### Syntax

All examples and supporting code is written in CoffeeScript. The reason is that I find CoffeeScript pleasant, it allows me to represent the subject matter clearly and it supports multiline strings (unlike javascript) which makes writing shaders much easier.

### Debugging

To aid shader debugging I have introduced a **custom build** step that seeks out "//essl" and replaces it with a #line directive indicating a sourceline and file. This is not necessary to run, since "//essl" would **just be a comment in essl** otherwise. However if you plan to **toy with the code**, I recommend you **use that buildstep** as it makes debugging shaders **much easier**.

### Code

You can obtain the code for all examples on github.

### Standalone Demos

A standalone version of the examples can be found here

### Play

All illustrations are interactive.

- Left click+drag for changing viewing angles
- Scroll for zoom in/out
- Stop/Start animations (button top right in the example viewport)
- Make fullscreen (button bottom right in the example viewport)

### Recommended Reading

You can read everything I'm going to explain here and much more in the book real-time shadows

The GPU Gems series by nvidia also has a wealth of shadowing information (freely available online)

- GPU Gems 1, Chapter 9: Efficient Shadow Volume Rendering
- GPU Gems 1, Chapter 11: Shadow Map Antialiasing
- GPU Gems 1, Chapter 12: Omnidirectional Shadow Mapping
- GPU Gems 1, Chapter 13: Generating Soft Shadows Using Occlusion Interval Maps
- GPU Gems 1, Chapter 14: Perspective Shadow Maps: Care and Feeding
- GPU Gems 2, Chapter 17: Efficient Soft-Edged Shadows Using Pixel Shader Branching
- GPU Gems 3, Chapter 8: Summed-Area Variance Shadow Maps
- GPU Gems 3, Chapter 10: Parallel-Split Shadow Maps on Programmable GPUs
- GPU Gems 3, Chapter 11: Efficient and Robust Shadow Volumes Using Hierarchical Occlusion Culling and Geometry Shaders

## Test scene without shadow

In this example a simple spotlight is setup that has the following characteristics:

- Attenuation with distance
- Influence area of 55 degrees
- 10 degree smoothing for the influence
- A simple lambertian surface radiance
- A 64x64 pixel shadow map is used (that is a low resolution). The purpose is to visualize error well.

This setup will be the basis for the further examples.

### Conventions and Spaces

Since shadow mapping is a variant of projective texturing, it is important to have a clear convention in what "space" a given data point is expressed. I use these conventions.

- World: The world space, positions and normals in this space are
**independent of viewpoint or model transformations**. I prefix variables in this space with "world". - Camera: This space expresses things in relation to the
**observers viewpoint**. The prefix "cam" is used. - Light: This space expresses things in relation to the
**light viewpoint**, the prefix "light" is used.

Furthermore each space might have distinctive variants these are:

- View: The
**translational/rotational**transformation to this space (such as camView, lightView). - Projection: The transform to device coordinates, using a projective matrix (usually perspective, such as camProj, lightProj).
- UV: The
**texture coordinates**of a data point (obviously important for shadow mapping).

### Preferred space for calculations

Lighting **calculations** in this tutorial are done **in light space**. The obvious benefit of this is to **avoid convoluted transformations** back and forth between light space and camera space for shadow mapping. As a consequence the light is defined in terms of a transform/rotation matrix and projection, rather than as a light position and direction.

## Hard Shadow Mapping

This example looks a bit better.

### Method

The **depth from the lights** point of view is rendered into a texture. This texture is then looked up in the shader and compared to the calculated depth in the camera pass in lightspace.

float lightDepth1 = texture2D(sLightDepth, lightUV).r; float lightDepth2 = clamp(length(lightPos)/40.0, 0.0, 1.0); float bias = 0.001; float illuminated = step(lightDepth2, lightDepth1+bias);

If lightDepth1+bias is bigger than lightDepth2 then an area is considered to be illuminated.

The **depth** value is **linear** and clamped to 0 and 1. In hard shadow mapping this serves no immediate purpose but it will become important later on. The **value 40** is chosen because at that distance given the light attenuation (and using a gamma of 2.2) the **observable radiance** has fallen below 0.5/256th and is hence **insignificant**. It is in fact the **far range of the light**.

### Shading vs. Shadowing

It deserves mention that the example code does shading (attenuation with distance, influence on the cone, surface radiance evaluation) at the same time as it computes the shadow.

The reason to do it this way is that **shadow mapping** algorithms have **artifacts**. But a lot of these artifacts are actually **not visible** once a scene **is shaded**. Hence it is good practise to evaluate them together.

### Drawbacks

There are some obvious problems with this method:

- Aliasing is visible from the light depth compare.
- The shadow border is very hard.

However it has the advantage of being fairly fast.

## Interpolated shadowing

The idea of this example is to linear interpolate shadow lookup. This functionality is present in actual OpenGL as texture2DShadow. We don't have that in WebGL, so let's reimplement it.

### Method

I introduce a new texturing function that does the same thing as in hard shadow mapping.

float texture2DCompare(sampler2D depths, vec2 uv, float compare){ float depth = texture2D(depths, uv).r; return step(compare, depth); }

Then this function is used to perform 4 lookups into the surrounding 4 texels, hence 4 compares are done.

float texture2DShadowLerp(sampler2D depths, vec2 size, vec2 uv, float compare){ vec2 texelSize = vec2(1.0)/size; vec2 f = fract(uv*size+0.5); vec2 centroidUV = floor(uv*size+0.5)/size; float lb = texture2DCompare(depths, centroidUV+texelSize*vec2(0.0, 0.0), compare); float lt = texture2DCompare(depths, centroidUV+texelSize*vec2(0.0, 1.0), compare); float rb = texture2DCompare(depths, centroidUV+texelSize*vec2(1.0, 0.0), compare); float rt = texture2DCompare(depths, centroidUV+texelSize*vec2(1.0, 1.0), compare); float a = mix(lb, lt, f.y); float b = mix(rb, rt, f.y); float c = mix(a, b, f.x); return c; }

The resulting illumination results are bilinearly interpolated and returned.

### Drawbacks

- It does
**not**offer**a much improvement**, it just smooths the shadowing a bit between texels. - The
**cost has gone up**as 4 lookups are performed now. They are expensive because shadow lookups are not vram/cache coherent. - The bilinear interpolation
**introduces artifacts**of its own such as the typical diamond pattern. - There can be depth error artifacts because now linear interpolation isn't a depth estimator
- Aliasing is still clearly visible

## Percentage Closer Filtering (PCF)

The idea behind this example is to simply **average the result of the compare** over a patch surrounding the uv coordinate. It offers a similar result to linear interpolation, but up close it looks pretty horrible.

### Method

We replace the texture2DShadowLerp function with the PCF function. This looks up the shadow compare for a 5x5 region with the UV coordinate at the center, then divides the result by 25.

float PCF(sampler2D depths, vec2 size, vec2 uv, float compare){ float result = 0.0; for(int x=-2; x<=2; x++){ for(int y=-2; y<=2; y++){ vec2 off = vec2(x,y)/size; result += texture2DCompare(depths, uv+off, compare); } } return result/25.0; }

### Drawbacks

- It is even
**more expensive**than linear interpolation. - It introduces a banding artifacts over the sample kernel.

## PCF and Interpolation

This example combines linear interpolation and PCF. This is starting to look fairly acceptable.

### Method

float PCF(sampler2D depths, vec2 size, vec2 uv, float compare){ float result = 0.0; for(int x=-1; x<=1; x++){ for(int y=-1; y<=1; y++){ vec2 off = vec2(x,y)/size; result += texture2DShadowLerp(depths, size, uv+off, compare); } } return result/9.0; }

The size of the kernel is reduced since with linear interpolation it does not have to be very big. The kernel banding is mostly gone. Some artifacts of aliasing are still visible, but much less so than previously. The quality of this method is quite good, of course at a cost.

### Drawbacks

- It's even more expensive than PCF alone. Although if there was a texture2DShadow function built in, that would obviously be a faster than reimplementing it in ESSL.

## Variance Shadow Mapping (VSM)

The idea behind this is to statistically measure the **likelyhood of occlusion** based on variance. Chebeyshevs inequality is used to compute an upper bound for the occlusion. It looks very similar to linear interpolated shadow mapping.

There several advantages to the technique.

- It is
**very cheap**(just one lookup per fragment) - It makes it possible to
**pre-filter the depths**

Important: VSM only works with linear depths starting at 0 near the light and going to 1 to the far range of the light.

### Method

First we need to compute the moments of the depths. This is done during light depth rendering:

float dx = dFdx(depth); float dy = dFdy(depth); gl_FragColor = vec4(depth, pow(depth, 2.0) + 0.25*(dx*dx + dy*dy), 0.0, 1.0);

In order to write the variance function we need a linstep function analogous to smoothstep:

float linstep(float low, float high, float v){ return clamp((v-low)/(high-low), 0.0, 1.0); }

Then the variance has to be used to compute the shadowing function:

float VSM(sampler2D depths, vec2 uv, float compare){ vec2 moments = texture2D(depths, uv).xy; float p = smoothstep(compare-0.02, compare, moments.x); float variance = max(moments.y - moments.x*moments.x, -0.001); float d = compare - moments.x; float p_max = linstep(0.2, 1.0, variance / (variance + d*d)); return clamp(max(p, p_max), 0.0, 1.0); }

There are a couple of noteworthy ways in which this works.

- "p" holds a hard shadow comparision, however the bias is applied softly via a smoothstep
- p_max is stepped between 0.2 and 1.0, this reduces an artifact known as light bleeding.

### Drawbacks

- In its plain form (unfiltered) VSM isn't better than linear interpolated shadows.
- Due to a need to sample the front surfaces, there can be
**more depth error banding issues**.

## Antialiased and Filtered VSM

The idea of this example is to **antialias** the shadow depths first and then **blur** them slightly. The result is **substantially better** than anything so far.

### Antialias

If there was Framebuffer MSAA or similar in WebGL we could use this. As it is, this is not a choice, so let's reimplement anti-aliasing in a fast brute force method. The light depth is rendered at 256x256 resolution and then supersampled efficiently with linear interpolation first to 128x128 and then to 64x64. This is equivalent to **4x4 MSAA**.

The definition of the filters:

downsample128 = new Filter 128, '''//essl return get(0.0, 0.0); ''' downsample64 = new Filter 64, '''//essl return get(0.0, 0.0); '''

Applying them after rendering the light depth:

downsample128.apply lightDepthTexture downsample64.apply downsample128

### Blur

A simple 3x3 box filter is then used on the downsampled 64x64 light depths.

boxFilter = new Filter 64, '''//essl vec3 result = vec3(0.0); for(int x=-1; x<=1; x++){ for(int y=-1; y<=1; y++){ result += get(x,y); } } return result/9.0; '''

And applying after downsampling:

boxFilter.apply downsample64

Now instead of passing in the lightDepthTexture for VSM, we pass in the boxFilter texture, the VSM code is unchanged:

.sampler('sLightDepth', boxFilter)

### Advantage

Unlike when filtering at shadow application, the filtering with VSM can be done prior at light depth texture resolution, this offers the following advantages:

- Filtering the light depth texture is
**VRAM/cache coherent**. - The
**resolution**of the light depth texture is usually**smaller**than fragments on screen (this example only uses 64x64 light depth texels) - In forward shading there
**might be overdraw**, which would cause multiple lookups into the light depth texture that are never used.