Shadow Maps

CS 481 Lecture, Dr. Lawlor

In OpenGL, a fragment program's job is to render one pixel:
    gl_FragColor = ...;

The problem is that each fragment knows only about its triangle, and fragments don't know about each other.  For some stuff, like simple diffuse lighting, this works--every pixel needs to know its normal, and based on the normal it can figure out how much of the light source it can see.

But what about shadows?  Well, plain old ordinary OpenGL doesn't have any:
scene without shadows

We'd like to make some shadows.  One trick is to note that along a ray to the light source, only the first object that hits the ray will be lit; everything farther from the light source will be in shadow.

We could easily manage this in OpenGL, if we have a way to know how far we are from the light source (an easy computation) and how far the first-lit thing is from the light source (er, how?):
	float lit=1.0;
if (our_dist > first_lit_dist) lit = 0.0; /* we are in shadow */
gl_FragColor = (a+d*lit)*reflectance + vec4(s*lit);
So the shadowing problem really boils down to: is somebody blocking my view of the light source?

One cool solution to this problem is called "shadow maps":
Here's what a shadow map texture looks like.  The colors code distance from the light source: black (0.0) is close to the light source, white (1.0) is farther away.
shadow map, with distances encoded as color
The big central spike is closest to the camera, and hence the darkest thing in the texture.

We can render a shadow map into a texture with a simple little chunk of code like this:
	"// GLSL Fragment shader\n"
"varying vec3 shadowCoords;\n"
"void main(void) {\n"
" gl_FragColor = vec4(shadowCoords.z);\n"


glClearColor(1.0,1.0,1.0,0); /* set background color to white (farthest away) */


/* Read the rendered shadow-view distances into the shadow texture */
glCopyTexSubImage2D(GL_TEXTURE_2D, 0, 0,0, 0,0, texWid,texHt);
(Note: a framebuffer object could render directly to the texture, which would  be slightly faster than rendering to the screen and then copying.  This also lets you render directly into a DEPTH_COMPONENT texture, as shown in the example code.)

Instead of using the depth buffer, you can actually just use "GL_MAX" or "GL_MIN" blending mode to keep the highest (or lowest) brightnesses:

Now that we've built a shadow map, for each pixel we can look up the closest-lit distance:
shadow map values stretched over geometry
This is the shadow map, stretched across our geometry.  Note how the closest-lit geometry doesn't change along a light ray--shadow maps are 2D, although light is 3D.

We can compare the closest-lit value against our own distance from the light source:
geometry distance from light source
This is the distance from each piece of geometry from the light source.

If we just compare the distance of our geometry to the closest-lit geometry, we get this:
comparison between shadow map value and geometry
Note that this is actually pretty close--the big dark spots are where shadows really should be.  But where the surface should be lit, it's self-shadowing due to the low precision of our shadow map depths (note 8-bit color means there are just 256 planes of depth!).  This ugly self-shadowing is called "shadow acne", but luckily we can cure acne by adding a small tolerance to our depth comparison, like:
    if (shadowCoords.z > shadowMapPix.z + 1.0/256) lit = 0.0; /* we're in shadow */
If we do this, then we can distinguish light from shadow reliably:
Clean lighting by biasing shadow comparison

The final step is to fold the light/shadow determination into our lighting calculation.  Typically you just set the diffuse and specular contributions to zero for shadowed pixels.
Full phong lighting with shadows

The bottom line on shadow maps is that they're easy, and they generate real shadows.  You should use them in your programs!

The only big downside to shadow maps is that it's not easy to pick a good shadow map resolution.  Too big and you'll waste fillrate rasterizing the huge shadow map.  Too small, and shadows can jump around in an ugly way.

A related limitation is the bounding volume for the geometry--anything that gets clipped off when we render the shadow map won't actually cast shadows.  You can adjust what gets rendered using a "shadow matrix", which it's easiest to just pull back from OpenGL after setting up modelview to fit the geometry onto the shadow screen:
	float shadowmat[4][4]; // transforms world into shadow coords
You just need to run the world coordinates through the shadow matrix again in your shader, before testing to see if they're in shadow.  This approach even allows you to use a projection matrix, creating a "projective shadow map".  See the example code for the gory details.