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:
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":
- Render the world from the point of view of the light source. Store distance as color.
- Copy the rendered view into a texture, the "shadow map".
- Render the world from the point of view of the camera. Look
up the distance to the first-lit object in the shadow map texture.
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.
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"
"}\n"
);
glUseProgramObjectARB(shadowProg);
glClearColor(1.0,1.0,1.0,0); /* set background color to white (farthest away) */
glClear(GL_COLOR_BUFFER_BIT + GL_DEPTH_BUFFER_BIT);
draw_the_world();
/* Read the rendered shadow-view distances into the shadow texture */
glBindTexture(GL_TEXTURE_2D,shadowTex);
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:
glBlendEquation(GL_MAX);
glBlendFunc(GL_ONE,GL_ONE);
glEnable(GL_BLEND);
glDisable(GL_DEPTH_TEST);
Now that we've built a shadow map, for each pixel we can look up the closest-lit distance:
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:
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:
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:
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.
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
glGetFloatv(GL_MODELVIEW_MATRIX,&shadowmat[0][0]);
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.