ShadowsWelcome 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. // Definition Of "INFINITY" For Calculating The Extension Vector For The Shadow Volume #define INFINITY 100 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. // Structure Describing A Vertex In An Object 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. // Structure Describing A Plane, In The Format: ax + by + cz + d = 0 struct Plane { GLfloat a, b, c, d; }; The Face structure contains all the information necessary about a triangle to cast a shadow.
// Structure Describing An Object's Face struct Face { int vertexIndices[3]; // Index Of Each Vertex Within An Object That Makes Up The Triangle Of This Face Point3f normals[3]; // Normals To Each Vertex Plane planeEquation; // Equation Of A Plane That Contains This Triangle int neighbourIndices[3]; // Index Of Each Face That Neighbours This One Within The Object bool visible; // Is The Face Visible By The Light? }; 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. struct ShadowedObject { int nVertices; Point3f *pVertices; // Will Be Dynamically Allocated int nFaces; Face *pFaces; // Will Be Dynamically Allocated }; 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. 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; } // Read Vertices 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 ); } // Read Faces 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; // No Neigbours Set Up Yet for ( j = 0; j < 3; j++ ) { fscanf( pInputFile, "%d", &pFace->vertexIndices[j] ); pFace->vertexIndices[j]--; // Files Specify Them With A 1 Array Base, But We Use A 0 Array Base } 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. 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: 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). int vertA1 = pFaceA->vertexIndices[edgeA]; int vertA2 = pFaceA->vertexIndices[( edgeA+1 )%3]; int vertB1 = pFaceB->vertexIndices[edgeB]; int vertB2 = pFaceB->vertexIndices[( edgeB+1 )%3]; // Check If They Are Neighbours - IE, The Edges Are The Same 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. // Draw An Object - Simply Draw Each Triangular Face. 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. void calculatePlane( const ShadowedObject& object, Face& face ) { // Get Shortened Names For The Vertices Of The 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). void castShadow( ShadowedObject& object, GLfloat *lightPosition ) { // Determine Which Faces Are Visible By The Light. 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. glPushAttrib( GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_ENABLE_BIT | GL_POLYGON_BIT | GL_STENCIL_BUFFER_BIT ); glDisable( GL_LIGHTING ); // Turn Off Lighting glDepthMask( GL_FALSE ); // Turn Off Writing To The Depth-Buffer glDepthFunc( GL_LEQUAL ); glEnable( GL_STENCIL_TEST ); // Turn On Stencil Buffer Testing glColorMask( GL_FALSE, GL_FALSE, GL_FALSE, GL_FALSE ); // Don't Draw Into The Colour Buffer 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). // First Pass. Increase Stencil Value In The Shadow glFrontFace( GL_CCW ); glStencilOp( GL_KEEP, GL_KEEP, GL_INCR ); doShadowPass( object, lightPosition ); // Second Pass. Decrease Stencil Value In The Shadow 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:
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, ...! glFrontFace( GL_CCW ); glColorMask( GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE ); // Enable Rendering To Colour Buffer For All Components // Draw A Shadowing Rectangle Covering The Entire Screen 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. void doShadowPass( ShadowedObject& object, GLfloat *lightPosition ) { for ( int i = 0; i < object.nFaces; i++ ) { const Face& face = object.pFaces[i]; if ( face.visible ) { // Go Through Each Edge 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. // If There Is No Neighbour, Or Its Neighbouring Face Is Not Visible, Then This Edge Casts A Shadow 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. // Get The Points On The Edge const Point3f& v1 = object.pVertices[face.vertexIndices[j]]; const Point3f& v2 = object.pVertices[face.vertexIndices[( j+1 )%3]]; // Calculate The Two Vertices In Distance 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: // Draw The Quadrilateral (As A Triangle Strip) 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: bool drawGLScene() { GLmatrix16f Minv; GLvector4f wlp, lp; // Clear Color Buffer, Depth Buffer, Stencil Buffer glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT); glLoadIdentity(); // Reset Modelview Matrix glTranslatef(0.0f, 0.0f, -20.0f); // Zoom Into Screen 20 Units glLightfv(GL_LIGHT1, GL_POSITION, LightPos); // Position Light1 glTranslatef(SpherePos[0], SpherePos[1], SpherePos[2]); // Position The Sphere gluSphere(q, 1.5f, 32, 16); // Draw A Sphere 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. glLoadIdentity(); // Reset Matrix glRotatef(-yrot, 0.0f, 1.0f, 0.0f); // Rotate By -yrot On Y Axis glRotatef(-xrot, 1.0f, 0.0f, 0.0f); // Rotate By -xrot On X Axis glTranslatef(-ObjPos[0], -ObjPos[1], -ObjPos[2]); // Move Negative On All Axis Based On ObjPos[] Values (X, Y, Z) glGetFloatv(GL_MODELVIEW_MATRIX,Minv); // Retrieve ModelView Matrix (Stores In Minv) lp[0] = LightPos[0]; // Store Light Position X In lp[0] lp[1] = LightPos[1]; // Store Light Position Y In lp[1] lp[2] = LightPos[2]; // Store Light Position Z In lp[2] lp[3] = LightPos[3]; // Store Light Direction In lp[3] VMatMult(Minv, lp); // We Store Rotated Light Vector In 'lp' Array Now, palm off some of the work to draw the room, and the object. Calling castShadow draws the shadow of the object. glLoadIdentity(); // Reset Modelview Matrix glTranslatef(0.0f, 0.0f, -20.0f); // Zoom Into The Screen 20 Units DrawGLRoom(); // Draw The Room glTranslatef(ObjPos[0], ObjPos[1], ObjPos[2]); // Position The Object glRotatef(xrot, 1.0f, 0.0f, 0.0f); // Spin It On The X Axis By xrot glRotatef(yrot, 0.0f, 1.0f, 0.0f); // Spin It On The Y Axis By yrot drawObject(obj); // Procedure For Drawing The Loaded Object castShadow(obj, lp); // Procedure For Casting The Shadow Based On The Silhouette The following few lines draw a little orange circle where the light is: glColor4f(0.7f, 0.4f, 0.0f, 1.0f); // Set Color To An Orange glDisable(GL_LIGHTING); // Disable Lighting glDepthMask(GL_FALSE); // Disable Depth Mask glTranslatef(lp[0], lp[1], lp[2]); // Translate To Light's Position // Notice We're Still In Local Coordinate System gluSphere(q, 0.2f, 16, 8); // Draw A Little Yellow Sphere (Represents Light) glEnable(GL_LIGHTING); // Enable Lighting glDepthMask(GL_TRUE); // Enable Depth Mask The last part updates the object's position and returns. xrot += xspeed; // Increase xrot By xspeed yrot += yspeed; // Increase yrot By yspeed glFlush(); // Flush The OpenGL Pipeline return TRUE; // Everything Went OK } We did specify a DrawGLRoom function, and here it is - a bunch of rectangles to cast shadows against: void DrawGLRoom() // Draw The Room (Box) { glBegin(GL_QUADS); // Begin Drawing Quads // Floor glNormal3f(0.0f, 1.0f, 0.0f); // Normal Pointing Up glVertex3f(-10.0f,-10.0f,-20.0f); // Back Left glVertex3f(-10.0f,-10.0f, 20.0f); // Front Left glVertex3f( 10.0f,-10.0f, 20.0f); // Front Right glVertex3f( 10.0f,-10.0f,-20.0f); // Back Right // Ceiling glNormal3f(0.0f,-1.0f, 0.0f); // Normal Point Down glVertex3f(-10.0f, 10.0f, 20.0f); // Front Left glVertex3f(-10.0f, 10.0f,-20.0f); // Back Left glVertex3f( 10.0f, 10.0f,-20.0f); // Back Right glVertex3f( 10.0f, 10.0f, 20.0f); // Front Right // Front Wall glNormal3f(0.0f, 0.0f, 1.0f); // Normal Pointing Away From Viewer glVertex3f(-10.0f, 10.0f,-20.0f); // Top Left glVertex3f(-10.0f,-10.0f,-20.0f); // Bottom Left glVertex3f( 10.0f,-10.0f,-20.0f); // Bottom Right glVertex3f( 10.0f, 10.0f,-20.0f); // Top Right // Back Wall glNormal3f(0.0f, 0.0f,-1.0f); // Normal Pointing Towards Viewer glVertex3f( 10.0f, 10.0f, 20.0f); // Top Right glVertex3f( 10.0f,-10.0f, 20.0f); // Bottom Right glVertex3f(-10.0f,-10.0f, 20.0f); // Bottom Left glVertex3f(-10.0f, 10.0f, 20.0f); // Top Left // Left Wall glNormal3f(1.0f, 0.0f, 0.0f); // Normal Pointing Right glVertex3f(-10.0f, 10.0f, 20.0f); // Top Front glVertex3f(-10.0f,-10.0f, 20.0f); // Bottom Front glVertex3f(-10.0f,-10.0f,-20.0f); // Bottom Back glVertex3f(-10.0f, 10.0f,-20.0f); // Top Back // Right Wall glNormal3f(-1.0f, 0.0f, 0.0f); // Normal Pointing Left glVertex3f( 10.0f, 10.0f,-20.0f); // Top Back glVertex3f( 10.0f,-10.0f,-20.0f); // Bottom Back glVertex3f( 10.0f,-10.0f, 20.0f); // Bottom Front glVertex3f( 10.0f, 10.0f, 20.0f); // Top Front glEnd(); // Done Drawing Quads } And before I forget, here is the VMatMult function which multiplies a vector by a matrix (get that Math textbook out again!): void VMatMult(GLmatrix16f M, GLvector4f v) { GLfloat res[4]; // Hold Calculated Results 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]; // Results Are Stored Back In v[] v[1]=res[1]; v[2]=res[2]; v[3]=res[3]; // Homogenous Coordinate } The function to load the object is simple, just calling readObject, and then setting up the connectivity and the plane equations for each face. int InitGLObjects() // Initialize Objects { if (!readObject("Data/Object2.txt", obj)) // Read Object2 Into obj { return FALSE; // If Failed Return False } setConnectivity(obj); // Set Face To Face Connectivity for ( int i=0;i < obj.nFaces;i++) // Loop Through All Object Faces calculatePlane(obj, obj.pFaces[i]); // Compute Plane Equations For All Faces return TRUE; // Return True } Finally, KillGLObjects is a convenience function so that if you add more objects, you can add them in a central place. 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:
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 )
|