52926.fb2 NeHes OpenGL Tutorials - читать онлайн бесплатно полную версию книги . Страница 27

NeHes OpenGL Tutorials - читать онлайн бесплатно полную версию книги . Страница 27

Lesson 27

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.

// 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.

• 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.

// 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:

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, …!

 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:

• 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.

Banu Cosmin (Choko) & Brett PorterJeff Molofee (NeHe)

* DOWNLOAD Visual C++ Code For This Lesson.

* DOWNLOAD Linux Code For This Lesson. (Conversion by Jay Groven