Textures in Modern OpenGL

CS 480 Lecture, Dr. Lawlor

A "texture" in OpenGL is just an image; a 2D array of color pixels.  Textures live in graphics card memory, where the graphic card can access them at absurd speed.  But this means rather than just a plain C++ array, a texture has to be referenced via an integer "handle", and all your reads and writes to textures need to go through OpenGL.  Like most of OpenGL, you operate on a texture by first "bind"ing the texture, which makes all subsequent texture calls modify that texture.

Here's how you set up a new texture of size "w" by "h" pixels in OpenGL:
	GLuint myTex=0; /* openGL "texture handle" */
glGenTextures(1,&myTex); /* make a new texture */
glBindTexture(GL_TEXTURE_2D,myTex); /* modify (or draw) our texture */
GLenum format=GL_RGBA8; /* color, 8 bits per channel */
glTexImage2D(GL_TEXTURE_2D,0,format, w,h,0, GL_RGBA,GL_UNSIGNED_BYTE,0); /* the texture is w x h pixels */
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER,GL_LINEAR); /* no mipmaps (see below) */
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); /* no repeat (see below) */
glBindTexture(GL_TEXTURE_2D,0); /* back to default texture */
The last parameter to glTexImage2D can also be a pointer to a CPU array of pixels.  If you pass zero, the texture is uninitialized, which is fine as long as you write to the texture using a framebuffer object (see below).

Rendering from a Texture (Texture Reads)

Here's how to draw a texture onscreen: basically just bind the texture, enable texturing, and set up texture coordinates before drawing your vertices:
glEnable(GL_TEXTURE_2D); /* turn on texturing */
glColor4f(1,1,1,1); /* gets multiplied by texture colors */
glTexCoord2f(0,0); glVertex3f(0,0,0);
glTexCoord2f(1,0); glVertex3f(sz,0,0);
glTexCoord2f(1,1); glVertex3f(sz,sz,0);
glTexCoord2f(0,1); glVertex3f(0,sz,0);
From GLSL, you have to declare a "uniform sampler2D myTex;" and then look up colors with "vec4 c=texture2D(myTex,texCoords);" with a vec2 texCoords of texture coordinates.  You also need to glBindTexture and set up the sampler uniform using "glUniform1iARB(glGetUniformLocationARB(program,"myTex"),0);" from C++.  The 0 there is the current 'texture unit'; you can combine several textures at once by calling glActiveTexture to switch units before you bind your texture handle.

Rendering to a Texture (Texture Writes)

You can render pixels into a texture using a framebuffer object.  Here's how you make a framebuffer object "fb" hooked up to render into "myTex":
	GLuint fb=0;
glGenFramebuffersEXT(1, &fb);
glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, fb);
Now you can render stuff into your texture like so:
	glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, fb);

/* From 0..1 texture coords to -1..+1 clip coords */


// ... glClear, glBegin / glEnd, etc will now render into myTex!
Be sure to reset the rendering state afterwards by binding the zero framebuffer object (the screen):
	glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, 0);
If you want to enable GL_DEPTH, you need to make a depth texture (or a "renderbuffer") and attach that to your framebuffer as well.

Texture Coordinate Wrapping

You can change what happens outside normal texture coordinate bounds, on both the S (x axis) and T (y axis) texture coordinate axes.

Texture Filtering

You can also change how colors interpolate between pixels in the texture map when blowing up the texture map onscreen ("magnification" mode):
The same options exist when shrinking a texture map down onscreen ("minification" mode), along with extra "mipmap" options.  You set up mipmaps above with glGenerateMipmapEXT(GL_TEXTURE_2D), gluBuild2DMipmaps, or a whole set of glTexImage2D calls (one per level).  BEWARE!  If you enable one of these mipmap filtering modes, but you didn't set up all the mipmap levels in your texture, then OpenGL will IGNORE your texture, resulting in a white polygon!
Another cool and very easy feature is "anisotropic filtering", which combines several mipmaps to get sharper results from steeply tilted surfaces.
Both the texture wrapping and filtering modes above apply to the currently bound texture--if you switch textures, your wrapping and filtering modes change too.  

Texture File Formats

Generally, there are two classes of file formats out there: simple but huge, and complicated but small.  The simple formats don't do any real data compression, so they're really easy for programs to read and write, but they don't do any data compression, so they take up a lot of space on disk.  The complicated formats do data compression, so they're really unreasonably difficult to read without a dedicated library, but on the plus side they take much less space on disk.

Typical compressed image formats:
Typical simple image formats:
It's pretty easy to write code that reads these simple image formats from disk, since they're all just some sort of small binary header followed by RGB pixel data.  In the 481_texture example program, I'm using the "image.cpp" functions from Nigel Stewart's GLT library to read images, although I've slightly modified my versions to work outside of the rest of GLT.  There's also a much heavier-duty library called SOIL that can read almost anything (JPEG, PNG, BMP, ...), but it contains two or three separate .c files (I've never used it).

Lossy (DCT)
Amazingly tight compression.
libjpeg is big, and no alpha channel.  Can cause artifacts on sharp edges.
Portable Network Graphics
Good compression, can represent alpha channel properly.
libpng is big, and not present by default on Windows machines.
Windows Bitmap
None (usually)
Builtin editors on Windows.  Simple format.  Also known as "pcx", which is the same format.
Files are big.  No alpha channel.
Simple format--easy to read.  See C++ ltga library and docs.  Unlike every other format, most tga files store the data from bottom to top, which nicely matches OpenGL's default Y axis.
Not completely standardized.  For example, some targas store un-premultiplied alpha (normal R, G, B, A); others store premultiplied alpha (RA,GA,BA,A).  Files are big.
Portable Pixel Map
Very simple ASCII header followed by binary data.
ASCII header can include comments.  Files are big.  No alpha channel.

Any decent image editing program, like the GIMP, can read or write all these formats, including the alpha channel.

One annoying thing about old graphics hardware: sometimes old hardware only works with power-of-two texture sizes.  This is trivial if you're generating the image, but it's a real pain if you're reading an image off disk: your options are to scale the image, or pad the image and scale your texture coordinates.  Both options are annoyingly tricky to get right.