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;

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 );

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]+

		if ( side > 0 )
			object.pFaces[i].visible = true;
			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.

	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 );
	glStencilFunc( GL_NOTEQUAL, 0, 0xFFFFFFFFL );
	glStencilOp( GL_KEEP, GL_KEEP, GL_KEEP );
		glVertex3f(-0.1f, 0.1f,-0.10f);
		glVertex3f( 0.1f, 0.1f,-0.10f);
		glVertex3f( 0.1f,-0.1f,-0.10f);

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 );

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
	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[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

* 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 >