OpenGL Texture Mapping

2D Texture

2D Textures are rectangular arrays of data (e.g., images, patterns) that can be mapped onto the surfaces of a model. The mapped surface could be rectangular or non-rectangular. Each element of the texture array is called a texel (texture element) and can contain a RGB/RGBA value. 2D Texture Coordinates is denoted as (s, t), to differentiate from the (x, y, z) coordinates of the 3D model.

Loading the Texture Image/Pattern

The image or pattern to be used as texture must be provided to the OpenGL application, in the form of a 2D array of RGB/RGBA values. The following codes define the storage for the texture image data, and create a texture based on the image data via glTexImage2D.

#define IMAGE_ROWS 64
#define IMAGE_COLS 64
GLubyte imageData[IMAGE_ROWS][IMAGE_COLS][3];  // Texture image data array
......
// Need to populate the imageData array with pattern or image
......
glEnable(GL_TEXTURE_2D);  // Enable 2D Texture
// Create texture based on the imageData
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, IMAGE_COLS, IMAGE_ROWS, 0, GL_RGB, GL_UNSIGNED_BYTE, imageData);

Texture is a OpenGL state, and could be enabled/disabled via glEnable(GL_TEXTURE_2D) or glDisabled(GL_TEXTURE_2D).

The syntax for glTexImage2D is as follows:

void glTexImage2D(Glenum target, GLint level, GLint textureInternalFormat,
                  GLsizei imageWidth, GLsizei imageHeight, GLint imageBorder, GLenum imageFormat,
                  GLenum imageDataType, const GLvoid *imageData)
// target: Specify the texture target.
// level: level of details. OpenGL supports so-called mipmapping to use a sequence of images at different resolutions.
//       Level 0 is the highest resolution image.
// textureInternalFormat: Specifies the color format of the texture,
//       e.g., GL_RGB, GL_RGBA, GL_LUMINANCE.
// imageWidth, imageHeight, imageBorder: Width, height and border (0 or 1) of the image data.
// imageFormat: Format of the image data, e.g., GL_RGB, GL, RGBA, GL_LUMINANCE.
// imageDataType: image data type, e.g, GL_UNSIGNED_BYTE.
// imageData: Pointer to image data array.

Mapping Texels to Vertices

The command glTexCoord can be used to map a texel (of a texture) to a vertex (of the model). A 2D Texel is expressed in (s, t) coordinates, whereas the model is expressed in (x, y, z) coordinates.

void glTexCoord[1234][sifd](type s-coord, type t-coord,...)
void glTexCoord[1234][sifd]v(type *coords)

The texture coordinates (s, t) are normalized to the range of [0, 1], where (0, 0) is the lower-left corner and (1, 1) is the upper-right corner. For example,

glBegin(GL_QUADS);
   glTexCoord2f(0.0f, 0.0f);  // Lower-left corner of texture
   glVertex3f(......)
   glTexCoord2f(0.0f, 1.0f);  // Upper-left corner of texture
   glVertex3f(......)
   glTexCoord2f(1.0f, 0.0f);  // Lower-right corner of texture
   glVertex3f(......)
   glTexCoord2f(1.0f, 1.0f);  // Upper-right corner of texture
   glVertex3f(......)
glEnd();

Activity 1: A Checkerboard Texture Pattern

Here is a complete example (texture.cpp) that lays a checkerboard texture pattern over a rotating cube.

  1. Copy and paste the code on your machine and try it out. Pay attention to the texture details.
  2. Change one texture coordinate for the front face (from 1.0 to 0.0, for example) and observe the effect.
  3. Next set the same texture coordinate to a value outside the range [0,1] and observe the effect.

Texture Parameters

Additional texture parameters can be specified via glTexParameter command.

void glTexParameter[if](GLenum target, GLenum name, type value)
void glTexParameter[if]v(GLenum target, GLenum name, type *value)

Wrapping

The texture (s, t) coordinates is normalized to the range of [0, 1]. We can use the parameters GL_TEXTURE_WARP_S and GL_TEXTURE_WARP_T to specify the wrapping behavior for the s and t values outside the range. The possible values are GL_REPEAT and GL_CLAMP (clamped at 0.0 and 1.0). For example,

glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);  // Repeat the pattern
glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP);   // Clamped to 0.0 or 1.0
glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP);

Magnification & Minification Filters

When a texture is mapped to a surface, each texel may cover multiple pixels (called magnification), or multiple texels may cover one pixel (called minification). A scheme called point sampling, where each pixel is mapped to a point in the texture space using interpolation (GL_NEAREST), is simple and fast but can leading to unacceptable visual artifacts (such as aliasing). A smoother appearance can be obtained by using the average of a group of texels around the sampled point, i.e., linear filtering (GL_LINEAR). Another scheme called mipmapping will be discussed later.

In OpenGL, you can set the filter for magnification and minification independently.

// Nearest Point Sampling - fast but visual artifacts
glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
// 2x2 linear averaging - slower but smoother
glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);

In perspective projection, the fast texture interpolating scheme may not handle the distortion caused by the perspective projection. The following command can be used to ask the renderer to produce a better texture image, in the expense of processing speed.

glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST);

Using a Bitmap as Texture (Nehe Lesson #6)

To use an external image (BMP, JPEG, GIF, PNG) as texture, you need to find a routine that reads the external image into the texture's image data array, before you can use the image data to build the texture.

Nehe's solution uses GLAUX, which is no longer provided in the Windows SDK. The following source file (loadBMP.cpp) contains a simple routine

   void loadBMP(char *filename);
that reads a 24-bit uncompressed BMP image file into a texture image array (which must be declared in the calling program).

For example, the following codes uses the above routine to loads a BMP image and build a texture:

#include <malloc.h>
......
// Global Variables
unsigned char *imageData;
int imageRows = ...;  // height of your image
int imageCols = ...;  // width of your image
extern void loadBMP(char *);  // Declare external function prototype
......
// In initGL()
loadBMP("bmpdata/image.bmp");
glTexImage2D(GL_TEXTURE_2D, 0, 3, imageCols, imageRows, 0, GL_RGB, 
         GL_UNSIGNED_BYTE, imageData);
glEnable(GL_TEXTURE_2D);  // Enable 2D texture 
if(imageData) free(imageData); // Deallocate memory for texture data 
Do not worry, OpenGL still has the texture after you free the memory for your texture image. When calling glTexImage2D, the image data gets copied into the OpenGL system, and possibly into the the video card's texture memory.

Activity 2: Using BMP Images as Texture

In this activity you will lay a BMP image over your rotating cube.
  1. Download a 24-bit BMP image of your choice, or select one of the 24-bit BMP images posted here. If you choose do download your own image, make sure that your image size is a power of 2, and that the format of your image is 24-bit bmp (if not, open your image in Paint and convert it to 24-bit bmp format).
  2. Lay your BMP image over the rotating cube (use the code snippets above).

Mipmaps

At times, simple point sampling or local linear filtering does not yield acceptable visual result. For example, a far-away object in perspective projection will look strange. In this case, we need to use a value that is averaged over a larger area of the texture.

In mipmapping, we can create a series of texture image at different resolutions. For example, suppose the original image is 64x64 (Level 0), we can create lower resolution images at 32x32, 16x16, 8x8, 4x4, 2x2, 1x1. The highest resolution is refered to as level 0; the next is level 1; and so on.

We can use a single image and ask OpenGL to produce the lower-resolution images via command gluBuild2DMipmaps (in place of glTexImage2d).

int gluBuild2DMipmaps(GLenum target, GLint internalFormat,
   GLsizei width, GLsizei height, GLenum imageDataFormat, GLenum imageDataType, const void *imageData)

We can then specify the mipmapping filter is to be used via:

glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);   // MAG filter is linear
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_NEAREST);  // MIN filter is mipmap

Example (Nehe Lesson #7): Texture Filters

In this example, we create 3 textures with different filters (GL_NEAREST, GL_LINEAR, mipmap) for a rotating cube. We also program 'f' key to switch to the next filter.

The new pieces of code are:

#define NUM_TEXTURES 3  // Number of textures
   
// Global Variables
   
// Texture
GLuint textureIDs[NUM_TEXTURES]; // Array of texture IDs
GLuint currentTextureFilter = 0; // Currently-used texture filter
      
void initGL(GLvoid) {
   ......
   // Create 3 textures
   loadBMP(imageFilename);   // Load BMP image
   glGenTextures(NUM_TEXTURES, textureIDs); // Generate 3 texture IDs
   
   // Texture ID 0: Use nearest filter which point-samples the texture with interpolation
   glBindTexture(GL_TEXTURE_2D, textureIDs[0]);
   glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
   glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
   glTexImage2D(GL_TEXTURE_2D, 0, 3, imageCols, imageRows, 0, GL_RGB,
         GL_UNSIGNED_BYTE, imageData);
   
   // Texture ID 1: Use linear filter which average 2x2 texels for smoother appearance
   glBindTexture(GL_TEXTURE_2D, textureIDs[1]);
   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, 3, imageCols, imageRows, 0, GL_RGB,
         GL_UNSIGNED_BYTE, imageData);
   
   // Texture ID 2: Use mipmapping (for minification) which creates textures at lower resolutions
   glBindTexture(GL_TEXTURE_2D, textureIDs[2]);
   glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
   glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_NEAREST );
   gluBuild2DMipmaps(GL_TEXTURE_2D, 3, imageCols, imageRows, GL_RGB,
         GL_UNSIGNED_BYTE, imageData);
   
   glEnable(GL_TEXTURE_2D);  // Enable 2D texture
   glBindTexture(GL_TEXTURE_2D, textureIDs[currentTextureFilter]);  // Select the texture
   
   // Correct texture distortion in perpective projection
   glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST);
}
      
// Handler for key event
void keyboard(unsigned char key, int x, int y) {
   switch (key) {
      case 27:    // ESC key: exit the program
         exit(0); break;
      case 'f':   // 'f' key: Select the next texture filter
         currentTextureFilter++;
         if (currentTextureFilter >= NUM_TEXTURES) currentTextureFilter = 0;
         glBindTexture(GL_TEXTURE_2D, textureIDs[currentTextureFilter]);  // Select the texture
         break;
      default: break;
   }
}

Explanation:

In this example, 3 textures with different filter (GL_NEAREST, GL_LINEAR and mipmap) are created, using the same image data. We first use glGenTextures to generate 3 texture IDs (or names), and then use glBindTexture to choose a particular texture ID as the current texture. We program 'f' key to switch to the next texture ID. glTexImage2D is used for creating GL_NEAREST and GL_LINEAR filter, while gluBuild2DMipmaps is used for mipmap.

Activity 3: Switching Between 3 Texture Filters

  1. Incorporate the code snippets above into a complete program.
  2. Switch on the fly between different filters and observe the difference.
  3. Use glRasterPos2f and glutBitmapCharacter to display the current filter index in your window.

REFERENCES & RESOURCES