Texture Mapping


Highlights of this lab:

This lab is an introduction to Texture Mapping. It is organized by the following sections:

Assignment:

After the lab lecture, you have until next week to:

Online OpenGL Manual

More on texture mapping.


A. Pictorial Overview and Definitions

The following diagram represents the idea of texture mapping:

Some definitions:


B. Coding Overview

Download
For the following discussion you may want to download
the source for the checkerboard example.

The Steps in Texture Mapping are the following

  1. Create a texture name and bind it to the type of texture we will be creating
  2. Specify texture properties and the texture image
  3. Enable texture mapping
  4. Provide the mapping between texture coordinates and the object's coordinates.

B1. Texture names

To begin with, we need a texture name. This is actually a number that OpenGL uses to identify or index all of the textures. Any unsigned integer may be used as a texture name, but using glGenTextures() is recommended to avoid accidentally reusing names. glGenTextures() generates unused texture names. The code is the following:

    GLuint texName
    //allocate a texture name
    glGenTextures(1, &texName);

We now have a texture name (or unique index), now we want to specify what texture we are working with. There are two forms of textures in OpenGL, 1D and 2D. In this lab, we will be working with 2D, this is reflected in the GL_TEXTURE_2D in the following code:

    //select our current texture
    glBindTexture(GL_TEXTURE_2D, texName);

By issuing the glBindTexture() for the first time with texName, we are creating a texture object which is 2D and contains some initial default values or properties.

You will notice that we will call glBindTexture() again later with texName. When glBindTexture() is called with a previously created texture object, that texture object becomes active. Therefore, the next call to glBindTexture() will make texName the active or current texture state.

B2. Texture Properties and the Image

We've got a texture name and we've created a texture with the glBindTexture() command, now we can specify some properties or parameters for the texture. If we don't specify any parameters, the default values will be used.

The following may be some parameters that we want to set. The comments provide some idea as to what each of these calls is doing. For more details about these parameters, you might want to consult the manual for glTexParameter .

    //The texture wraps over the edges (repeats) in the x direction
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);

    //The texture wraps over the edges (repeats in the y direction
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);

    //when the texture area is large, repeat texel nearest center
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);

    //when the texture area is small, repeat texel nearest center
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);

Next, we can specify the image we are going to use. Assuming that we have an image (checkImage) which is a 2 dimensional checkerboard pattern which is stored in a three dimension array containing RGBA components. We can set the texture to the checkImage with the following code:

glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, checkImageWidth, checkImageHeight, 0, GL_RGBA, GL_UNSIGNED_BYTE, checkImage);

See the Online notes for glTexImage2D

B3. Enable Texture Mapping

We've set up the texture, we've specified some parameters, and we've provided a checkerboard image. Now, we need to turn on texture mapping. This is done with a call to glEnable sending it the GL_TEXTURE_2D parameter:

    glEnable (GL_TEXTURE_2D);

You also need to specify which texture you are going to be working with. You do that through a call to glBindTexture. Remember, the first time we called glBindTexture it created a texture object with default values for the texture image and texture properties. The second time we call it with the texture name, that texture data becomes the current texture state.

    glBindTexture(GL_TEXTURE_2D, texName);

B4. Provide the Mapping between Texture Coordinates and the Object's Coordinates.

This is the last key to texture mapping. We have to specify which part of our texture image are going to fit or map onto the object. Texture coordinates are often referred to as (s and t). For a texture image, s and t are in the range of 0 to 1. The mapping itself is similar to setting a color for each vertex. For texture mapping, you specify a texture coordinate and then you specify the coordinates for a vertex of the object. To map the checkerboard texture onto a square, our code would look like this:

    glBegin(GL_QUADS);
	glTexCoord2f( 0.0,  0.0); glVertex3f(-2.0, -1.0,  0.0);
	glTexCoord2f( 0.0,  1.0); glVertex3f(-2.0,  1.0,  0.0);
	glTexCoord2f( 1.0,  1.0); glVertex3f( 0.0,  1.0,  0.0);
	glTexCoord2f( 1.0,  0.0); glVertex3f( 0.0, -1.0,  0.0);
    glEnd();

Graphically, we could see the mapping as the following:

Download
For the preceding discussion you may want to download
the source for the checkerboard example.


C. Repeating a Texture Pattern or Clamping it

In the example that we are using, we have specified that we would like to repeat the pattern, but we are currently not making use of this repeating feature. When we repeat a texture, it provides a "tile" effect. The following code specifies that we will repeat the texture map.

    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
 

But in order to see any results, we have to assign texture coordinates outside the range [0,1]. For instance to get a 3x3 tile of the checkerboard, we would specify the coordinates as the following:

    glBegin(GL_QUADS);
	glTexCoord2f(0.0, 0.0); glVertex3f(-2.0, -1.0, 0.0);
	glTexCoord2f(0.0, 3.0); glVertex3f(-2.0, 1.0, 0.0);
	glTexCoord2f(3.0, 3.0); glVertex3f(0.0, 1.0, 0.0);
	glTexCoord2f(3.0, 0.0); glVertex3f(0.0, -1.0, 0.0);
    glEnd();

Try it out!

Graphically, we could see the mapping as the following (the red lines are placed in the texture map to help show that the texture is repeated three times)

Instead of specifying that we want to repeat the texture, we can specify that we want to clamp the texture. In this case, we change the GL_REPEAT to GL_CLAMP.

   glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP); 
   glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP); 

What does this mean? Any values greater than 1.0 are set to 1.0, and any values less than 0.0 are set to 0.0. For instance, if we use GL_CLAMP with the above texture coordinates, then a small copy of the texture will appear in the bottom left-hand corner of the object. To the right will be a "clamp" of the texture at s=1. To the top, will be a clamp of t=1.

Graphically, we could see the mapping as something like this (again, the red lines are in the image to point out the original texture in the bottom left-hand corner).


D. Working with Image Files

The checkerboard pattern in the above example was a simple black and white image that we stored into a three dimensional array. We may want to use more complicated pictures--for instance, images that we have taken or scanned.

There are several graphics file formats. OpenGL does not come with any image loading utilities, but if we have an image loading library, we can read from them and use them for our texture map. This lab uses FreeImage, an open source cross platform image loading library that supports a wide variety of image types. The library has been included in your exercise as a static library for your convenience.

FreeImage is easy to use, but to make it even easier it has been wrapped by the Image class found in . To use it, simply create an Image object and .open() an image by name. The object will store width (sizeX), height (sizeY) and pixel data (data) for the currently open image. You may use the same object to open any number of images one at a time. See the exercise for an example of its use. If you provided a palettized image, it will be converted to either 24 or 32-bit by the loader based on whether it has transparency.

One interesting quirk of FreeImage is that it stores data in BGR or BGRA byte order, rather than in the RGB or RGBA order that you might expect and that OpenGL uses internally. This byte order is preserved in the Image class's data member. Fortunately, OpenGL provides a compatible image format specifier. You shoule specify GL_BGR for 24-bit images, and GL_BGRA for 32-bit images.

Legacy: Loading .BMP files

If you prefer not to use a library, or would like to try writing your own texture loader, Windows .BMP format for 24-bit images is pretty easy to figure out since no de-compression or palette lookup tables are involved. This lab used to have a custom BMP loader, which you can see in this old version of this lab's exercise. The notes that follow can help you understand how it works.

Windows bitmap files are stored in a device-independent bitmap (DIB) format that allows Windows to display the bitmap on any type of display device. The term "device independent" means that the bitmap specifies pixel color in a form independent of the method used by a display to represent color. The default filename extension of a Windows DIB file is .BMP.
(From: http://www.dcs.ed.ac.uk/home/mxr/gfx/2d/BMP.txt)

You can summarize the bitmap as containing four chunks of information or data:

  1. BITMAPFILEHEADER
  2. BITMAPINFOHEADER
  3. Color table (doesn't apply for bitmaps with 24 color bits)
  4. Array of bytes that define the bitmap bits

The first bytes in the file contain the header information (1, 2, and 3 in the list above), then subsequent bytes contain the RGB components of each pixel in the image.

To summarize the header information, the first 14 bytes contain the BITMAPFILEHEADER, which is broken down into:
2 bytes type of file which must be BM
4 bytes size of the file
2 bytes reserved1 which must be 0
2 bytes reserved2 which must be 0
4 bytes byte offset from the BITMAPFILEHEADER structure to the actual bitmap data in the file

The next 40 bytes contain the BITMAPINFOHEADER, which is broken down into:
4 bytes number of bytes required by the BITMAPINFOHEADER
4 bytes width of the bitmap in pixels
4 bytes height of the bitmap in pixels
2 bytes number of planes for the target device (must be 1)
2 bytes number of bits per pixel. In our example, it must be 24.
4 bytes type of compression for a compressed bitmap
4 bytes size, in bytes of the image
4 bytes horizontal resolution, in pixels per meter
4 bytes vertical resolution, in pixels per meter
4 bytes number of color indexes in the color table actually used by the bitmap
4 bytes number of color indexes that are considered important for displaying the bitmap

Next is the color table, for our example, there is no color table because each pixel is represented by 24-bit red-green-blue (RGB) values in the actual bitmap data area.

Finally is the bitmap data area. It consist of a series of BYTE values representing consecutive rows, or "scan lines," of the bitmap. Each scan line consists of consecutive bytes representing the pixels in the scan line, in left-to-right order. The first byte represents the pixel in the lower left-hand corner of the bitmap.

I have explained all this to give you a little more understanding for the code in this lab's exercise. The key thing to know is that most of the header information for the bitmap is skipped. The height and the width of the bitmap are extracted to allow you to load images of varying sizes. Then, the RGB components are stored into a dynamically allocated array (data), which is part of the Image structure. Another thing to note is that .BMP files store their color information in Blue-Green-Red order. For OpenGL, the order is changed to Red-Green-Blue.

Please Note:

Although it would appear that your texture images can be of varying sizes, for maximum compatibility (see Issue #2) you should use images with both the width and height a power of 2. For instance, valid sizes would be: 64 x 16, 256 x 256, 128 x 128, and 16 x 4. Whereas invalid sizes would be: 100 x 100, 6 x 4, 5 x 5, and 2 x 22.

If you do have an odd size image, you can use the gluScaleImage() function in your code. Refer to the Online OpenGL manual.


E. References

The following are a list of references which were used in the making of this lab:


F. Exercise

Goals:

Click here for a zipped Visual Studio 2008 Project. You will have to unzip this file and double click on lesson06.sln.

Click here for a zipped XCode Project. You will have to unzip this file and double click on NeHe-Lesson-06.xcodeproj.

Click here for zipped source code and textures for this exercise. You will have to unzip this file and double click on lesson06.sln.

The code for this exercise is provided from http://nehe.gamedev.net (lesson 6). Modify the code by following these steps:

  1. First, build and run the code to see how it works. You should see a cube with a NEHE logo. To animate it, you can uncomment the
        glutTimerFunc(ms_delay, &animate, 1); 
    line in main(). Now hitting the 'a' key will toggle animation. You will also notice that when the window is redrawn (for instance, when it is re-sized), the cube will be rotated.
  2. Pick three of your favorite images and use an image editor (on Windows, under the Start menu and Microsoft Office Tools, you will find the Microsoft Photo Editor) to crop a portion of an image and save it as a Window's bitmap file (*.BMP). Save the three images with a different size: 512 x 512, 500 x 500, and 256 x 256. Store these files in the Data directory.

  3.    (3 marks--one mark for each file)

  4. Edit the code in lesson6.cpp so that you can switch between the three BMP files that you created.
  5. Compile and run the code. Try all three images. Do they all work? From the lab notes tell me whether they are guaranteed to work on all OpenGL implementations? Why wouldn't it?

  6.    (1 mark--for answering question)

  7. For the image that wouldn't work everywhere, use gluScaleImage() to scale it to 64x64.

  8.    (2 marks)

  9. Modify the code so that your image is tiled on the surface of the cube (similar to the image below). Save a copy of this code or the executable to show the instructor.
       (2 marks)

  10. Modify the code so that your image is in the middle of the cube with the "clamped" image along the edges (similar to the image below).
       (2 marks)

      (TOTAL   /10)

For a little more on texture mapping, click here.

Deliverables

For code components turn in uniquely named .cpp files, and one copy of your Data folder.