Intermediate Computer Graphics Week 7: Shadows

Shadows are incredibly important in graphics. They increase realism and can also be used for communicating subtle information to the viewer. For example, the location of a character’s shadow can indicate the character’s depth in the scene. It can also tell us whether or not the character is jumping in a static image.

The location of the shadow in the first image tells us that the character is not touching the ground.

The location of the shadow in the first image tells us that the character is not touching the ground. The second image does not show the shadow, and so we cannot tell if the character is on the ground or not.

There are several techniques to create shadows in a scene, but some of them are too resource intensive to use in real-time applications. One technique that is a good trade-off between performance and realism is shadow mapping.

Shadow mapping is a technique where shadows are produced by checking if a fragment is visible to the light source. If the fragment is visible to the light source, then it is not in shadow, and otherwise it is. Objects in the scene can be blockers or receivers. Blockers cast shadows, while receivers are the surfaces upon which the shadows fall.

The shadow mapping algorithm proceeds as follows. First, the scene is rendered from the light’s perspective, and only the depth information is stored. This depth information is called the shadow map. Then, the scene is rendered a second time from the camera’s perspective with the shadow map as an input.  During this second render, each fragment is transformed into light-space and the depth is compared to a sample from the shadow map. If the depth is greater than that found in the shadow map, the fragment is in shadow. Otherwise, the fragment is not in shadow. I prefer to be able to manipulate and post-process shadows if I need to, so I have separated the shadow generation out from the regular scene rendering. I then composite the two at the end of the process.

The first pass to create the shadow map uses the exact same shader as is normally used for rendering. The only difference is that the color information is discarded, and only the depth information is kept. Therefore, I will not show this shader here.

Here is the fragment shader that I use to draw shadows to their own framebuffer. Note that I work in world-space in my fragment shaders, so the transformation matrix to transform the position of the current fragment is from world-space to shadow map-space, instead of camera-space to shadow map-space.

#version 420

in vec3 Position;
in vec2 texCoord;
in vec3 Normal;

out vec4 color;

uniform mat4 worldToShadowMap;	  // The matrix to transform the fragment to shadow map-space
uniform sampler2D shadowMapDepth; // The shadow map, stored as a texture.
uniform bool isReceiver;	  // Indicates whether or not to cast a shadow on this fragment.

void main()
{
	vec3 result = vec3(1.0);

	if(isReceiver) // Only draw a shadow if this fragment is a receiver.
	{
		// Transform the position of the current fragment to shadow map-space
		vec4 shadowCoord = worldToShadowMap * vec4(Position, 1.0);

		// Sample the shadow map to get the depth of the fragment that casts the shadow.
		float shadowDepth = texture(shadowMapDepth, shadowCoord.xy).r;

		// Compare the depth of the current fragment to that in the shadow map
		if(shadowDepth < shadowCoord.z - 0.001)	// The 0.001 offset is used to remove an artifact called shadow acne
		{
			// Apply shadow by multiplying the color by a value less than 1.
			// Lower values will produce darker shadows.
			result *= 0.5;
		}
	}

	color = vec4(result, 1.0);
}

Finally, I use the following very simple fragment shader to composite image of the scene with the image containing the shadows. The result is drawn to a full-screen quad.

#version 420

uniform sampler2D scene;   // The scene, stored as a texture.
uniform sampler2D shadows; // The shadows that should be in the scene, stored as a texture.

in vec2 texCoord;

out vec3 outColor;

void main()
{
	// Sample the scene.
	vec3 sceneColor = texture(scene, texCoord).rgb;

	// Sample the shadows.
	vec3 shadowColor = texture(shadows, texCoord).rgb;

	// Multiply the samples together to obtain the result.
	outColor = sceneColor * shadowColor;
}

The code required to execute shadow mapping is not actually very complicated. The most trouble comes from wrapping your mind around what is actually happening, along with getting your transformation matrix correct.

Leave a comment