SHADOWS IN OPENGL Consider the problem of creating a shadow

SHADOWS IN OPENGL
JONATHAN R. SENNING
1. M ATHEMATICAL D EVELOPMENT
Consider the problem of creating a shadow cast by a single polygon onto the plane
given by ax + by + cz + d = 0. The problem really is one of finding out where the shadow
of each vertex of the polygon falls on the plane and then drawing the corresponding
polygon in the “shadow color” using the “shadow vertices”.
Before we begin, note that OpenGL supports both positional and directional light sources.
A positional light source (also called a point source) is located at a particular point and
light rays from it travel radially outward and so are all traveling in different directions. In
contrast, a directional light source is assumed to be located far enough from the scene so
that all of its rays are parallel to one another. Using homogeneous coordinates, the light
position vector is l = [lx ly lz lw ]T where lw = 1 for a positional light and lw = 0 for a
directional light.
Suppose the vertex s = [sx sy sz 1]T casts a shadow from light source l = [lx ly lz lw ]T
onto the plane P , given by ax + by + cz + d = 0. We call P the shadow projection plane. This
situation can be pictured as follows:
y
(lx , ly , lz )
(sx , sy , sz )
(x, y, z)
z
x
ax + by + cz + d = 0
If the light source is positional, then the point of intersection between the line through
l and s and the plane P is given by
(1)
p = s + α(l − s).
If, on the other hand, the light source is directional, then l is a vector rather than a point
and the line parallel to l through s intersects P at the point
(2)
p = s + αl.
Date: Written in 2004. Revised 2006.
1
In both cases, once we find the correct value of α we can compute p.
To begin, let n = [a b c d]T so we can write the plane equation ax + by + cz + d = 0 as
nT p = 0. If the light is positional then we use p from equation (1) to find
nT (s + α(l − s)) = 0
which, when solved for α, yields
α=
−nT s
.
nT l − nT s
Using this α in equation (1) we obtain
p = s + α(l − s)
= (1 − α)s + αl
=
nT l
nT s
s
−
l.
nT l − nT s
nT l − nT s
If we take β = nT l − nT s (this is a scalar quantity) then we can write
p0 = βp = (nT l)s − (nT s)l.
(3)
If, on the other hand, the light source is directional, then we use equation (2) and find
α is
nT s
α=− T .
n l
so that
p = s + αl
=s−
nT s
l.
nT l
In this case we take β = nT l and obtain
p0 = βp = (nT l)s − (nT s)l
(4)
which is the same as equation (3) except for the value of β.
If p0 = [x0 y 0 z 0 w0 ]T then the individual components of both equations (3) and (4) are
x0 = (alx + bly + clz + dlw )sx − (asx + bsy + csz + d)lx ,
y 0 = (alx + bly + clz + dlw )sy − (asx + bsy + csz + d)ly ,
z 0 = (alx + bly + clz + dlw )sz − (asx + bsy + csz + d)lz ,
w0 = (alx + bly + clz + dlw ) − (asx + bsy + csz + d)lw .
Canceling terms gives
x0 = (bly + clz + dlw )sx − blx sy − clx sz − dlx ,
y 0 = −aly sx + (alx + clz + dlw )sy − cly sz − dly ,
z 0 = −alz sx − blz sy + (alx + bly + dlw )sz − dlz ,
w0 = −alw sx − blw sy − clw sz + (alx + bly + clz + dlw ) − dlw .
2
Finally, if k = nT l = alx + bly + clz + dlw then we can write
x0 = (k − alx )sx − blx sy − clx sz − dlx
y 0 = −aly sx + (k − bly )sy − cly sz − dly
z 0 = −alz sx − blz sy + (k − clz )sz − dlz
w0 = −alw sx − blw sy − clw sz + (k − dlw )
which gives the concise matrix equation
 0  
x
(k − alx )
−blx
−clx
−dlx
 y 0   −aly
(k − bly )
−cly
−dly
 0 =
 z   −alz
−blz
(k − clz )
−dlz
0
w
−alw
−blw
−clw
(k − dlw )


sx
  sy 


  sz  .
1
Notice that w0 = k − (asx + bsy + csz + d)lw = nT l − nT slw . For both possible values of lw
(1 for a positional light source and 0 for a directional light source) we have w0 = β so that
when perspective division is carried out (done automatically in OpenGL) the point p0 is
transformed into the point p.
2. D RAWING S HADOWS IN O PEN GL
The function shadowTransform(float n[], float light[]) shown in Figure 1
can be used to modify the current matrix (normally the modelview matrix) so that all
subsequent drawing is projected onto the plane given by n[] with a center of projection
given by light[] (if light[3] = 1) or with a direction of projection given by light[]
(if light[3] = 0).
When drawing shadows we need to keep track of several things:
• The shadow projection transformation developed above will cause all objects to
be drawn to be projected on the indicated plane. Usually we will only draw the
object creating the shadow. We need to make sure, however, that the shadow only
appears on the portion of the plane corresponding to the polygon we want the
shadow to be cast on.
• In real situations there is usually enough ambient light or secondary light sources
so that the region in shadow is not completely dark. As a result, we do not want
to draw shadows in black, but in a gray that is blended with the preexisting color
on the shadowed surface.
• Since the object creating the shadow will most likely be composed of multiple
faces, some of which may be culled, we need to make sure that the blending done
consistently between the shadow color and the preexisting color.
To address the first concern we can use OpenGL’s stencil buffer. The stencil buffer can
be used in several ways. In our case, we want to use it to limit the shadow drawing to the
polygon(s) that the shadow falls on. The basic sequence of operations is
(1) enable stencil operation
(2) configure stencil buffer to record the positions of the frame buffer that are written
to by subsequent drawing operations
(3) draw the polygons in the shadow plane
(4) disable the depth buffer test
3
void shadowTransform( float n[], float light[] )
// This function updates the current matrix so that
// subsequently drawn objects are projected (using
// light[] as the center or direction of projection)
// onto the plane given by
//
//
n[0]*x + n[1]*y + n[2]*z + n[3] = 0.
//
// The location or direction of the light is given by
// the light[] vector. If the 4th component (light[3])
// is 1 (or other non-zero value), then the light is
// located at a particular point, the center of projection
// for the shadow. On the other hand, if the 4th component
// is 0 then the light is assumed to be a directional
// source and the light vector specifies the direction of
// the light.
{
float m[16];
const float k = n[0] * light[0] + n[1] * light[1]
+ n[2] * light[2] + n[3] * light[3];
for ( int i = 0; i < 4; i++ )
{
for ( int j = 0; j < 4; j++ )
{
m[4*i+j] = -n[i] * light[j];
}
m[5*i] += k; // add k to diagonal entries
}
glMultMatrixf( m );
}
F IGURE 1. Function to implement shadow projection transformation
(5) configure stencil buffer so that only positions in the frame buffer that where written to while the stencil buffer was enabled for writing can now be written to
(6) push the modelview matrix and compute the shadow transformation
(7) draw the object (this draws the shadow)
(8) pop the modelview matrix
(9) disable the stencil buffer, enable the depth buffer test
(10) draw the object (this draws the actual object)
4
The second concern can be handled with blending and the alpha values of the shadow
color. Lighting is disabled so we can set the shadow color with glColor*(). Blending
is enabled before the shadow is drawn; blending is disabled and lighting is enabled after
the shadow drawing is complete.
Finally, to make sure that each portion of the shadow is drawn only once, we once
again use the stencil buffer. In fact no additional work is required beyond configuring the
stencil buffer mode so that when the shadow is drawn the stencil buffer value is erased
so that subsequent attempts to write to the same location will be ignored.
Putting this all together gives OpenGL code like that shown in Figure 2.
E-mail address: [email protected]
D EPARTMENT OF M ATHEMATICS AND C OMPUTER S CIENCE , G ORDON C OLLEGE , 255 G RAPEVINE R OAD ,
W ENHAM MA, 01984
5
// Use the stencil buffer to make sure shadow is drawn
// only on the background. Configure stencil buffer
// so subsequent drawing sets stencil locations to 1.
glEnable( GL_STENCIL_TEST );
glStencilFunc( GL_ALWAYS, 0x1, 0xffffffff );
glStencilOp( GL_REPLACE, GL_REPLACE, GL_REPLACE );
// Draw polygons in the shadow plane.
draw_polygons_in_shadow_plane();
// Configure stencil buffer so that drawing only
// occurs where stencil is 1 and so that drawing
// a pixel resets corresponding stencil value to 0.
glDisable( GL_LIGHTING );
glDisable( GL_DEPTH_TEST );
glStencilFunc( GL_EQUAL, 0x1, 0xffffffff );
glStencilOp( GL_KEEP, GL_KEEP, GL_ZERO );
// Set up for blending and define the shadow color.
glEnable( GL_BLEND );
glBlendFunc( GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA );
glColor4f( 0.0, 0.0, 0.0, shadowAlpha );
// Draw the shadow by modifying the MODELVIEW matrix
// and then drawing the object that casts the shadow.
glPushMatrix();
shadowTransform( shadow_plane, light_position );
draw_object();
glPopMatrix();
glDisable( GL_BLEND );
glDisable( GL_STENCIL_TEST );
glEnable( GL_LIGHTING );
glEnable( GL_DEPTH_TEST );
// draw the object
draw_object();
F IGURE 2. Sequence of OpenGL calls to draw object with shadow
6