CS 381 Fall 2012  >  Lecture Notes for Thursday, October 25, 2012

CS 381 Fall 2012
Lecture Notes for Thursday, October 25, 2012

See the lecture notes for Tuesday, October 23 for the “Cel Shading” subtopic.

Unit Overview—Textures & Mapping Techniques

Now we begin our fifth unit: Textures & Mapping Techniques.

Topics in this unit:

Introduction to Textures

Basics

A texture is an image that can be painted on a polygon—or some generalization of this idea. Each pixel in a texture is a texel.

Most techniques involving textures have the word “mapping” in them somewhere. I will use map to refer to the data set, i.e., the texture itself. I will use mapping to refer to the technique. For example, the act of painting a texture on the polygons making up a surface, is referred to as texture mapping.

In order to use a texture, we need two pieces of information.

The Texture
This begins as a 2-D (for now) array of color values (for now). It must be handed over to OpenGL, and, if shaders are used, communicated to the shaders.
Texture Coordinates
Each vertex gets texture coordinates. These specify the point in the texture image that the vertex corresponds to. Texture coordinates are vertex attributes, and so they are generally specified inside a glBegin-glEnd pair. They can also be specified in a vertex array.

There is also a texture transformation, which is very similar to the model/view and projection transformations, but is applied to texture coordinates.

Our initial discussion of textures will be organized as follows. We will first discuss the initialization phase: how to make a texture. Next, we discuss the display phase in the application: how to render using a texture in OpenGL. Finally, we discuss how textures are dealt with in GLSL shaders.

See usetextures.cpp for a C++ application that uses a texture. The application requires shaders. However, since we are now dealing with textures, the old shaders are not good enough. We will write texture-using shaders specifically for this new application, marking their filenames with “_tex”.

Making a Texture

There are three ways we might create an image for use as a texture.

The first, I will leave to you. We will discuss the second, eventually. For now, I will use a “quick and dirty” version of the third: creating an image from a string array. We will discuss more sophisticated methods.

In any case, making a texture is usually something you do in the initialization section of your program. Doing it during display can be slow. Further, if the texture does not change, then it only needs to be made once.

When it is turned over to OpenGL, a texture image should be stored in a 2-D array of color values, with each dimension being a power of \(2\). The values can be either RGB or RGBA. I usually store a color component as a GLubyte, in which case its value is in the range \([0,255]\).

[C++]

const int IMG_WIDTH = 8, IMG_HEIGHT = 8;
GLubyte teximage[IMG_HEIGHT][IMG_WIDTH][3];  // Texture temp storage
   // The image
   // 3rd subscript 0 = R, 1 = G, 2 = B

The last dimension above would be \(4\) if colors are stored as RGBA.

We send the texture to OpenGL using an interface based on the same ideas as that for a VBO. We first generate a texture name. This is an integer (GLuint) that identifies the texture to OpenGL. If we have more than one texture loaded, then we can specify which one to use, by its name. I generally store names in a global array.

[C++]

const int NUM_TEXTURES = 1;
GLuint texnames[NUM_TEXTURES];

To generate texture names, call glGenTextures, with the number of names required, and a pointer to the array.

[C++]

glGenTextures(NUM_TEXTURES, texnames);

Once we do this, the first texture name is stored in texnames[0], the second (if NUM_TEXTURES is greater than 1) in texnames[1], etc.

In most OpenGL commands, a texture is referred to by a target, which is a predefined OpenGL constant. For a 2-D texture image, the target is GL_TEXTURE_2D. In order to make sure that target refers to the proper texture, we bind the texture to the target, by calling glBindTexture with the target and the texture’s name.

[C++]

glBindTexture(GL_TEXTURE_2D, texnames[0]);

After doing the above, each time we use GL_TEXTURE_2D, we are referring to the texture whose name is stored in texnames[0]. This remains true until we bind a different texture.

At this point, we are ready to give the texture to OpenGL. Make sure the array holds the proper color data, and then call one of two functions: glTexImage2D or gluBuild2DMipmaps. (If you have been paying attention, then you can probably guess that the second function is more convenient to use, but we will look at both anyway.)

Funcion glTexImage2D has nine parameters. All are integers except the last, which is a pointer.

target
This should be GL_TEXTURE_2D. Remember that, since we bound our texture to this target, this is really a reference to the named texture.
level
This has to do with something called “mipmaps”, which we will discuss later. Make this zero, for now.
internalFormat
What information you want OpenGL to store for each texel. For now, you want a color; make this GL_RGBA.
width
The width of the texture image, in pixels (a power of \(2\), remember).
height
The height of the texture image, in pixels (again, a power of \(2\)).
border
This relates to a feature we are not using, which allows a single-color border to surround a texture image. Make this zero.
format
How each color in your array is stored. Make this GL_RGB if your array is as above. Make it GL_RGBA if your array holds four-component colors.
type
The type of the items in your array. For GLubyte values, make this GL_UNSIGNED_BYTE.
data
A pointer to your array: the address of the first item.

Example call:

[C++]

glTexImage2D(GL_TEXTURE_2D,  // target
    0,                       // level
    GL_RGBA,                 // internalFormat
    IMG_WIDTH, IMG_HEIGHT,   // width, height
    0,                       // border
    GL_RGB,                  // format
    GL_UNSIGNED_BYTE,        // type
    &teximage[0][0][0]);     // data

An alternate, and simpler, method is to use the GLU wrapper gluBuild2DMipmaps. We will discuss mipmaps later. For now, you may simply call this function, with the same parameters as glTexImage2D, except for level and border.

Example call:

[C++]

gluBuild2DMipmaps(GL_TEXTURE_2D,  // target
    GL_RGBA,                      // internalFormat
    IMG_WIDTH, IMG_HEIGHT,        // width, height
    GL_RGB,                       // format
    GL_UNSIGNED_BYTE,             // type
    &teximage[0][0][0]);          // data

Once this is done, the texture image is server-side data. OpenGL has stored a copy of the texture, and the information in the (client-side) array is no longer needed. Thus our array may be reused for another texture.

Lastly, we will want to send our textures to our GLSL shaders. This is done via a texture channel (a.k.a. texture unit). Texture channels are numbered: \(0\), \(1\), \(2\), etc. Generally, we send each texture over a different channel. To indicate which texture channel is to be used, just before binding a texture name, call glActiveTexture with the proper channel, specified by an OpenGL constant: GL_TEXTURE followed by a number.

[C++]

glActiveTexture(GL_TEXTURE0);  // Texture channel 0
glBindTexture(GL_TEXTURE_2D, texnames[0]);

Display Using a Texture

We need to send our texture(s) to the shaders. Shaders access a texture using a variable of “sampler” type; we discuss these in the next subsection. For now, we assume that for each texture, there is a uniform sampler variable.

The application only needs to send a sampler a single integer. This is the number of the texture channel over which the texture itself should be received. For example, suppose that our program object is prog1, and a shader has a uniform sampler variable named mytex0, which should receive a texture over channel 0. Then we do the following in our display function.

[C++]

GLint loc = glGetUniformLocationARB(prog1, "mytex0");
glUniform1iARB(loc, 0);  // 0 is texture channel

When we draw a textured polygon, we need to specify texture coordinates for each vertex. This is done, for each vertex, with glTexCoord*. For 2-D textures, we generally use glTexCoord2d.

[C++]

glBegin(GL_TRIANGLES);
   ...
   glNormal3d(1., 2., 1.);
   glTexCoord2d(0.5, 0.7);
   glVertex3d(2., 5., -3.);
   ...
glEnd();

We can modify texture coordinates using the texture transformation. Like vertices, texture coordinates are sent through the pipeline as 4-D vectors in homogeneous form. Thus, the texture transformation is performed just like the model/view and projection transformations: by multiplication by a \(4\times 4\) matrix, with the 4th-coordinate division being necessary to get a useable 3-D (or 2-D) vector.

To set the texture transformation, use GL_TEXTURE as the matrix mode. Be sure to go back to model/view mode when you are done. Also, a texture transformation is sent to shaders over a texture channel; this can be the same channel as a texture image. Be sure to set the channel before moving to texture-matrix mode.

[C++]

glActiveTexture(GL_TEXTURE0);
glMatrixMode(GL_TEXTURE);
glLoadIdentity();
glRotated(20., 0., 0., 1.);  // 2-D, so z-axis rotation
glMatrixMode(GL_MODELVIEW);

Using Textures in Shaders

Texture coordinates are given to the vertex shader in gl_MultiTexCoord0, which is an attribute vec4 in homogeneous form. The texture transformation is given in gl_TextureMatrix[channel], which is a uniform mat4, with channel being the texture channel number.

2-D texture coordinates can be sent to the fragment shader in a varying vec2. Be sure to convert correctly from homogeneous form.

[GLSL vertex shader]

// Global variable
varying vec2 mytexcoord;

// Inside some function
vec4 mytexcoord4 = gl_TextureMatrix[0] * gl_MultiTexCoord0;
mytexcoord = mytexcoord4.st / mytexcoord.q;

Above, the “.st” and “.q” are using the convention that texture coordinates are referred to as \(s\), \(t\), \(p\), \(q\). Using “.xy” and “.w” would have the same effect.

Note: The traditional letter for the third texture coordinate is actually \(r\), not \(p\). However, when GLSL was designed, “.r” was already used for “red”, and so “.p” was used instead.

In GLSL, a sampler variable allows for resolution-independent access to an image. A 2-D texture should be accessed using a variable of type sampler2D, which should be uniform. Usually, we only need such a variable in our fragment shader. Associated with each sampler type is a look-up function. For 2-D samplers, the function is texture2D. This takes a sampler and a vec2 and returns the color in a vec4.

Putting all this together:

[GLSL fragment shader]

// Global variables
varying vec2 mytexcoord;
uniform sampler2D mytex0;

// Inside some function
vec4 texturecolor = texture2D(mytex0, mytexcoord);

See basic_tex_shaders.zip for examples of shaders that use textures. These shaders are intended for use with usetextures.cpp.

Texture Parameters

In our application, when a texture name has been bound to a target, we can set parameters for the texture. The parameters we looked at, all have to do with the operation of the texture look-up (e.g., texture2D in GLSL); in other words, they modify the functionality of a sampler.

Texture parameters are set by the glTexParameter* command. Usually, we call glTexParameteri. This takes the texture target (e.g., GL_TEXTURE_2D), the parameter name (e.g., GL_TEXTURE_WRAP_S), and the new value for the parameter.

The GL_TEXTURE_WRAP_S parameter determines what happens when the s texture coordinate is outside the range [0, 1]. The value GL_REPEAT replaces the coordinate with its fractional part. For example, \(2.4\) would become \(0.4\), and \(-1.3\) would become \(0.7\). The value GL_CLAMP replaces the coordinate with either \(0\) or \(1\), whichever is nearer. So \(2.4\) would become \(1\), and \(-1.3\) would become \(0\).

Parameters GL_TEXTURE_WRAP_T and (for 3-D textures) GL_TEXTURE_WRAP_R are similar.

Note: GL_TEXTURE_WRAP_R because the traditional letter for the third texture coordinate is \(r\).

Parameter GL_TEXTURE_MAG_FILTER determines what happens when a texel is significantly larger than a framebuffer pixel. Value GL_NEAREST makes the look-up function return the color of the nearest texel. Value GL_LINEAR makes it return a weighted average of the 4 nearest texel colors.

Parameter GL_TEXTURE_MIN_FILTER determines what happens when a texel is significantly smaller than a framebuffer pixel. Values GL_NEAREST and GL_LINEAR are available for this parameter. However, more interesting values are also available; these involve something called “mipmaps”.

Mipmaps

When a texel is very much smaller than a pixel, we generally get improved (faster, better looking) results by doing our texture look-up with a reduced-size version of the texture image. Collections of versions of a texture image, of varying sizes, are called mipmaps. (“mip” stands for the Latin phrase multum in parvo, which means something like “much in a small space”.)

OpenGL mipmaps always include the full-size image, and then half-size, quarter-size, etc., down to \(1\times 1\) pixel. The full-size image is level 0, the half-size is level 1, and so on.

If we use glTexImage2D, then the level is the second parameter. If we want to use mipmaps, then we generate multiple images, and make multiple calls to glTexImage2D, each with a different level.

Mipmaps can also be generated automatically. There are two ways to do this. The first method is to replace the call to glTexImage2D with gluBuild2DMipmaps (remove the level and border parameters), and pass only the full-size image. The second method is, after passing in the full-size image using glTexImage2D, to call glGenerateMipmapEXT, passing the texture target. This function is an OpenGL extension.

Once we have mipmaps, we need to determine which version of our texture image to use. There are two possibilities: NEAREST chooses the best-fit texture-image size, while LINEAR does a look-up on the two closest texture images, and returns a weighted average of the results. Thus, four new values for the GL_TEXTURE_MIN_FILTER parameter become available: GL_NEAREST_MIPMAP_NEAREST, GL_NEAREST_MIPMAP_LINEAR, GL_LINEAR_MIPMAP_NEAREST, GL_LINEAR_MIPMAP_LINEAR.


CS 381 Fall 2012: Lecture Notes for Thursday, October 25, 2012 / Updated: 25 Oct 2012 / Glenn G. Chappell / ggchappell@alaska.edu