Tutorial 4: Texture Mapping

One of the most powerful tools in computer graphics is texture mapping.  Texture mapping applies an image to a surface.  Modeling a complex surface is often impractical because of the detail required and it would be difficult to render this fine detail accurately.  Instead, texture mapping allows a simple polygon to appear to have a complex surface texture. 

For this tutorial you'll be working with some code I've created.  You'll find this code in tutorial4.zip

Note that my CTexture class that you will be using can read PPM and BMP files.  The criteria for an OpenGL texture is:

In this tutorial you will:

  1. Add new textures to the cube.
  2. Texture map a globe image onto a sphere.
  3. Texture map two linked hoops.

Running the Program

When you run the program, you'll see a cube.  Try the options in the Lab Stuff menu.  Note that the code includes a cube, sphere, and two hooked tori.  The cube has a texture on the front face.  You can open the file plank01.bmp to see the image that is being placed on the front of the cube.  Let's take a look at what is happening here.

Basic Image Management

First, look at the file Texture.cpp.  This is a class I have created for storing texture images.  It contains member functions for loading an image from a file.  You can also create your own images manually if you really want to.  An image is stored like this:

    BYTE ** m_image;

The array for the image is allocated like this:

BYTE *image = new BYTE[usewidth * m_height];
m_image = new BYTE *[m_height];
for(int i=0; i<m_height; i++, image += usewidth)
{
    m_image[i] = image;
}

Note that this LOOKS LIKE a 2D array.  The actual image is in a 1D array, so the image data is sequential.  This is important because OpenGL expects only a single pointer to all of the memory for your image.  Since we want to be able to access the memory as a 2D array, though, we create a simple array of pointers to the data for each row.

I'm loading this in using the Microsoft byte ordering of Blue, Green, Red (BGR).  So, a column is of data is 3 times the width of the image because there are three bytes per pixel.  Then the data is simply BGRBGRBGR sequentially for the first three pixels.  

Tell OpenGL about the texture

We have to do several things to enable this as an OpenGL texture.  The first of these is getting an integer that will serve as a texture identification, what OpenGL calls a texture name.  But, we need to understand the concept of a current OpenGL rendering context.  During execution of the function OnGLDraw, you can freely use OpenGL functions.  However, you can't use OpenGL functions at other places in your program.  As an example, you can't make any OpenGL calls in the constructor for CChildView.  When that constructor is run, OpenGL has not even been initialized.  When OnGLDraw is executed there is a current OpenGL rendering context, meaning OpenGL is active and ready to receive commands.  After the function ends, the context is turned back off.  

What this means in this application is that the calls to initialize the texture need to be done in OnGLDraw.  (There are other options, which we'll discuss later).  Since I want to do the LoadFile operation for CTexture in the constructor, I wanted an easier way to deal with initializing OpenGL when is okay to do so.

There is a member function CTexture::TexName(). This function returns the texture name.  However, the first time it is called, it will not have a texture name.  Instead, it will create one at this time and set up the texture for OpenGL.  Here's the code to do this (from Texture.cpp):

if(m_initialized)
    return m_texname;

glGenTextures(1, &m_texname);
glBindTexture(GL_TEXTURE_2D, m_texname);

glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, m_width, m_height, 0,
GL_BGR_EXT, GL_UNSIGNED_BYTE, m_image[0]);

m_initialized = true;

return m_texname;

The glGenTextures() call creates a texture name.  All this is really doing is ensuring you get a unique integer that's not been used as a texture ID before.  Then, I'm binding that name as the current 2D texture.  The following calls all operate on the current texture.  Since we bound our new ID, these calls work for our texture.

The first two glTexParameter calls tell OpenGL that I want to be able to "tile" my texture.  Tiling simply means that the texture will repeat if I go beyond its bounds.  Note that some textures work well for tiling and others do not.  You may have to work on a texture to get it tile nicely.  The S and T dimensions are the X,Y dimensions of a texture.  OpenGL uses other variables so you won't confuse S and T with X and Y.  

The next two glTexParameter calls tell OpenGL that we're going to use bilinear interpolation to determine what the color is between two pixels on a texture.  The other option is GL_NEAREST, which is often faster, but not as good in quality.  

Then, the glTexImage2D call tells OpenGL where to find the actual texture data.  Note that this is a reference, so don't delete the data until you're sure you're not going to draw it again.  You can look up the parameters for this function in MSDN.

Loading the Image

I found it easiest to add CTexture objects as members of CChildView and load the image in the CChildView constructor like this:

    m_wood.LoadFile("plank01.bmp");

This simply loads the file plank01.bmp into the object.  

Using the Image

Go to the function CChildView::Box().  This function draws the 6 quadrilaterals that make up the faces of the cube.  The first of these is texture mapped.  To texture map a primitive, so the following:

Enable the texture you want to use like this:

glEnable(GL_TEXTURE_2D);
glTexEnvf(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_MODULATE);
glBindTexture(GL_TEXTURE_2D, m_wood.TexName());

The first call enables texture mapping.  Be sure you disable it when you are done, or it will try to texture map everything else.  You'll see a glDisable(GL_TEXTURE_2D) after the quadrilateral is drawn.  The second line sets the texture environment mode to GL_MODULATE.  There are several ways textures can be combined with surface colors.  GL_REPLACE can be specified to just draw the surface using the texture colors only.  But, we are using lighting in this tutorial and want the surface intensity to vary depending on the lighting on the surface.  GL_MODULATE means the computed surface color is multiplied by the texture color.

NOTICE:  When you texture map a surface, the surface color you select will be multiplied by the texture color.  If the surface color is not white, you may get tinting of your textures.  Also, you'll not get the nice specular highlights when you apply textures in OpenGL 1.1.  (1.2 fixes this problem in some implementations).  

The last line simply binds the wood texture as the current 2D texture.  We can then utilize the texture.

Consider the texture like a rubber wallpaper.  You can pick points on the texture and attach them to vertices.  Texture coordinates range from 0 to 1.  If you enable GL_REPEAT (we have), you can access the texture beyond these coordinates and it will be repeated.  As an example, our wood texture has four corners:  (0,0), (1, 0), (1,1) and (0,1).  But, the image is repeated at (1,0), (2, 0), (2,1), (1, 1).  

We'll assign the corners of the texture to the corners of the image.  The code for this is:

glBegin(GL_QUADS);
   glNormal3d(0, 0, 1);
   glTexCoord2f(0, 0);
   glVertex3dv(a);
   glTexCoord2f(1, 0);
   glVertex3dv(b);
   glTexCoord2f(1, 1);
   glVertex3dv(c);
   glTexCoord2f(0, 1);
   glVertex3dv(d);
glEnd();

Note the glTexCoord2f calls to specify the texture coordinates.  These must be specified BEFORE the glVertex call.  Note the glDisable after all of this is done.

That's all there is to getting this image onto the cube front face.  Run it one more time to make sure you understand what is happening.

Let's Map the World onto the Side of the Cube

Now, we're going to map another side of the cube.  First add a new member variable to CChildView of type CTexture named m_worldmap.  You can do this by right-clicking CChildView in ClassView and selecting Add Member Function.  Then, we'll add the following to the CChildView constructor:

    m_worldmap.LoadFile("worldmap.bmp");

This file is in your local directory with the rest of the project.  

Now, we'll utilize it.  Move the glDisable before the second quad in the cube to after the quad.  Where it was, add the line:

glBindTexture(GL_TEXTURE_2D, m_worldmap.TexName());

Now, before each glVertex() call, we want to specify a texture coordinate.  If you look at how this face is defined, you'll see that it is drawn by specifying the vertices in a counter-clockwise order starting with the top left corner (vertex c).  We want the top left corner of the map to correspond to this location and then specify the coordinates in counter-clockwise around the map.  Note that you can reverse order and flip the map, do strange things with it, etc.  You might try some wrong orders just to see what happens.  When done, your code should look like this:

glBindTexture(GL_TEXTURE_2D, m_worldmap.TexName());

// Right
glBegin(GL_QUADS);
glNormal3d(1, 0, 0);
glTexCoord2f(0, 1);
glVertex3dv(c);
glTexCoord2f(0, 0);
glVertex3dv(b);
glTexCoord2f(1, 0);
glVertex3dv(f);
glTexCoord2f(1, 1);
glVertex3dv(g);
glEnd();

glDisable(GL_TEXTURE_2D);

Run this and see if it works right.

Tasks for you to do.

Now, please do these tasks on your own:

Mapping a Globe onto the Sphere

We have a sphere in this code.  Wouldn't it be neat to map the world onto that sphere?  But, what would it take to do this?

First, let's enable the texture.  Add the following code to the Sphere function before the SphereFace calls.  Then, all a glDisable(GL_TEXTURE_2D) call after those calls.

glEnable(GL_TEXTURE_2D);
glTexEnvf(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_MODULATE);
glBindTexture(GL_TEXTURE_2D, m_worldmap.TexName());

A question you may have:  Why am I using the glTexEnvf when I didn't for the map on the cube?  I knew there was no other code between faces of the cube that might change GL_TEXTURE_ENV_MODE.  But, I don't know if it may have been changed elsewhere.  Also, should I just call sphere without ever calling cube, the parameter would never be set.

The interesting problem for the sphere is to determine the texture coordinates for each vertex.  One way to look at this problem is to use the surface normal as way to tell where we are on the face of the sphere.  If we consider this a vector from the inside of the sphere, we can easily compute the latitude and longitude on the surface of the globe.  These values correspond to point on the map.

Right after the call to glBegin(GL_TRIANGLES), add the following code:

// What's the texture coordinate for this normal?
tx1 = atan2(a[0], a[2]) / (2. * GR_PI) + 0.5;
ty1 = asin(a[1]) / GR_PI + .5;

glTexCoord2f(tx1, ty1);

So, what does this do?  a[0] and a[2] are the X and Z values of the normal.  If you look straight down onto the globe from the top, the vector made up of the X and Z values will tell you the longitude on the globe!  I use atan2 to convert that to an angle in radians.  This angle is between -PI and PI.  I divide by 2PI, so it's now between -.5 and .5.  Adding 0.5 makes it range from 0 to 1.  This is the X value in the texture map.  

Next, I compute the Y value in the texture map.  a[1] is the Y value of the normal.  If you consider a right triangle with a hypotenuse of length 1 (our normalized vector) and a rise of Y, we can compute the angle using asin.  This is the angle between the Y vector and the X/Z plane.  This gives us values from -PI / 2 to PI / 2.  (up to 90 degrees up or down).  ty1 then ranges from 0 to 1.  

Add lines like this for the other two vertices.  I suggest not reusing tx1 and ty1, since we'll need to change something in a moment.

Run this and spin the globe.  You may notice that there's part of the globe that's messed up.  Try to take a moment and figure out what it is.  Then, read the following answer.

The problem is that some triangles will map to both ends of the map.  After all, the right edge of the map meets the left edge.  Imagine a triangle hanging off one edge.  The problem is that the trig functions simply wrap the value around.  So, you end of with a triangle the has two vertices on one edge of the map and one on the other edge.  This causes all of the map between these points to be smashed into the image mapped onto the polygon. 

So, how do we fix this?  The easiest solution is to check for this problem.  Try this for the second vertex:

// The second vertex
tx = atan2(b[0], b[2]) / (2. * GR_PI) + 0.5;
ty = asin(b[1]) / GR_PI + .5;
if(tx < 0.75 && tx1 > 0.75)
tx += 1.0;
else if(tx > 0.75 && tx1 < 0.75)
tx -= 1.0;

glTexCoord2f(tx, ty);

Do the same for the third vertex (based on vector c, of course), and you should have a nicely mapped globe.

An Exercise

Your task, now, is to texture map the two Tori.  Please use different textures for each of them.  Important requirement:  The texture should appear the same no matter what values you use for the torus steps.  I should be able to change a Torus call from Torus(5, 1, 50, 20) to Torus(5, 1, 30, 30) without changing the appearance of the texture.  You need to think about how to map a texture onto this surface and how you want to utilize tiling.  

Hint:  This is much easier than the sphere because the angles are already there for you.  Read the torus generator code carefully and get a good understanding of what the two angles for any vertex are.  It only takes a single line for each vertex to compute and set the values.