TexturingFrom all the features available in OpenGL, textures is the one that provides the most impact on realism. A few triangles can become a gloomy scenario by selecting the right texture maps, and visual appeal is simply multiplied. OpenGL implements textures on a per-primitive basis. You can specify which texture map should be applied to each triangle or quad, and how the texture should adapt to the object's geometry. Keep in mind, however, that OpenGL is not designed for textures to be applied manually. The best choice is to use a modeling package, apply textures with it, and then have OpenGL render those objects with the same texturing. But before we can explore the texturing capabilities of OpenGL, we will take a look at how textures are loaded and stored by the API. To begin with, texture data can come from any file format such as TGA, JPEG, or any other. There is no restriction on the source of the material. All you have to do is load the data and deliver it to OpenGL so the API can manipulate it. This is usually called the external format. Keep in mind that once the texture map is loaded, it does not have to remain in user memory. OpenGL will copy the map to its own memory area, and further operations are performed directly inside the API, with no direct control of the texture. Once in OpenGL's memory, texture data can be stored in a variety of ways: palletized, true-color, and so on. It also allows you to specify the texture map size or even to select the filtering algorithm to be applied. All these properties are called the internal format of the texture and determine how much memory each map will take. As an example, here is the source code that takes a single TGA file, loads it into user memory, and delivers it to OpenGL to be used as a texture map. To make the code shorter, I will assume the texture is 256x256, RGBA. This way I can skip decoding the TGA header: int LoadTexture(char *filename, int ident) { glBindTexture(GL_TEXTURE_2D,ident); glPixelStorei(GL_UNPACK_ALIGNMENT,1); 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); FILE *f; f=fopen(filename,"rb"); if (f==NULL) { // filename is wrong... probably file does not exist return -1; } char *rgb=new char[Width*Height*Depth]; fread(rgb,sizeof(unsigned char),18,f); // skip the header fread(rgb,256*256*3,1,f); glTexImage2D(GL_TEXTURE_2D,0,3,256,256,0,GL_RGB,GL_UNSIGNED_BYTE,rgb); delete[] rgb; return 0; } This is a significant amount of code, so let's go through it step by step. The call to glBindTexture tells OpenGL which texture map we will be working on. Texture maps are identified by nonnegative integers, so each texture map has a unique identifier. The glBindTexture is used whenever we want to work on a texture map, to initialize OpenGL's texture access routines. The next five lines set some useful texture properties. The first one (glPixelStore) sets the unpack alignment, so data is byte aligned. Then, we use four lines to set the filtering and tiling properties of the texture map. The following lines tell OpenGL we want this map to repeat endlessly over the object's surface: glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_WRAP_S,GL_REPEAT); glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_WRAP_T,GL_REPEAT); This way we can make copies of a small texture map over a large surface, as in a brick pattern covering a large wall. To achieve this, we will specify texture coordinates outside the standard (0,1) range to indicate repetition. An alternative would be to use GL_DECAL instead of GL_REPEAT. This would make the texture map appear only once, so any texture coordinate beyond the texture limit would not be painted. As for the filtering, the following lines indicate how the texture must be filtered when magnified or minified: glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MAG_FILTER,GL_LINEAR); glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MIN_FILTER,GL_LINEAR); In both cases, we indicate we want a bilinear filter to be applied, so texture maps look smooth when looked at from a distance or up close. Another possibility would be to use GL_NEAREST, which implements a nearest-neighbor policy. This is equivalent to switching off filtering capabilities. After the texturing options have been set, it is time to load the texture data to a temporary buffer. In this rather simplistic example, I assume texture size and color depth are known. I have chosen the popular TGA file format, which basically uses an 18-byte long header and an RGB dump of the texture data. Thus, I can skip the header and load the data into a large temporary buffer. Then, it is time to pass the texture to OpenGL so we can begin working with it. This is achieved by the glTexImage2D call, which has the following profile: void glTexImage2D(GLenum target, GLint level, GLint internalformat, GLint width, GLint height, GLint border, GLenum format, GLenum type, void *pixels); where target specifies the type of texture we are working with. Values can be GL_TEXTURE_2D for normal textures or GL_PROXY_TEXTURE_2D. The second option is used when we want to initialize an OpenGL texture map, but we don't want to fill it with data yet. Thus, all memory allocation and consistency checking is performed, but no data is read from pixels. The level parameter is used for mipmapping and specifies the level of detail number. For regular textures, the parameter should be set to 0, and a value of N is the Nth mipmap reduction. Mipmapping is an advanced filtering option and is explained fully in Chapter 18, "Texture Mapping." The third parameter is used to declare the internal format of the texture. It can be 1, 2, 3, or 4 to specify the number of components for each pixel. Thus, an RGB map would be 3. But you can achieve higher flexibility by using one of the following symbolic constants:
The next two parameters are straightforward and indicate the width and height (in pixels) of the texture map. Some graphics cards are limited to square textures, others require power-of-two sizes, and still others limit texture sizes to 256 in both directions. Unless you are sure about your graphics card, a 256x256 texture map is always a safe bet. Then, we can specify how many pixels the texture should have as a border. This is added to the size of the texture, but most of the time this parameter should simply be set to zero. To apply textures to OpenGL primitives we must specify their texture coordinates, which determine the orientation, sizing, and so on of the material on the primitive. Texture coordinates are specified in U,V values, which are interpreted as the position of the texture map corresponding to a given point in 3D space. Then, texture mapping stretches the map so that each vertex gets its corresponding texture coordinate, and those pixels in between get interpolated smoothly. Except for very simple demos, texture coordinates are meant to be taken from a modeling package because placing them by hand is a tedious process, especially in complex objects with elaborate texture designs. Texture coordinates are thus stated per vertex, right before the vertex is declared. There is no priority between calls to texturing and coloring routines, but they must take place before their corresponding vertex for OpenGL to work properly. Texture coordinates are specified with the call: glTexCoord*(....); This call sets the texture coordinates for the current vertex. It allows several syntax variants, although the most popular incarnation is void glTexCoord2f(float u, float v); Here we are passing the texture coordinates as float values. As a complete example, the following code shows the "right" way of rendering a textured quad, which stretches a single texture map to cover all its surface: glBindTexture(GL_TEXTURE_2D,materialid); glBegin(GL_QUADS); glColor3f(1,1,1); glTexCoord2f(0,0); glVertex3f(-1,-1,0); glColor3f(1,1,0); glTexCoord2f(1,0); glVertex3f(1,-1,0); glColor3f(1,0,1); glTexCoord2f(1,1); glVertex3f(1,1,0); glColor3f(0,1,1); glTexCoord2f(0,0); glVertex3f(-1,1,0); glEnd(); Notice how we bind the desired texture map outside the Begin/End construct. Failing to do so will make OpenGL ignore the call. In addition, you can combine texture mapping with colors to achieve a modulation effect. The incoming texture color will be multiplied with the per-vertex color (using interpolation when needed). This is especially useful in software illumination algorithms. We would apply base (unlit) textures to geometry primitives and modulate them with information from the software illumination stage. |