Bump Mapping Using CG (3rd Edition)

By Søren Dreijer (Halloko)

Preface

So, you've heard so much about people doing bump mapping these days and seen the cool effect in games like Doom 3 and Half-Life and now you're sitting all alone with your sparkling new graphics card wondering if you could achieve the same effect in your own game?

Well, it turns out this is your lucky day. I recently read up on bump mapping myself and after hours and hours browsing through the Web I thought I'd finally found a bunch of great tutorials. It turned out, though, that these tutorials contained way too much unnecessary and irrelevant code which required one to be selective and to sort huge parts of the tutorials away until you got down to the real meat!
Therefore I decided to do a tutorial myself. I've tried to explain as much as possible in here but if you already know some of the basics feel free to just quickly browse through the theory section and dive straight into the code. The main purpose of this tutorial is to make it readable for as broad an audience as possible!

This tutorial is written using OpenGL and nVIDIA's CG shader language. The reason we're using CG is because of its huge similarity to the C language and its ease of use. Personally, I think using CG shaders to achieve e.g. lighting effects is way easier than to use pure OpenGL extensions.

Anyways, without further ado, let's get down to the point that really matters: What is bump mapping and how do we create it!

Disclaimer

The stone texture which has been used in this tutorial was taken from the bump mapping article at NeHe's, which is mentioned in reference [4]. The normal map for the texture was generated from a height map using my own height map to normal map converter.

What you need to know on beforehand

  • Have a basic understanding of how to set up an OpenGL window and how OpenGL works, including the modelview and projection matrices.

  • Basic 3D math, including an understanding of vectors and matrices.

Required software

  • nVIDIA's CG Toolkit, which you can get from here: http://developer.nvidia.com/object/cg_toolkit.html
    The CG Manual is supplied in the package and is a very good reference. It contains a great introduction to the language and the runtime and it's vital that you read the sections "Introduction to the Cg Language" and "Using the Cg Runtime Library" in the CG Manual before starting any CG coding!

  • The OglExt library, which makes using OpenGL extensions a piece of cake: http://www.julius.caesar.de/index.php/OglExt

What is Bump Mapping

Basically, bump mapping is the art of making a 2D texture look as if it's in 3D as shown in the two pictures below:


Original 2D Picture

Bump Mapped Picture

The terms "per-pixel lighting" and "bump mapping" go hand in hand since what we're basically doing when using bump mapping is to evaluate the current light intensity at any given pixel on the texture (also often referred to as a texel).

The Math Behind It All

Imagine an ordinary 2D texture. The surface of this texture is plain flat and therefore the normals of the texture surface go straight up, as shown in the picture below. That's why, when you step close to, say, a wooden box in a game, the box doesn't appear like it has any depth. That's the results we get when using boring 2D textures..
The solution to this problem, however, is to vary the light intensities across the texture as we render. Look at the screenshot to the right above, for instance, and imagine a light source positioned in the top left corner of the picture. As you can see, this causes the edges of the stones facing towards the light to appear brighter than the ones facing away (creating shadows on the ones facing away from the light). That's the look we're after; we want to be able to calculate the color intensity per pixel to give the texture a "fake" depth and to give the player the impression that he's dealing with real 3D stones.
In order to be able to calculate the color intensity at a given texel, we have to know the surface normal at any given texel. That's where the normal map comes in. A normal map holds information about the direction of the surface normal at each texel, as can be seen on the picture below (the curved surface drawn underneath is the actual surface which the normals try to emulate):

Below is a screenshot of an actual normal map (which probably looks a bit weird at first):

Notice the three axes I've drawn in the bottom left of the texture; the x-axis points to the right, the y-axis points upwards while the z-axis is pointing out of the screen. Now, bind the color red to the x-axis, green to the y-axis and blue to the z-axis. If you look closely enough, you can actually see that the edges facing towards the positive x-axis' direction are red, the ones facing towards the positive y-axis' direction are green and the ones pointing straight up from the surface are blue. Since the majority of textures have most of their surface normals pointing out of the texture (following the direction of the positive z-axis), that explains the bluish color which applies to almost all normal maps.

What we really want to do is to interpret the color at any given texel as the direction of the normal at that position. The direction of a normal is defined by three coordinates, [x, y, z] and all three coordinates range from [-1; 1]. However, in the texture above the RGB (red, green, blue) colors are clamped to the range [0;1] and we want to interpret the red value as x, the green value as y and the blue value as z. So, we have to convert the color values' range from [0;1] to [-1;1]. We'll refer to this process as decompressing the color values in the normal map. The equation to decompress a color value to an actual coordinate goes like this:
  x = 2.0 * (colorValue - 0.5)
Try plugging in the value 0 as colorValue-1.

Now that we have a way to read and interpret the directions of the normals at each texel from the normal map, we're ready to continue.

The lighting equation is an essential part when dealing with dynamic lighting. It goes like this:
  I = A + att*(D + S) , where
I is the final color intensity
A is the ambient light. That said, in order to emulate the real world and to avoid complete dark areas, we add some "default" light which is always applied to any given pixel.
att is the attenuation of the light. All light sources have some maximum range the light reaches and this distance controls how much we should fade the current pixel based on the center of the light source.
D is the diffuse color, which is the color that you actually see when the light hits an object.
S is the specular color, which is the reflective color of an object.

If you want a more detailed description of how lighting works or a more correct mathematical explanation of the lighting equation, have a look at reference [1], which is absolutely awesome!

To make it easier on us to begin with, we'll simplify the lighting equation a bit and reduce it to:   I = D
That said, all we'll deal with in the beginning will be the diffuse component which is defined as:
  I = Dl * Dm * clamp(L•N, 0, 1) , where
Dl is the diffuse color of the light.
Dm is the diffuse color of the material on the object.
L is the vector from the point on the surface to the light source (the light vector).
N is the normal of the surface at the current pixel.
L•N is the dot product between L and N

The illustration below (which has been kindly borrowed from www.delphi3d.net, see reference [1]) illustrates this concept very well:


According to the (simplified) lighting equation above we need three parameters to successfully calculate the light intensity at any given pixel.
Let's start with the dot product between L and N.
L can easily be found by subtracting the position of the current vertex from the light position. N is even easier since that's the vector we read directly from the normal map we discussed earlier.
Next up is Dm which is the material's color at the current texel. Here we just use the color that's on the original texture. For instance, if you take a look at the rock texture from earlier, Dm is likely to be in a grayish "stone" tone.
Dl is the color of the light itself, which is usually just white.

Ok, so far so good. We have covered almost all the theory behind basic bump mapping. One little thing is left though and which is very important to discuss. As an OpenGL programmer you probably already know about the modelview and projection matrices. You use those two to convert a vertex from object space to clip space. Object space is the coordinate system you're actually working with while coding, like when you position something using glTranslatef(10.0f, 0.0f, -20.0f). We need to convert all object space vertices into clip space in order to render it correctly on our monitor. This is done by multiplying the concatenated modelview and projection matrices by a vertex in object space (though this is done automatically by OpenGL when we aren't using shaders).
For a deeper discussion of this, check out reference [2] and [3].
Below is a screenshot of the relationship between the different spaces and how to convert between them.


You might be wondering what the tangent space is and why we need it if all we're doing is to specify vertices directly in object space.
The tangent (or texture) space is the space relative to the surface as shown in the following picture:


Think of the S tangent as the x-axis, the T tangent as the y-axis and the normal as the z-axis going out of the screen. Each vertex on a surface has its own tangent space as you can see on the picture (four vertices in total; one in each corner). Together, the three axes form a basis at that vertex and they define a coordinate space called tangent space (or texture space, if you want). If you put the axes into a matrix you'll have the TBN matrix (Tangent, Binormal, Normal), where Tangent is the S tangent and Binormal the T tangent:
(Tx, Bx, Nx)
(Ty, By, Ny)
(Tz, Bz, Nz)


If you multiply a point in tangent space by this matrix, you'll transform the point into object space. Check out reference [6] for a great explanation of tangent space.

To calculate the tangent basis at any given vertex, we use the equation presented in reference [6] (scroll down a bit). Without discussing the actual derivation of the equation (since it's done in reference [6]) I'm just going to present it, ready for use:

For those of you not very strong in matrix math, here's how we'll calculate T and B:

, where
is the vector from vertex1 to vertex2 (V2 - V1)
is the vector from vertex1 to vertex3 (V3 - V1)
= V2.texcoord.x - V1.texcoord.x
= V2.texcoord.y - V1.texcoord.y
= V3.texcoord.x - V1.texcoord.x
= V3.texcoord.y - V1.texcoord.y
and

As mentioned previously when discussing the normal map and the lighting equation, we want to take the dot product between any normal on the surface and the light vector. The surface normal exists in tangent space while the light vector exists in object space. Therefore, in order for the dot product to make sense, we need those two vectors to be in the same space and hence we have the two following options to go for:

  1. Transform the light vector into tangent space.

  2. Transform all surface normals on the normal map into object space.

It's probably quite clear that option 1 would be the more desirable of the two because we'll only have to do one vector conversion as opposed to converting all the normals of the normal map to object space. Since the TBN matrix converts from tangent space to object space and we want to perform the opposite (to convert the light vector from object space into tangent space) we need to use the inverse of the TBN matrix, which is shown below:

A word on optimization though. As reference [6] mentions, if we're only dealing with triangles we only need to calculate one inverse TBN matrix per triangle and not per vertex as we've mentioned earlier. However, it's important to stress that whenever you're rotating a triangle you have to recalculate the inverse TBN matrix for it. Translating a triangle doesn't require a recalculation, though.

So, all we have to do, is to multiply the inverse TBN matrix by the light vector and, voilá, we'll have the light vector in tangent space, ready for the dot product calculation.

Update 9-13-2007:
Of course, in our case it is possible to obtain the inverse TBN matrix by transposing the matrix since our matrix is made up of three orthongonal vectors (the T, B, and N vectors). Imagine, however, that you have an object, such as a sphere, with is modeled using a number of triangles. In this case, you do not want your normals to be orthogonal to the surface of the triangles, but rather orthogonal to the actual surface of the sphere. This means that the normals are vertex-based rather than triangle-based, and thus, we have to compute a TBN matrix for each vertex. In the following, we therefore stick to the general derivation as described above, but keep in mind that if your model is flat shaded (the normals are triangle-based) rather than smooth shaded (the normals are vertex-based) then you can compute the inverse TBN matrix simply by transposing it.

So, let's sum up the process of doing bump mapping:

  1. Calculate an inverse TBN matrix for all triangles (only has to be done at startup and after a rotation has occurred)

  2. Calculate the light vector and transform it from object space into tangent space.

  3. Read the normal vector from the normal map and decompress the range so it goes from [-1;1] instead of [0;1].

  4. Calculate the final diffuse color by taking the dot product between the light vector and the normal and multiplying it with the color of the light and the color of the surface's material.

  5. Repeat this process for all the pixels on a given surface.

What's CG

CG is nVIDIA's try at a shader language. A shader is a small program which you download to the graphics card's GPU. This makes the shader very powerful as it runs directly on the graphics card and can access such things as textures and matrices directly.

The CG Runtime

First off, there are two kinds of shaders: the vertex shader and the fragment (pixel) shader.
The vertex shader is called for every vertex we draw on the screen by using e.g. glVertex3f(). Imagine we draw the triangle below which takes three glVertex3f() calls. This will result in calling the (active) vertex shader three times. Then, when we draw the stuff between the three vertices, the blue fill, we interpolate across the triangle until we've drawn everything. For every pixel we interpolate on the triangle, the fragment shader is called.


Basically, the vertex shader takes care of transforming vertices into the space we want them. For instance, a vertex shader would be the perfect place to transform the light vector from object space into tangent space.
The fragment shader, on the other hand, is where the real magic takes place though. Since the fragment shader is called for every pixel we can use it to perform our bump mapping calculations on every pixel.

In order to implement CG successfully in your application we'll have to pull a few, but quite easy, strings first.
Without delving too deeply into the nitty-gritty's of the language (since you can, and should, read the great introduction in the CG Manual), I'm just going to quickly mention the steps required to set up a basic CG program.

First of all we're dealing with a CG context. A CG context is a container for multiple CG programs. A CG program can be either a vertex or fragment shader. When initializing the application, we're going to create the CG context, create two CG programs, one vertex program and one fragment program and bind them to the CG context.
Furthermore, we have something called a CG profile. Because graphics cards are different and they don't all support all of CG's shader features, a CG profile defines a set of the CG language which is available on the particular hardware platform. We need a CG profile for each shader, as vertex shaders have different features than fragment shaders.
Therefore, when we've created the CG context, we call a CG profile function for every CG program we want to create and it returns the best suitable profile for the computer we're running the application on. We then attach these profiles to the vertex and fragment programs. The cool thing about this is that we don't hardcode a specific profile. Instead, it's determined at runtime by the CG profile function and that way we get the best suitable profile for the current machine.. how neat *is* that :)

Another cool thing when dealing with CG shaders is that you can bind (enable) and disable the shaders just as you do with e.g. textures in OpenGL. This makes it possible to have as many shader programs as you want in your application (only one vertex and fragment shader enabled at the same time, though) and just switch between them as you please. Also, if you suddenly just want OpenGL to handle the rendering once again you can just disable all your active CG shaders and you're set.

One last thing worth mentioning is that you can either compile your shaders on beforehand, or at runtime. Doing it at runtime will benefit from a, possibly, updated version of the CG compiler (which can make the shader run ever smoother several years after the main program has been compiled). If you want a more elaborate discussion of the differences between the two kinds of shader files, you should check out the CG Manual supplied with the CG Toolkit.

The CG shaders

So, without further ado, let's get this monster up and rolling!
Let's start out with the shader programs themselves. First we have the vertex shader. It's responsible of converting the light vector from object space to tangent space. Furthermore, since our shader is replacing the standard OpenGL vertex handling, we'll have to manually transform the current vertex position (remember that the vertex shader is called for every vertex we draw) from object space to clip space. As mentioned earlier, this is simply done my multiplying the concatenated modelview and projection matrices with the vertex position.
Our vertex shader will look like the following:

void main(	in float4 position : POSITION,			// The position of the current vertex. This parameter is required by CG in a vertex shader!
		in float2 texCoords : TEXCOORD0, 		// To send the data to the shader we use glMultiTexCoord2fARB(GL_TEXTURE0_ARB, ...)
		in float3 vTangent : TEXCOORD1,			// To send the data to the shader we use glMultiTexCoord3fARB(GL_TEXTURE1_ARB, ...)
		in float3 vBinormal : TEXCOORD2,		// To send the data to the shader we use glMultiTexCoord3fARB(GL_TEXTURE2_ARB, ...)
		in float3 vNormal : TEXCOORD3,			// To send the data to the shader we use glMultiTexCoord3fARB(GL_TEXTURE3_ARB, ...)

		out float4 positionOUT : POSITION,		// Send the transformed vertex position on to the fragment shader
		out float2 texCoordsOUT : TEXCOORD0,		// Send the texture map's texcoords to the fragment shader
		out float2 normalCoordsOUT : TEXCOORD1,		// Send the normal map's texcoords to the fragment shader
		out float3 vLightVector : TEXCOORD2, 		// Send the transformed light vector to the fragment shader

		const uniform float4x4 modelViewProjMatrix,	// The concatenated modelview and projection matrix
		const uniform float3 vLightPosition) 		// The light sphere's position in object space
{
	// Calculate the light vector
	vLightVector = vLightPosition - position.xyz;

	// Transform the light vector from object space into tangent space
	float3x3 TBNMatrix = float3x3(vTangent, vBinormal, vNormal);
	vLightVector.xyz = mul(TBNMatrix, vLightVector);
	
	// Transform the current vertex from object space to clip space, since OpenGL isn't doing it for us
	// as long we're using a vertex shader
	positionOUT = mul(modelViewProjMatrix, position);
	
	// Send the texture map coords and normal map coords to the fragment shader
	texCoordsOUT = texCoords;
	normalCoordsOUT = texCoords;
}

The : character we use after some of the parameter names, tells CG that we want to bind that parameter to a specific OpenGL call. Like, ": POSITION", which will get the value of the glVertex3f() call and "baseCoords : TEXCOORD0" which will be bound to the value we set with glMultiTexCoord2fARB(GL_TEXTURE0_ARB, ...). ": POSITION" is called a binding semantic.
You might wonder why we're sending the texture coordinates and the normal map coordinates to the fragment shader separately! After all, they both hold the same value. However, in order to make the demo compatible with older graphics cards we have to send them down separately.
The uniform parameters are the ones that don't change a lot. For instance, neither the light position, nor the concatenated modelview and projection matrices change while we're rendering the four vertices which make up our quad. Therefore, before we begin drawing anything to the screen, we set those two uniform values as follows (in the main.cpp file):

// Set the "modelViewProjMatrix" parameter in the vertex shader to the current concatenated
// modelview and projection matrix
cgGLSetStateMatrixParameter(g_modelViewMatrix, CG_GL_MODELVIEW_PROJECTION_MATRIX, CG_GL_MATRIX_IDENTITY);

// Set the light position in the vertex shader
cgGLSetParameter3f(g_lightPosition, g_vLightPos.x, g_vLightPos.y, g_vLightPos.z);

Notice how we bind the out parameters in the shader to a binding semantic as well. That is to make sure we can read them directly from the fragment shader. As you will see shortly, vLightVector for instance is bound to TEXCOORD2 and is read in the fragment shader as a parameter bound to the semantic TEXCOORD2.

The vertex shader above should be quite straightforward if you understood the math discussion earlier.
Next up is the fragment shader:

void main(	in float4 colorIN : COLOR0,
		in float2 texCoords : TEXCOORD0,		// The texture map's texcoords
		in float2 normalCoords : TEXCOORD1,		// The normal map's texcoords
		in float3 vLightVector : TEXCOORD2,		// The transformed light vector (in tangent space)

		out float4 colorOUT : COLOR0,			// The final color of the current pixel

		uniform sampler2D baseTexture : TEXUNIT0,	// The whole rock texture map
		uniform sampler2D normalTexture : TEXUNIT1,	// The whole normal map
		uniform float3 fLightDiffuseColor)		// The diffuse color of the light source
{
	// We must remember to normalize the light vector as it's linearly interpolated across the surface,
	// which in turn means the length of the vector will change as we interpolate
	vLightVector = normalize(vLightVector);

	// Since the normals in the normal map are in the (color) range [0, 1] we need to uncompress them
	// to "real" normal (vector) directions.
	// Decompress vector ([0, 1] -> [-1, 1])
	float3 vNormal = 2.0f * (tex2D(normalTexture, normalCoords).rgb - 0.5f);
	
	// Calculate the diffuse component and store it as the final color in 'colorOUT'
	// The diffuse component is defined as: I = Dl * Dm * clamp(L•N, 0, 1)
	// saturate() works just like clamp() except that it implies a clamping between [0;1]
	colorOUT.rgb = fLightDiffuseColor * tex2D(baseTexture, texCoords).rgb * saturate(dot(vLightVector, vNormal));
}

There shouldn't be any problems understanding what this shader does either. Just compare it to the bump mapping theory discussed earlier and you should be fine.

That concludes the shader part. What's left is to make CG work with our application.

The Main Application

We'll start out with the necessary includes in order to implement CG. We assume the CG headers have been installed in the compiler's #include path in a subdirectory called CG. This is just like the OpenGL headers which are stored in a subdirectory called GL (we assume the OglExt library has been installed in either the project's folder or in the compiler's LIBRARY path as well!):

// Include the headers required by CG
#include <CG/cg.h>
#include <CG/cgGL.h>

// Include the library files needed for CG
// If you don't use Visual Studio (and therefore cannot use the #pragma directive), you should include them manually in the project's settings.
#pragma comment (lib, "cg.lib")
#pragma comment (lib, "cgGL.lib")

// 'OglExt.lib' is used to allow for easy implementation of OpenGL extensions
#pragma comment (lib, "OglExt.lib")

Next up is our InitCG() function which sets up CG and creates the CG shader programs:

BOOL InitCG()
{
	// Set up the function which gets called by CG if something goes wrong
	// We’ll define ‘CGErrorCallback’ in a second..
	cgSetErrorCallback(CGErrorCallback);

	// Create the CG context which will hold our shader programs
	g_context = cgCreateContext();

	// Find the best matching profile for the fragment shader
	g_fragmentProfile = cgGLGetLatestProfile(CG_GL_FRAGMENT);
	cgGLSetOptimalOptions(g_fragmentProfile);
	if (g_fragmentProfile == CG_PROFILE_UNKNOWN)
	{
		g_pLog.PrintLn("Unsupported Graphics Card! Could Not Find A Suitable Fragment Shader Profile!");
		return FALSE;
	}

	// Find the best matching profile for the vertex shader
	g_vertexProfile = cgGLGetLatestProfile(CG_GL_VERTEX);
	cgGLSetOptimalOptions(g_vertexProfile);
	if (g_vertexProfile == CG_PROFILE_UNKNOWN)
	{
		g_pLog.PrintLn("Unsupported Graphics Card! Could Not Find A Suitable Vertex Shader Profile!");
		return FALSE;
	}

	// Create the fragment program.
	// cgCreateProgramFromFile() takes the following parameters:
	// A CG context, the type of the CG file (GL_SOURCE specifies that we’re reading a shader which hasn’t been compiled yet), the filename of the CG 
	// shader, the shader profile we just created, the name of the start function in the shader.
	g_fragmentProgram = cgCreateProgramFromFile(g_context, CG_SOURCE, "FragmentShader.cg", g_fragmentProfile, "main", 0);
	if (!g_fragmentProgram)
		return FALSE;

	// Load the fragment program
	cgGLLoadProgram(g_fragmentProgram);

	// Create the vertex program
	g_vertexProgram = cgCreateProgramFromFile(g_context, CG_SOURCE, "VertexShader.cg", g_vertexProfile, "main", 0);
	if (!g_vertexProgram)
		return FALSE;

	// Load the vertex program
	cgGLLoadProgram(g_vertexProgram);

	// This calculates the TBN matrices for all triangles in the scene
	CalculateTBNMatrix(g_vQuad[0], g_vTexCoords[0], g_TBNMatrix[0]);
	CalculateTBNMatrix(g_vQuad[1], g_vTexCoords[1], g_TBNMatrix[1]);

	// Get the parameters which we can pass to the vertex and fragment shaders.
	// Think of it like “main(int nFoo)”. What we do below is to fetch a reference to the parameters (like nFoo)
	// and we can then set their value from outside of the CG program. You’ll see the connection a bit later..
	g_modelViewMatrix = cgGetNamedParameter(g_vertexProgram, "modelViewProjMatrix");
	g_lightPosition = cgGetNamedParameter(g_vertexProgram, "vLightPosition");
	g_lightDiffuseColor = cgGetNamedParameter(g_fragmentProgram, "fLightDiffuseColor");

	// Create the light sphere
	g_lightSphere = gluNewQuadric();

	return TRUE;
}

In the following, we've defined our quad as two triangles and have defined the corresponding texture coordinates. Notice that we're only using two TBN matrices! This is because, as we mentioned earlier, we're dealing with triangles and we only need one TBN matrix per triangle. Actually, since we know we're about to render a quad that has a completely plain surface we could have managed with only one TBN matrix, but let's keep things simple here:

// The coordinates for our quad (2 triangles of 3 vertices each)
CVector g_vQuad[2][3] = { { CVector(-10.0f, -10.0f, -40.0f), CVector(10.0f, -10.0f, -40.0f), CVector(10.0f, 10.0f, -40.0f) },
			  { CVector(-10.0f, 10.0f, -40.0f), CVector(-10.0f, -10.0f, -40.0f), CVector(10.0f, -10.0f, -40.0f) } };

// The texture coordinates for the 2 triangles
CVector2 g_vTexCoords[2][3] = { { CVector2(0.0f, 0.0f), CVector2(1.0f, 0.0f), CVector2(1.0f, 1.0f) }, 
				{ CVector2(0.0f, 1.0f), CVector2(0.0f, 0.0f), CVector2(1.0f, 0.0f) } };

// Two TBN-matrices (one for each triangle). Each matrix consists of 3 vectors
CVector g_TBNMatrix[2][3]; 

Following is our error callback function, which is called whenever something goes wrong in the CG shaders, like a compile error when we load and compile the shaders:

void CGErrorCallback()
{
	// Print the error message to the log:
	// cgGetErrorString() returns the error that has occurred.
	// cgGetLastListing() returns a more descriptive report of the error
	g_pLog.PrintLn("%s - %s", cgGetErrorString(cgGetError()), cgGetLastListing(g_context));
}

It's also important to remember to clean up when everyone's going home and we're turning off the lights:

void DestroyCG()
{
	// Destroy and free our light sphere
	if (g_lightSphere)
		gluDeleteQuadric(g_lightSphere);

	// Destroying the CG context automatically destroys all attached CG programs
	cgDestroyContext(g_context);
}

Before we can render anything, we need to calculate the TBN matrix for each triangle. The function CalculateTBNMatrix() does just that. As input it takes three parameters:
- The triangle for which we want to calculate the TBN matrix (remember we only need to calculate one per triangle)
- The texture coordinates for the given triangle
- The TBN matrix which we fill with the calculated data and return to the calling function

Below is CalculateTBNMatrix() shown in its entirety. Please note that this function has changed since the 2nd edition of this article:

void CalculateTBNMatrix(const CVector *pvTriangle, const CVector2 *pvTexCoords, CVector *pvTBNMatrix)
{
	// Calculate the tangent basis for each vertex of the triangle
	// UPDATE: In the 3rd edition of the accompanying article, the for-loop located here has 
	// been removed as it was redundant (the entire TBN matrix was calculated three times 
	// instead of just one).
	//
	// Please note, that this function relies on the fact that the input geometry are triangles
	// and the tangent basis for each vertex thus is identical!
	//
	// We use the first vertex of the triangle to calculate the TBN matrix, but we could just 
	// as well have used either of the other two. Try changing 'i' below to 1 or 2. The end 
	// result is the same.
	int i = 0;

	// Calculate the index to the right and left of the current index
	int nNextIndex = (i + 1) % 3;
	int nPrevIndex = (i + 2) % 3;

	// Calculate the vectors from the current vertex to the two other vertices in the triangle
	CVector v2v1 = pvTriangle[nNextIndex] - pvTriangle[i];
	CVector v3v1 = pvTriangle[nPrevIndex] - pvTriangle[i];

	// The equation presented in the article states that:
	// c2c1_T = V2.texcoord.x – V1.texcoord.x
	// c2c1_B = V2.texcoord.y – V1.texcoord.y
	// c3c1_T = V3.texcoord.x – V1.texcoord.x
	// c3c1_B = V3.texcoord.y – V1.texcoord.y

	// Calculate c2c1_T and c2c1_B
	float c2c1_T = pvTexCoords[nNextIndex].x - pvTexCoords[i].x;
	float c2c1_B = pvTexCoords[nNextIndex].y - pvTexCoords[i].y;

	// Calculate c3c1_T and c3c1_B
	float c3c1_T = pvTexCoords[nPrevIndex].x - pvTexCoords[i].x;
	float c3c1_B = pvTexCoords[nPrevIndex].y - pvTexCoords[i].y;

	float fDenominator = c2c1_T * c3c1_B - c3c1_T * c2c1_B;
	if (ROUNDOFF(fDenominator) == 0.0f)	
	{
		// We won't risk a divide by zero, so set the tangent matrix to the identity matrix
		pvTBNMatrix[0] = CVector(1.0f, 0.0f, 0.0f);
		pvTBNMatrix[1] = CVector(0.0f, 1.0f, 0.0f);
		pvTBNMatrix[2] = CVector(0.0f, 0.0f, 1.0f);
	}
	else
	{
		// Calculate the reciprocal value once and for all (to achieve speed)
		float fScale1 = 1.0f / fDenominator;

		// T and B are calculated just as the equation in the article states
		CVector T, B, N;
		T = CVector((c3c1_B * v2v1.x - c2c1_B * v3v1.x) * fScale1,
				(c3c1_B * v2v1.y - c2c1_B * v3v1.y) * fScale1,
				(c3c1_B * v2v1.z - c2c1_B * v3v1.z) * fScale1);

		B = CVector((-c3c1_T * v2v1.x + c2c1_T * v3v1.x) * fScale1,
				(-c3c1_T * v2v1.y + c2c1_T * v3v1.y) * fScale1,
				(-c3c1_T * v2v1.z + c2c1_T * v3v1.z) * fScale1);

		// The normal N is calculated as the cross product between T and B
		N = T.CrossProduct(B);

		// Calculate the reciprocal value once and for all (to achieve speed)
		float fScale2 = 1.0f / ((T.x * B.y * N.z - T.z * B.y * N.x) + 
					(B.x * N.y * T.z - B.z * N.y * T.x) + 
					(N.x * T.y * B.z - N.z * T.y * B.x));
		
		// Calculate the inverse if the TBN matrix using the formula described in the article.
		// We store the basis vectors directly in the provided TBN matrix: pvTBNMatrix
		pvTBNMatrix[0].x = B.CrossProduct(N).x * fScale2;
		pvTBNMatrix[0].y = -N.CrossProduct(T).x * fScale2;
		pvTBNMatrix[0].z = T.CrossProduct(B).x * fScale2;
		pvTBNMatrix[0].Normalize();

		pvTBNMatrix[1].x = -B.CrossProduct(N).y * fScale2;
		pvTBNMatrix[1].y = N.CrossProduct(T).y * fScale2;
		pvTBNMatrix[1].z = -T.CrossProduct(B).y * fScale2;
		pvTBNMatrix[1].Normalize();

		pvTBNMatrix[2].x = B.CrossProduct(N).z * fScale2;
		pvTBNMatrix[2].y = -N.CrossProduct(T).z * fScale2;
		pvTBNMatrix[2].z = T.CrossProduct(B).z * fScale2;
		pvTBNMatrix[2].Normalize();
	}
}

So, finally we've arrived at the actual function where all the magic takes place: the Render() function. Here the light is rendered, we set up multitexturing, we enable our shaders (both the vertex and fragment ones) and we call the function RenderQuad() which renders our quad in the middle of the screen:

void Render()
{
	glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
	glLoadIdentity();

	glPushAttrib(GL_CURRENT_BIT);

	// Render the white light source
	glColor3f(1.0f, 1.0f, 1.0f);
	glPushMatrix();
		// We want to make the light source go round in a circle. Here we just apply some basic trigonometry.
		// Please note we're not factoring in time in this calculation (which might result in a 
		// faster/slower moving circle on some computers)
		g_fRotAngle += (float)PI * 0.02f * g_fLightSpeed;
		g_vLightPos.x = cosf(g_fRotAngle) * 10.0f;
		g_vLightPos.y = sinf(g_fRotAngle) * 10.0f;

		// Position and render the light sphere
		glTranslatef(g_vLightPos.x, g_vLightPos.y, g_vLightPos.z);
		gluSphere(g_lightSphere, 1.0f, 20, 20);
	glPopMatrix();
	glPopAttrib();

	// Enable the vertex and fragment profiles and bind the vertex and fragment programs
	cgGLEnableProfile(g_vertexProfile);
	cgGLEnableProfile(g_fragmentProfile);

	cgGLBindProgram(g_vertexProgram);
	cgGLBindProgram(g_fragmentProgram);

	// Set the "modelViewProjMatrix" parameter in the vertex shader to the current concatenated
	// modelview and projection matrix
	cgGLSetStateMatrixParameter(g_modelViewMatrix, CG_GL_MODELVIEW_PROJECTION_MATRIX, CG_GL_MATRIX_IDENTITY);

	// Set the light position parameter in the vertex shader
	cgGLSetParameter3f(g_lightPosition, g_vLightPos.x, g_vLightPos.y, g_vLightPos.z);

	// Set the diffuse of the light in the fragment shader
	cgGLSetParameter3f(g_lightDiffuseColor, 1.0f, 1.0f, 1.0f);

	// Enable and bind the rock texture
	glActiveTextureARB(GL_TEXTURE0_ARB);
	glEnable(GL_TEXTURE_2D);
	glBindTexture(GL_TEXTURE_2D, g_uiRockTexture);

	// Enable and bind the normal map
	glActiveTextureARB(GL_TEXTURE1_ARB);
	glEnable(GL_TEXTURE_2D);
	glBindTexture(GL_TEXTURE_2D, g_uiNormalTexture);

	RenderQuad();

	// Disable the vertex and fragment profiles again
	cgGLDisableProfile(g_vertexProfile);
	cgGLDisableProfile(g_fragmentProfile);

	// Disable textures
	glActiveTextureARB(GL_TEXTURE0_ARB);
	glDisable(GL_TEXTURE_2D);

	glActiveTextureARB(GL_TEXTURE1_ARB);
	glDisable(GL_TEXTURE_2D);
}

The RenderQuad() function is shown below. You'll probably notice we've cheated a bit and are using GL_TRIANGLE_FAN to draw the two triangles instead of GL_TRIANGLE. That said, by using GL_TRIANGLE_FAN we only need to specify four vertices instead of having to render each triangle separately which would have required six vertices.
You should also notice that we specify the (inverted) TBN matrices through the glMultiTexCoord3fARB() calls.

void RenderQuad()
{
	glBegin(GL_TRIANGLE_FAN);
		// Set the texture coordinates for the rock texture and the normal map
		glMultiTexCoord2fARB(GL_TEXTURE0_ARB, g_vTexCoords[0][0].x, g_vTexCoords[0][0].y);

		// Specify the tangent matrix vectors, one by one, for the first triangle and send them to the vertex shader
		glMultiTexCoord3fARB(GL_TEXTURE1_ARB, g_TBNMatrix[0][0].x, g_TBNMatrix[0][0].y, g_TBNMatrix[0][0].z);
		glMultiTexCoord3fARB(GL_TEXTURE2_ARB, g_TBNMatrix[0][1].x, g_TBNMatrix[0][1].y, g_TBNMatrix[0][1].z);
		glMultiTexCoord3fARB(GL_TEXTURE3_ARB, g_TBNMatrix[0][2].x, g_TBNMatrix[0][2].y, g_TBNMatrix[0][2].z);

		// Draw the bottom left vertex
		glVertex3f(g_vQuad[0][0].x, g_vQuad[0][0].y, g_vQuad[0][0].z);

		// ------------------------------------------------------------ //

		// Set the texture coordinates for the rock texture and the normal map
		glMultiTexCoord2fARB(GL_TEXTURE0_ARB, g_vTexCoords[0][1].x, g_vTexCoords[0][1].y);

		// Draw the bottom right vertex
		glVertex3f(g_vQuad[0][1].x, g_vQuad[0][1].y, g_vQuad[0][1].z);

		// ------------------------------------------------------------ //

		// Set the texture coordinates for the rock texture and the normal map
		glMultiTexCoord2fARB(GL_TEXTURE0_ARB, g_vTexCoords[0][2].x, g_vTexCoords[0][2].y);

		// Draw the top right vertex
		glVertex3f(g_vQuad[0][2].x, g_vQuad[0][2].y, g_vQuad[0][2].z);

		// ------------------------------------------------------------ //

		// Set the texture coordinates for the rock texture and the normal map
		glMultiTexCoord2fARB(GL_TEXTURE0_ARB, g_vTexCoords[1][0].x, g_vTexCoords[1][0].y);

		// Specify the tangent matrix vectors, one by one, for the second triangle and send them to the vertex shader
		glMultiTexCoord3fARB(GL_TEXTURE1_ARB, g_TBNMatrix[1][0].x, g_TBNMatrix[1][0].y, g_TBNMatrix[1][0].z);
		glMultiTexCoord3fARB(GL_TEXTURE2_ARB, g_TBNMatrix[1][1].x, g_TBNMatrix[1][1].y, g_TBNMatrix[1][1].z);
		glMultiTexCoord3fARB(GL_TEXTURE3_ARB, g_TBNMatrix[1][2].x, g_TBNMatrix[1][2].y, g_TBNMatrix[1][2].z);

		// Draw the top left vertex
		glVertex3f(g_vQuad[1][0].x, g_vQuad[1][0].y, g_vQuad[1][0].z);
	glEnd();
}

And there we go! We've successfully implemented bump mapping using the shader language CG.

Conclusion

One could easily extend the tutorial's code to support the specular component. All you should do is to read up on the specular term in reference [1] and you should be able to implement it almost straight into the vertex and fragment shaders. If you feel like you don't have time for that, don't worry, as we're currently working on an attenuation tutorial as well.
It's important to notice that one could speed up the whole process by using normalization cubemaps, but the primary intend of this tutorial is to keep things simple compared to all the other tutorials. A discussion of normalization cubemaps might be included in a future article.

I hope you enjoyed the ride as much as I did! Feel free to drop me an e-mail if you need help or find something a bit unclear. Of course, suggestions and corrections to the tutorial are very welcome too :)

Make sure you download the demo supplied below to delve deeper into the actual code!

Acknowledgments

  • Huge thanks go out to my buddy Jakob Gath (Hel) for contributing with suggestions and corrections to the article and for discovering an error with the compilation of the provided demo.
  • Philipp Karbach ([PR]JazzD) for making me do the tutorial before he did himself and for pointing out potential errors with older graphics cards.
  • My brother and ProPuke for helping me getting the tutorial converted to HTML.
  • Thanks to McClaw at www.sulaco.co.za for providing valuable feedback and spreading the word about the article.

Demo

I'm a Visual Studio 2003 user and if you use another IDE all you need to do is to create an empty Windows project and manually add all .h and .cpp files to it.
Download the demo here and make sure you read the Readme.txt file.

The demo has been tested on Geforce3 - Geforce6 cards without any problems. However, if you run the demo and receive a "Profile Not Supported" error then your graphics card doesn't have the required shader features to run the demo!

EDIT:
It appears that some graphics cards, in the latest version of the demo, have difficulty handling the normalization of the light vector in the fragment shader. Therefore, I've provided the old shader files for the unfortunate ones here.
(Thanks go out to Jeroen Put for notifying me of the problem)

EDIT (28/03/06):
It was brought to my attention by Philippe Komma that the for-loop located in the CalculateTBNMatrix() function was redundant and caused the entire TBN matrix to be calculated each loop.
This has now been corrected and the loop has been completely removed.

EDIT (06/10/07):
Please note that the demo might not be compilable and/or linkable with newer versions of Visual Studio. If you want to play around with the code and you're unable to successfully compile and link the demo, I recommend getting rid of the two external dependencies (TextureMgr Library.lib and LogLibrary.lib) and use your own. I apologize for not linking dynamically to those libraries when I first created this article, but my codebase has changed so much since then that it will be a significant amount of work to make such a change.

References:

[1] - http://www.delphi3d.net/articles/viewarticle.php?article=phong.htm
[2] - http://www.paulsprojects.net/tutorials/simplebump/simplebump.html
[3] - http://developer.nvidia.com/object/mathematicsofperpixellighting.html
[4] - http://nehe.gamedev.net/data/articles/article.asp?article=20
[5] - http://www.gamedev.net/columns/hardcore/cgbumpmapping
[6] - http://www.blacksmith-studios.dk/projects/downloads/tangent_matrix_derivation.php


Return to articles overview

Copyright © Blacksmith Studios 2007