OpenGL Programming/Intermediate/Textures
A reader requests expansion of this page to include more material. You can help by adding new material (learn how) or ask for assistance in the reading room. |
Types
editThere are several different types of textures that can be used with OpenGL:
- GL_TEXTURE_1D: This is a one dimensional texture. (Requires OpenGL 1.0)
- GL_TEXTURE_2D: This is a two dimensional texture (it has both a width and a height). (Requires OpenGL 1.0)
- GL_TEXTURE_3D: This is a three dimensional texture (has a width, height and a depth). (Requires OpenGL 1.2)
- GL_TEXTURE_CUBE_MAP: Cube maps are similar to 2D textures but generally store six images inside the texture. Special texture mapping is used to map these images onto a virtual sphere. (Requires OpenGL 1.3)
- GL_TEXTURE_RECTANGLE_ARB: this texture format is much like the two dimensional texture but supports non power of two sized textures (Requires
Basic usage
editGLuint theTexture; glGenTextures(1, &theTexture); glBindTexture(GL_TEXTURE_2D, theTexture); glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_DECAL); // ... glTexImage2D(...); // draw stuff here glDeleteTextures(1, &theTexture);
When you set up a new texture, you will usually have a pixel array in memory, as well as the dimensions of the image. (Image libraries or OpenGL wrappers might provide you with routines to read various graphic formats and directly make textures from them, but in the end it is all the same.)
For that, you first tell OpenGL to give you a new texture "template", which you then select to work with it afterwards. You set various parameters, like how the texture is drawn. It is possible to make it that, for textures with alpha channel, the objects behind it are drawn, but it is incompatible with the depth buffer (it does not know the texture is translucent, marks the object in the front as solid and will not draw the objects behind). You then need to sort the objects by their distance to the camera by yourself and draw them in that order to get proper results. This is not handled here, by the way.
Finally you give OpenGL the pixel array and it will load the texture into memory.
Let's write a function that allows us to read the pixel array from a file, specifying the dimensions as parameters:
#include <stdio.h>
#include <stdlib.h>
#include <GL/gl.h>
#include <GL/glut.h>
GLuint raw_texture_load(const char *filename, int width, int height)
{
GLuint texture;
unsigned char *data;
FILE *file;
// open texture data
file = fopen(filename, "rb");
if (file == NULL) return 0;
// allocate buffer
data = (unsigned char*) malloc(width * height * 4);
// read texture data
fread(data, width * height * 4, 1, file);
fclose(file);
// allocate a texture name
glGenTextures(1, &texture);
// select our current texture
glBindTexture(GL_TEXTURE_2D, texture);
// select modulate to mix texture with color for shading
glTexEnvf(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_REPLACE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_DECAL);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_DECAL);
// when texture area is small, bilinear filter the closest mipmap
glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_NEAREST);
// when texture area is large, bilinear filter the first mipmap
glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
// texture should tile
glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
// build our texture mipmaps
gluBuild2DMipmaps(GL_TEXTURE_2D, 4, width, height, GL_RGBA, GL_UNSIGNED_BYTE, data);
// free buffer
free(data);
return texture;
}
The function eats image files that contain all pixel values in the order R, G, B, A. Such can be created using GIMP, for example: Make the image, merge all layers (necessary, because the RAW export module is broken), give it an alpha channel, save it and select RAW in the list of file types (at the bottom). (A recent version, 2.3 or up may be needed, I had problems with 2.2).
It returns you an OpenGL texture ID which you can select with glBindTexture(GL_TEXTURE_2D, texture) (passing the ID for texture).
Now that we loaded our texture, see how we can use it:
Example
edit /* compile with: gcc -lGL -lglut -Wall -o texture texture.c */
#include <GL/gl.h>
#include <GL/glut.h>
/* This program does not feature some physical simulation screaming
for continuous updates, disable that waste of resources */
#define STUFF_IS_MOVING 0
#if STUFF_IS_MOVING
#include <unistd.h>
#endif
#include <stdlib.h>
#include <math.h>
#include <time.h>
/* using the routine above - replace this declaration with the snippet above */
GLuint raw_texture_load(const char *filename, int width, int height);
static GLuint texture;
void render()
{
glMatrixMode(GL_PROJECTION);
glLoadIdentity();
/* fov, aspect, near, far */
gluPerspective(60, 1, 1, 10);
gluLookAt(0, 0, -2, /* eye */
0, 0, 2, /* center */
0, 1, 0); /* up */
glMatrixMode(GL_MODELVIEW);
glLoadIdentity();
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
glPushAttrib(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
glEnable(GL_TEXTURE_2D);
/* create a square on the XY
note that OpenGL origin is at the lower left
but texture origin is at upper left
=> it has to be mirrored
(gasman knows why i have to mirror X as well) */
glBegin(GL_QUADS);
glNormal3f(0.0, 0.0, 1.0);
glTexCoord2d(1, 1); glVertex3f(0.0, 0.0, 0.0);
glTexCoord2d(1, 0); glVertex3f(0.0, 1.0, 0.0);
glTexCoord2d(0, 0); glVertex3f(1.0, 1.0, 0.0);
glTexCoord2d(0, 1); glVertex3f(1.0, 0.0, 0.0);
glEnd();
glDisable(GL_TEXTURE_2D);
glPopAttrib();
glFlush();
glutSwapBuffers();
}
void init()
{
glClearColor(0.0, 0.0, 0.0, 0.0);
glShadeModel(GL_SMOOTH);
glEnable(GL_LIGHTING);
glEnable(GL_LIGHT0);
glEnable(GL_DEPTH_TEST);
glLightfv(GL_LIGHT0, GL_POSITION, (GLfloat[]){2.0, 2.0, 2.0, 0.0});
glLightfv(GL_LIGHT0, GL_AMBIENT, (GLfloat[]){1.0, 1.0, 1.0, 0.0});
texture = raw_texture_load("texture.raw", 200, 256);
}
#if STUFF_IS_MOVING
void idle()
{
render();
usleep((1 / 50.0) * 1000000);
}
#endif
void resize(int w, int h)
{
glViewport(0, 0, (GLsizei) w, (GLsizei) h);
}
void key(unsigned char key, int x, int y)
{
if (key == 'q') exit(0);
}
int main(int argc, char *argv[])
{
glutInit(&argc, argv);
glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGBA | GLUT_DEPTH);
glutInitWindowSize(640, 480);
glutCreateWindow("Texture demo - [q]uit");
init();
glutDisplayFunc(render);
glutReshapeFunc(resize);
#if STUFF_IS_MOVING
glutIdleFunc(idle);
#endif
glutKeyboardFunc(key);
glutMainLoop();
return 0;
}
Wow, that was a lot of code. Let's try to understand it, starting with main():
The first lines set up GLUT and are not worth being explained further.
init() sets up the canvas and the light and loads our texture into a global variable. Since we only have one texture here, it's useless, as we don't need to switch textures, but it's here anyway. (You switch textures using glBindTexture, see above.)
Then some callbacks are set up, also not worth being talked about, except the display function. First the camera is set up (if you would change the perspective during simulation, you'd make use of this - in this case it's redundant). The canvas is cleared then (you should also know that).
Now we switch texture mode on, which means everything we do now will be textured. The currently selected texture will be used, which is in this case the one we created before (it is still selected since it was first selected during setup). Then a square is created on the XY axis. Note the glTexCoord2d calls: They define which part of the texture is to be assigned to the next vertex. We will make more use of it in another example.
Eh, and then it's drawn. It didn't really hurt, did it?
You want to place a RAW image called texture.raw in the working directory, RGBA 256x256. Such files can be created with some graphics editors, including GIMP.
A simple libpng example
editThis c++ code snippet is an example of loading a png image file into an OpenGL texture object. It requires libpng and OpenGL to work. To compile with gcc, link png glu32 and opengl32 . Most of this is taken right out of the libpng manual. There is no checking or conversion of the png format to the OpenGL texture format. This just gives the basic idea.
#include <GL/gl.h>
#include <GL/glu.h>
#include <png.h>
#include <cstdio>
#include <string>
#define TEXTURE_LOAD_ERROR 0
using namespace std;
/** loadTexture
* loads a png file into an opengl texture object, using cstdio , libpng, and opengl.
*
* \param filename : the png file to be loaded
* \param width : width of png, to be updated as a side effect of this function
* \param height : height of png, to be updated as a side effect of this function
*
* \return GLuint : an opengl texture id. Will be 0 if there is a major error,
* should be validated by the client of this function.
*
*/
GLuint loadTexture(const string filename, int &width, int &height)
{
//header for testing if it is a png
png_byte header[8];
//open file as binary
FILE *fp = fopen(filename.c_str(), "rb");
if (!fp) {
return TEXTURE_LOAD_ERROR;
}
//read the header
fread(header, 1, 8, fp);
//test if png
int is_png = !png_sig_cmp(header, 0, 8);
if (!is_png) {
fclose(fp);
return TEXTURE_LOAD_ERROR;
}
//create png struct
png_structp png_ptr = png_create_read_struct(PNG_LIBPNG_VER_STRING, NULL,
NULL, NULL);
if (!png_ptr) {
fclose(fp);
return (TEXTURE_LOAD_ERROR);
}
//create png info struct
png_infop info_ptr = png_create_info_struct(png_ptr);
if (!info_ptr) {
png_destroy_read_struct(&png_ptr, (png_infopp) NULL, (png_infopp) NULL);
fclose(fp);
return (TEXTURE_LOAD_ERROR);
}
//create png info struct
png_infop end_info = png_create_info_struct(png_ptr);
if (!end_info) {
png_destroy_read_struct(&png_ptr, &info_ptr, (png_infopp) NULL);
fclose(fp);
return (TEXTURE_LOAD_ERROR);
}
//png error stuff, not sure libpng man suggests this.
if (setjmp(png_jmpbuf(png_ptr))) {
png_destroy_read_struct(&png_ptr, &info_ptr, &end_info);
fclose(fp);
return (TEXTURE_LOAD_ERROR);
}
//init png reading
png_init_io(png_ptr, fp);
//let libpng know you already read the first 8 bytes
png_set_sig_bytes(png_ptr, 8);
// read all the info up to the image data
png_read_info(png_ptr, info_ptr);
//variables to pass to get info
int bit_depth, color_type;
png_uint_32 twidth, theight;
// get info about png
png_get_IHDR(png_ptr, info_ptr, &twidth, &theight, &bit_depth, &color_type,
NULL, NULL, NULL);
//update width and height based on png info
width = twidth;
height = theight;
// Update the png info struct.
png_read_update_info(png_ptr, info_ptr);
// Row size in bytes.
int rowbytes = png_get_rowbytes(png_ptr, info_ptr);
// Allocate the image_data as a big block, to be given to opengl
png_byte *image_data = new png_byte[rowbytes * height];
if (!image_data) {
//clean up memory and close stuff
png_destroy_read_struct(&png_ptr, &info_ptr, &end_info);
fclose(fp);
return TEXTURE_LOAD_ERROR;
}
//row_pointers is for pointing to image_data for reading the png with libpng
png_bytep *row_pointers = new png_bytep[height];
if (!row_pointers) {
//clean up memory and close stuff
png_destroy_read_struct(&png_ptr, &info_ptr, &end_info);
delete[] image_data;
fclose(fp);
return TEXTURE_LOAD_ERROR;
}
// set the individual row_pointers to point at the correct offsets of image_data
for (int i = 0; i < height; ++i)
row_pointers[height - 1 - i] = image_data + i * rowbytes;
//read the png into image_data through row_pointers
png_read_image(png_ptr, row_pointers);
//Now generate the OpenGL texture object
GLuint texture;
glGenTextures(1, &texture);
glBindTexture(GL_TEXTURE_2D, texture);
glTexImage2D(GL_TEXTURE_2D,0, GL_RGBA, width, height, 0,
GL_RGB, GL_UNSIGNED_BYTE, (GLvoid*) image_data);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
//clean up memory and close stuff
png_destroy_read_struct(&png_ptr, &info_ptr, &end_info);
delete[] image_data;
delete[] row_pointers;
fclose(fp);
return texture;
}
Specifying texture coordinates
editTexture coordinates assign certain points in the texture to vertices. It allows you to save texture memory and can make work a bit easier. For example, a dice has six different sides. You can put every side next to each other in a texture and say "this side should get the leftmost third of my texture".
This makes even more sense when modeling complex objects like humans, where you have the body, legs, arms, head, ... if you would use extra texture files for every part, you quickly end up with a lot of texture files which are terribly inefficient to manage. You better put all parts in one file and select the appropiate part when drawing, performs much better.
You saw texture coordinates already in the previous example, although the selected part was the whole texture. The two parameters given to glTexCoord2d are numbers ranging from 0 to 1. (This has the advantage of being size-independent - you can later decide to use a higher-resolution texture and do not need to change the rendering code!)
Tips
edit- Switching textures is inefficient. Try to draw all objects with texture A first, then switch textures and draw everything with texture B, and so on. (If you have alpha textures, this can't be accomplished always, as you then must order all objects by yourself. An article about this might follow soon.)
- Try to combine small textures into one large and select the part you want with texture coordinates. This reduces the memory overhead and also reduces the number of texture switches.