Shadows
Welcome to a fairly complex tutorial on shadow casting. The effect this demo creates is literally incredible. Shadows that stretch, bend and wrap around other objects and across walls. Everything in the scene can be moved around in 3D space using keys on the keyboard.
This tutorial takes a fairly different approach - It assumes you have a lot of OpenGL knowledge. You should already understand the stencil buffer, and basic OpenGL setup. If you need to brush up, go back and read the earlier tutorials. Functions such as CreateGLWindow and WinMain will NOT be explained in this tutorial. Additionally, some fundamental 3D math is assumed, so keep a good textbook handy! (I used my 1st year maths lecture notes from University - I knew they'd come in handy later on! :)
First we have the definition of INFINITY, which represents how far to extend the shadow volume polygons (this will be explained more later on). If you are using a larger or smaller coordinate system, adjust this value accordingly.
Next is the definition of the object structures.
The Point3f structure holds a coordinate in 3D space. This can be used for vertices or vectors.
1 2 3 4 5 | struct Point3f
{
GLfloat x, y, z;
};
|
The Plane structure holds the 4 values that form the equation of a plane. These planes will represent the faces of the object.
1 2 3 4 5 | struct Plane
{
GLfloat a, b, c, d;
};
|
The Face structure contains all the information necessary about a triangle to cast a shadow.
- The indices specified are from the object's array of vertices.
- The vertex normals are used to calculate the orientation of the face in 3D space, so you can determine which are facing the light source when casting the shadows.
- The plane equation describes the plane that this triangle lies in, in 3D space.
- The neighbour indices are indices into the array of faces in the object. This allows you to specify which face joins this face at each edge of the triangle.
- The visible parameter is used to specify whether the face is "visible" to the light source which is casting the shadows.
1 2 3 4 5 6 7 8 9 | struct Face
{
int vertexIndices[3];
Point3f normals[3];
Plane planeEquation;
int neighbourIndices[3];
bool visible;
};
|
Finally, the ShadowedObject structure contains all the vertices and faces in the object. The memory for each of the arrays is dynamically created when it is loaded.
1 2 3 4 5 6 7 8 | struct ShadowedObject
{
int nVertices;
Point3f *pVertices;
int nFaces;
Face *pFaces;
};
|
The readObject function is fairly self explanatory. It will fill in the given object structure with the values read from the file, allocating memory for the vertices and faces. It also initializes the neighbours to -1, which means there isn't one (yet). They will be calculated later.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 | bool readObject( const char *filename, ShadowedObject& object )
{
FILE *pInputFile;
int i;
pInputFile = fopen ( filename, "r" );
if ( pInputFile == NULL )
{
cerr << "Unable to open the object file: " << filename << endl;
return false ;
}
fscanf ( pInputFile, "%d" , &object.nVertices );
object.pVertices = new Point3f[object.nVertices];
for ( i = 0; i < object.nVertices; i++ )
{
fscanf ( pInputFile, "%f" , &object.pVertices[i].x );
fscanf ( pInputFile, "%f" , &object.pVertices[i].y );
fscanf ( pInputFile, "%f" , &object.pVertices[i].z );
}
fscanf ( pInputFile, "%d" , &object.nFaces );
object.pFaces = new Face[object.nFaces];
for ( i = 0; i < object.nFaces; i++ )
{
int j;
Face *pFace = &object.pFaces[i];
for ( j = 0; j < 3; j++ )
pFace->neighbourIndices[j] = -1;
for ( j = 0; j < 3; j++ )
{
fscanf ( pInputFile, "%d" , &pFace->vertexIndices[j] );
pFace->vertexIndices[j]--;
}
for ( j = 0; j < 3; j++ )
{
fscanf ( pInputFile, "%f" , &pFace->normals[j].x );
fscanf ( pInputFile, "%f" , &pFace->normals[j].y );
fscanf ( pInputFile, "%f" , &pFace->normals[j].z );
}
}
return true ;
}
|
Likewise, killObject is self-explanatory - just delete all those dynamically allocated arrays inside the object when you are done with them. Note that a line was added to KillGLWindow to call this function for the object in question.
1 2 3 4 5 6 7 8 9 10 | void killObject( ShadowedObject& object )
{
delete [] object.pFaces;
object.pFaces = NULL;
object.nFaces = 0;
delete [] object.pVertices;
object.pVertices = NULL;
object.nVertices = 0;
}
|
Now, with setConnectivity it starts to get interesting. This function is used to find out what neighbours there are to each face of the object given. Here's some pseudo code:
1 2 3 4 5 6 7 | for each face (A) in the object
for each edge in A
if we don't know this edges neighbour yet
for each face (B) in the object (except A)
for each edge in B
if A 's edge is the same as B' s edge, then they are neighbouring each other on that edge
set the neighbour property for each face A and B, then move onto next edge in A
|
The last two lines are accomplished with the following code. By finding the two vertices that mark the ends of an edge and comparing them, you can discover if it is the same edge. The part (edgeA+1)%3 gets a vertex next to the one you are considering. Then you check if the vertices match (the order may be different, hence the second case of the if statement).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | int vertA1 = pFaceA->vertexIndices[edgeA];
int vertA2 = pFaceA->vertexIndices[( edgeA+1 )%3];
int vertB1 = pFaceB->vertexIndices[edgeB];
int vertB2 = pFaceB->vertexIndices[( edgeB+1 )%3];
if (( vertA1 == vertB1 && vertA2 == vertB2 ) || ( vertA1 == vertB2 && vertA2 == vertB1 ))
{
pFaceA->neighbourIndices[edgeA] = faceB;
pFaceB->neighbourIndices[edgeB] = faceA;
edgeFound = true ;
break ;
}
|
Luckily, another easy function while you take a breath. drawObject renders each face one by one.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | void drawObject( const ShadowedObject& object )
{
glBegin( GL_TRIANGLES );
for ( int i = 0; i < object.nFaces; i++ )
{
const Face& face = object.pFaces[i];
for ( int j = 0; j < 3; j++ )
{
const Point3f& vertex = object.pVertices[face.vertexIndices[j]];
glNormal3f( face.normals[j].x, face.normals[j].y, face.normals[j].z );
glVertex3f( vertex.x, vertex.y, vertex.z );
}
}
glEnd();
}
|
Calculating the equation of a plane looks ugly, but it is just a simple mathematical formula that you grab from a textbook when you need it.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | void calculatePlane( const ShadowedObject& object, Face& face )
{
const Point3f& v1 = object.pVertices[face.vertexIndices[0]];
const Point3f& v2 = object.pVertices[face.vertexIndices[1]];
const Point3f& v3 = object.pVertices[face.vertexIndices[2]];
face.planeEquation.a = v1.y*(v2.z-v3.z) + v2.y*(v3.z-v1.z) + v3.y*(v1.z-v2.z);
face.planeEquation.b = v1.z*(v2.x-v3.x) + v2.z*(v3.x-v1.x) + v3.z*(v1.x-v2.x);
face.planeEquation.c = v1.x*(v2.y-v3.y) + v2.x*(v3.y-v1.y) + v3.x*(v1.y-v2.y);
face.planeEquation.d = -( v1.x*( v2.y*v3.z - v3.y*v2.z ) +
v2.x*(v3.y*v1.z - v1.y*v3.z) +
v3.x*(v1.y*v2.z - v2.y*v1.z) );
}
|
Have you caught your breath yet? Good, because you are about to learn how to cast a shadow! The castShadow function does all of the GL specifics, and passes it on to doShadowPass to render the shadow in two passes.
First up, we determine which surfaces are facing the light. We do this by seeing which side of the plane the light is on. This is done by substituting the light's position into the equation for the plane. If this is larger than 0, then it is in the same direction as the normal to the plane and visible by the light. If not, then it is not visible by the light. (Again, refer to a good Math textbook for a better explanation of geometry in 3D).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | void castShadow( ShadowedObject& object, GLfloat *lightPosition )
{
for ( int i = 0; i < object.nFaces; i++ )
{
const Plane& plane = object.pFaces[i].planeEquation;
GLfloat side = plane.a*lightPosition[0]+
plane.b*lightPosition[1]+
plane.c*lightPosition[2]+
plane.d;
if ( side > 0 )
object.pFaces[i].visible = true ;
else
object.pFaces[i].visible = false ;
}
|
The next section sets up the necessary OpenGL states for rendering the shadows.
First, we push all the attributes onto the stack that will be modified. This makes changing them back a lot easier.
Lighting is disabled because we will not be rendering to the color (output) buffer, just the stencil buffer. For the same reason, the color mask turns off all color components (so drawing a polygon won't get through to the output buffer).
Although depth testing is still used, we don't want the shadows to appear as solid objects in the depth buffer, so the depth mask prevents this from happening.
The stencil buffer is turned on as that is what is going to be used to draw the shadows into.
1 2 3 4 5 6 7 | glPushAttrib( GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_ENABLE_BIT | GL_POLYGON_BIT | GL_STENCIL_BUFFER_BIT );
glDisable( GL_LIGHTING );
glDepthMask( GL_FALSE );
glDepthFunc( GL_LEQUAL );
glEnable( GL_STENCIL_TEST );
glColorMask( GL_FALSE, GL_FALSE, GL_FALSE, GL_FALSE );
glStencilFunc( GL_ALWAYS, 1, 0xFFFFFFFFL );
|
Ok, now the shadows are actually rendered. We'll come back to that in a moment when we look at the doShadowPass function. They are rendered in two passes as you can see, one incrementing the stencil buffer with the front faces (casting the shadow), the second decrementing the stencil buffer with the backfaces ("turning off" the shadow between the object and any other surfaces).
1 2 3 4 5 6 7 8 | glFrontFace( GL_CCW );
glStencilOp( GL_KEEP, GL_KEEP, GL_INCR );
doShadowPass( object, lightPosition );
glFrontFace( GL_CW );
glStencilOp( GL_KEEP, GL_KEEP, GL_DECR );
doShadowPass( object, lightPosition );
|
To understand how the second pass works, my best advise is to comment it out and run the tutorial again. To save you the trouble, I have done it here:
 |
 |
Figure 1: First Pass |
Figure 2: Second Pass |
The final section of this function draws one blended rectangle over the whole screen, to cast a shadow. The darker you make this rectangle, the darker the shadows will be. So to change the properties of the shadow, change the glColor4f statement. Higher alpha will make it more black. Or you can make it red, green, purple, ...!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | glFrontFace( GL_CCW );
glColorMask( GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE );
glColor4f( 0.0f, 0.0f, 0.0f, 0.4f );
glEnable( GL_BLEND );
glBlendFunc( GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA );
glStencilFunc( GL_NOTEQUAL, 0, 0xFFFFFFFFL );
glStencilOp( GL_KEEP, GL_KEEP, GL_KEEP );
glPushMatrix();
glLoadIdentity();
glBegin( GL_TRIANGLE_STRIP );
glVertex3f(-0.1f, 0.1f,-0.10f);
glVertex3f(-0.1f,-0.1f,-0.10f);
glVertex3f( 0.1f, 0.1f,-0.10f);
glVertex3f( 0.1f,-0.1f,-0.10f);
glEnd();
glPopMatrix();
glPopAttrib();
}
|
Ok, the next part draws the shadowed quads. How does that work? What happens is that you go through every face, and if it is visible, then you check all of its edges. If at the edge, there is no neighbouring face, or the neighbouring face is not visible, the edge casts a shadow. If you think about the two cases clearly, then you'll see this is true. By drawing a quadrilateral (as two triangles) comprising of the points of the edge, and the edge projected backwards through the scene you get the shadow cast by it.
The brute force approach used here just draws to "infinity", and the shadow polygon is clipped against all the polygons it encounters. This causes piercing, which will stress the video hardware. For a high-performance modification to this algorithm, you should clip the polygon to the objects behind it. This is much trickier and has problems of its own, but if that's what you want to do, you should refer to this Gamasutra article.
The code to do all of that is not as tricky as it sounds. To start with, here is a snippet that loops through the objects. By the end of it, we have an edge, j, and its neighbouring face, specified by neighbourIndex.
1 2 3 4 5 6 7 8 9 10 11 12 | void doShadowPass( ShadowedObject& object, GLfloat *lightPosition )
{
for ( int i = 0; i < object.nFaces; i++ )
{
const Face& face = object.pFaces[i];
if ( face.visible )
{
for ( int j = 0; j < 3; j++ )
{
int neighbourIndex = face.neighbourIndices[j];
|
Next, check if there is a visible neighbouring face to this object. If not, then this edge casts a shadow.
1 2 3 | if ( neighbourIndex == -1 || object.pFaces[neighbourIndex].visible == false )
{
|
The next segment of code will retrieve the two vertices from the current edge, v1 and v2. Then, it calculates v3 and v4, which are projected along the vector between the light source and the first edge. They are scaled to INFINITY, which was set to a very large value.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | const Point3f& v1 = object.pVertices[face.vertexIndices[j]];
const Point3f& v2 = object.pVertices[face.vertexIndices[( j+1 )%3]];
Point3f v3, v4;
v3.x = ( v1.x-lightPosition[0] )*INFINITY;
v3.y = ( v1.y-lightPosition[1] )*INFINITY;
v3.z = ( v1.z-lightPosition[2] )*INFINITY;
v4.x = ( v2.x-lightPosition[0] )*INFINITY;
v4.y = ( v2.y-lightPosition[1] )*INFINITY;
v4.z = ( v2.z-lightPosition[2] )*INFINITY;
|
I think you'll understand the next section, it justs draws the quadrilateral defined by those four points:
1 2 3 4 5 6 7 8 9 10 11 12 |
glBegin( GL_TRIANGLE_STRIP );
glVertex3f( v1.x, v1.y, v1.z );
glVertex3f( v1.x+v3.x, v1.y+v3.y, v1.z+v3.z );
glVertex3f( v2.x, v2.y, v2.z );
glVertex3f( v2.x+v4.x, v2.y+v4.y, v2.z+v4.z );
glEnd();
}
}
}
}
}
|
With that, the shadow casting section is completed. But we are not finished yet! What about drawGLScene? Lets start with the simple bits: clearing the buffers, positioning the light source, and drawing a sphere:
1 2 3 4 5 6 7 8 9 10 11 12 13 | bool drawGLScene()
{
GLmatrix16f Minv;
GLvector4f wlp, lp;
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);
glLoadIdentity();
glTranslatef(0.0f, 0.0f, -20.0f);
glLightfv(GL_LIGHT1, GL_POSITION, LightPos);
glTranslatef(SpherePos[0], SpherePos[1], SpherePos[2]);
gluSphere(q, 1.5f, 32, 16);
|
Next, we have to calculate the light's position relative to the local coordinate system of the object. The comments explain each step in detail. Minv stores the object's transformation matrix, however it is done in reverse, and with negative arguments, so it is actually the inverse of the transformation matrix. Then lp is created as a copy of the light's position, and multiplied by the matrix. Thus, lp is the light's position in the object's coordinate system.
1 2 3 4 5 6 7 8 9 10 | glLoadIdentity();
glRotatef(-yrot, 0.0f, 1.0f, 0.0f);
glRotatef(-xrot, 1.0f, 0.0f, 0.0f);
glTranslatef(-ObjPos[0], -ObjPos[1], -ObjPos[2]);
glGetFloatv(GL_MODELVIEW_MATRIX,Minv);
lp[0] = LightPos[0];
lp[1] = LightPos[1];
lp[2] = LightPos[2];
lp[3] = LightPos[3];
VMatMult(Minv, lp);
|
Now, palm off some of the work to draw the room, and the object. Calling castShadow draws the shadow of the object.
1 2 3 4 5 6 7 8 | glLoadIdentity();
glTranslatef(0.0f, 0.0f, -20.0f);
DrawGLRoom();
glTranslatef(ObjPos[0], ObjPos[1], ObjPos[2]);
glRotatef(xrot, 1.0f, 0.0f, 0.0f);
glRotatef(yrot, 0.0f, 1.0f, 0.0f);
drawObject(obj);
castShadow(obj, lp);
|
The following few lines draw a little orange circle where the light is:
1 2 3 4 5 6 7 8 | glColor4f(0.7f, 0.4f, 0.0f, 1.0f);
glDisable(GL_LIGHTING);
glDepthMask(GL_FALSE);
glTranslatef(lp[0], lp[1], lp[2]);
gluSphere(q, 0.2f, 16, 8);
glEnable(GL_LIGHTING);
glDepthMask(GL_TRUE);
|
The last part updates the object's position and returns.
1 2 3 4 5 6 | xrot += xspeed;
yrot += yspeed;
glFlush();
return TRUE;
}
|
We did specify a DrawGLRoom function, and here it is - a bunch of rectangles to cast shadows against:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 | void DrawGLRoom()
{
glBegin(GL_QUADS);
glNormal3f(0.0f, 1.0f, 0.0f);
glVertex3f(-10.0f,-10.0f,-20.0f);
glVertex3f(-10.0f,-10.0f, 20.0f);
glVertex3f( 10.0f,-10.0f, 20.0f);
glVertex3f( 10.0f,-10.0f,-20.0f);
glNormal3f(0.0f,-1.0f, 0.0f);
glVertex3f(-10.0f, 10.0f, 20.0f);
glVertex3f(-10.0f, 10.0f,-20.0f);
glVertex3f( 10.0f, 10.0f,-20.0f);
glVertex3f( 10.0f, 10.0f, 20.0f);
glNormal3f(0.0f, 0.0f, 1.0f);
glVertex3f(-10.0f, 10.0f,-20.0f);
glVertex3f(-10.0f,-10.0f,-20.0f);
glVertex3f( 10.0f,-10.0f,-20.0f);
glVertex3f( 10.0f, 10.0f,-20.0f);
glNormal3f(0.0f, 0.0f,-1.0f);
glVertex3f( 10.0f, 10.0f, 20.0f);
glVertex3f( 10.0f,-10.0f, 20.0f);
glVertex3f(-10.0f,-10.0f, 20.0f);
glVertex3f(-10.0f, 10.0f, 20.0f);
glNormal3f(1.0f, 0.0f, 0.0f);
glVertex3f(-10.0f, 10.0f, 20.0f);
glVertex3f(-10.0f,-10.0f, 20.0f);
glVertex3f(-10.0f,-10.0f,-20.0f);
glVertex3f(-10.0f, 10.0f,-20.0f);
glNormal3f(-1.0f, 0.0f, 0.0f);
glVertex3f( 10.0f, 10.0f,-20.0f);
glVertex3f( 10.0f,-10.0f,-20.0f);
glVertex3f( 10.0f,-10.0f, 20.0f);
glVertex3f( 10.0f, 10.0f, 20.0f);
glEnd();
}
|
And before I forget, here is the VMatMult function which multiplies a vector by a matrix (get that Math textbook out again!):
1 2 3 4 5 6 7 8 9 10 11 12 | void VMatMult(GLmatrix16f M, GLvector4f v)
{
GLfloat res[4];
res[0]=M[ 0]*v[0]+M[ 4]*v[1]+M[ 8]*v[2]+M[12]*v[3];
res[1]=M[ 1]*v[0]+M[ 5]*v[1]+M[ 9]*v[2]+M[13]*v[3];
res[2]=M[ 2]*v[0]+M[ 6]*v[1]+M[10]*v[2]+M[14]*v[3];
res[3]=M[ 3]*v[0]+M[ 7]*v[1]+M[11]*v[2]+M[15]*v[3];
v[0]=res[0];
v[1]=res[1];
v[2]=res[2];
v[3]=res[3];
}
|
The function to load the object is simple, just calling readObject, and then setting up the connectivity and the plane equations for each face.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | int InitGLObjects()
{
if (!readObject( "Data/Object2.txt" , obj))
{
return FALSE;
}
setConnectivity(obj);
for ( int i=0;i < obj.nFaces;i++)
calculatePlane(obj, obj.pFaces[i]);
return TRUE;
}
|
Finally, KillGLObjects is a convenience function so that if you add more objects, you can add them in a central place.
1 2 3 4 | void KillGLObjects()
{
killObject( obj );
}
|
All of the other functions don't require any further explanantion. I have left out the standard NeHe tutorial code, as well as all of the variable definitions and the keyboard processing function. The commenting alone explains these sufficiently.
Some things to note about the tutorial:
- The sphere doesn't stop shadows being projected on the wall. In reality, the sphere should also be casting a shadow, so seeing the one on the wall won't matter, it's hidden. It's just there to see what happens on curved surfaces :)
- If you are noticing extremely slow frame rates, try switching to fullscreen mode, or setting your desktop colour depth to 32bpp.
- Arseny L. writes: If you are having problems with a TNT2 in Windowed mode, make sure your desktop color depth is not set to 16bit. In 16bit color mode, the stencil buffer is emulated, resulting in sluggish performance. There are no problems in 32bit mode (I have a TNT2 Ultra and I checked it).
I've got to admit this was a lengthy task to write out this tutorial. It gives you full appreciation for the work that Jeff puts in! I hope you enjoy it, and give a huge thanks to Banu who wrote the original code! IF there is anything that needs further explaining in here, you are welcome to contact me (Brett), at brettporter@yahoo.com.
* Randy Ridge adds: In order to see shadows on my particular card the near clipping plane needs to be set to 0.001f rather than 0.1f in the ReSizeGLScene( ) section of code. The code has been modified in this tutorial and should work on all cards!
Banu Octavian (Choko) & Brett Porter
Jeff Molofee (NeHe)
* DOWNLOAD Visual C++ Code For This Lesson.
* DOWNLOAD Borland C++ Builder 6 Code For This Lesson. ( Conversion by Christian Kindahl )
* DOWNLOAD Code Warrior 5.3 Code For This Lesson. ( Conversion by Scott Lupton )
* DOWNLOAD Delphi Code For This Lesson. ( Conversion by Michal Tucek )
* DOWNLOAD Dev C++ Code For This Lesson. ( Conversion by Dan )
* DOWNLOAD Euphoria Code For This Lesson. ( Conversion by Evan Marshall )
* DOWNLOAD JoGL Code For This Lesson. ( Conversion by Abdul Bezrati )
* DOWNLOAD KDE/QT Code For This Lesson. ( Conversion by Zsolt Hajdu )
* DOWNLOAD Linux Code For This Lesson. ( Conversion by Jeff Pound )
* DOWNLOAD LWJGL Code For This Lesson. ( Conversion by Mark Bernard )
* DOWNLOAD Mac OS X/Cocoa Code For This Lesson. ( Conversion by Bryan Blackburn )
* DOWNLOAD Visual Studio .NET Code For This Lesson. ( Conversion by Grant James )
< Lesson 26Lesson 28 >