Shadows

You may need to read the articles about cubemaps and framebuffers before this one.


Shadow Maps

The most common way to render shadows is with shadow maps, which are textures that contain depth data from the point of view of a light source. A shadow map can be created by rendering a scene to a framebuffer with a depth attachment from the point of view of the light source. The fragment's depth is stored in the first (r) channel of the depth texture.

Any surface that falls within the frustum of the projection but has a lower depth value than the shadow map at that position is in shadow.

#version 300 es

precision mediump float;

in vec2 v_texcoord;
in vec4 v_projTexcoord;

uniform vec4 u_color;
uniform sampler2D u_texture;
uniform sampler2D u_projTexture;

out vec4 outColor;

void main() {
	vec3 projTexcoord = v_projTexcoord.xyz / v_projTexcoord.w;
	float depth = projTexcoord.z;

	bool inShadow = projTexcoord.x >= 0.0
		&& projTexcoord.x <= 1.0
		&& projTexcoord.y >= 0.0
		&& projTexcoord.y <= 1.0;

	float projDepth = texture(u_projTexture, projTexcoord.xy).r;
	float shadowLight = inShadow && projDepth <= depth ? 0.0 : 1.0;

	outColor = texture(u_texture, v_texcoord) * u_color;
	outColor.rgb *= shadowLight;
}

For diffuse lighting, it is typical to use an orthographic projection to render the shadow map, since diffuse lights are treated as being infinitely far away from the scene.

In order to create an omnidirectional shadow map, use a cubemap as the depth texture and render each face of the shadow map individually. Since omnidirectional shadow maps are used to emulate the shadows formed by a point light, it is appropriate to use a perspective projection to render each of the faces.

Shadow Acne

The strange patterns in the example above are called shadow acne and are a result of the limited resolution of the shadow map causing surfaces to shade themselves. The visual effect of shadow acne can be mitigated by adding a bias to the calculated depth of each fragment. I have also seen shadow acne be erroneously referred to as mach bands, but that is a distinct phenomenon.

float depth = projTexcoord.z + u_bias;

The bias required to remove shadow acne from a surface depends on the angle from the light source to that surface, so varying the bias value based on that angle can improve the appearance of the scene.

float bias = max(u_biasMax * (1.0 - dot(v_normal, dirToLight)), u_biasMin);

Adding a bias can lead to an effect called peter panning in which shadows appear to be slightly disjointed from their caster. The visual effect of peter panning can be mitigated by culling front faces when rendering the shadow map.

Percentage-Closer Filtering

Percentage-closer filtering (PCF) is a technique for anti-aliasing shadows. One simple implementation of PCF is to sample the surrounding texels from the depth map and average the results.

float shadowLight = 0.0;
vec2 texelSize = 1.0 / vec2(textureSize(u_projTexture, 0));
for (int x = -1; x <= 1; x++) {
	for (int y = -1; y <= 1; y++) {
		float projDepth = texture(u_projTexture,
			projTexcoord.xy + vec2(x, y) * texelSize).r;
		shadowLight += inShadow && projDepth <= depth ? 0.0 : 1.0;
	}
}
shadowLight /= 9.0;

The next article is about fog.