| CS 381 Fall 2012 > Lecture Notes for Thursday, October 25, 2012 |
See the lecture notes for Tuesday, October 23 for the “Cel Shading” subtopic.
Now we begin our fifth unit: Textures & Mapping Techniques.
Topics in this unit:
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.
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”.
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.
GL_TEXTURE_2D.
Remember that, since we bound our texture to this target,
this is really a reference to the named texture.GL_RGBA.GL_RGB
if your array is as above.
Make it GL_RGBA
if your array holds four-component colors.GLubyte values,
make this GL_UNSIGNED_BYTE.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]);
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);
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.
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”.
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.
ggchappell@alaska.edu