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_MAG_FILTER,GL_LINEAR);
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) */
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
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:
glBindTexture(GL_TEXTURE_2D,myTex);
glEnable(GL_TEXTURE_2D); /* turn on texturing */
glColor4f(1,1,1,1); /* gets multiplied by texture colors */
glBegin(GL_QUADS);
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);
glEnd();
glDisable(GL_TEXTURE_2D);
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);
glFramebufferTexture2DEXT(GL_FRAMEBUFFER_EXT,
GL_COLOR_ATTACHMENT0_EXT, GL_TEXTURE_2D, myTex, 0);
Now you can render stuff into your texture like so:
glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, fb);
glViewport(0,0,myTex_wid,myTex_ht);
glMatrixMode(GL_PROJECTION);
glLoadIdentity();
/* From 0..1 texture coords to -1..+1 clip coords */
glTranslatef(-1.0,-1.0,-1.0);
glScalef(2.0,2.0,2.0);
glMatrixMode(GL_MODELVIEW);
glLoadIdentity();
// ... 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);
glViewport(0,0,glutGet(GLUT_WINDOW_WIDTH),glutGet(GLUT_WINDOW_HEIGHT));
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.
- glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
Works like fract(texCoords). Texture coordinates wrap back around to
0.0 when they exceed 1.0, which causes the texture to repeat. This is
the default and most common wrap mode. - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
Works like clamp(texCoords,0.0+0.5/pixelSize,1.0-0.5/pixelSize), where
"pixelSize" is the corresponding dimension of the texture in *texture*
pixels. This has the effect of extending out the edge
pixels. With GL_NEAREST filtering, it's identical to GL_CLAMP,
but it's different for GL_LINEAR. I claim CLAMP_TO_EDGE is usually what you want if you don't want REPEAT mode.
- glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP);
Works like clamp(texCoords,0.0,1.0). Texture coordinates outside the normal range are flattened to 0.0-1.0. - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_BORDER);
Works like clamp(texCoords,0.0-0.5/pixelSize,1.0+0.5/pixelSize).
The edge pixels linearly blend down to the "border color" (usually a
transparent black), which then extends outward. This is
probably what GL_CLAMP should have been.
Texture Filtering
You can also change how colors interpolate between pixels in the
texture map when blowing up the texture map onscreen ("magnification"
mode):
- glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER,GL_NEAREST);
Return the color of the nearest pixel. Results in ugly boxy shapes, but it's really fast, and never blurry.
- glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER,GL_LINEAR);
Take
a bilinear blend of neighboring pixels to create output values.
Gives nice smooth output, but may be strange around the edges without
GL_CLAMP_TO_BORDER or GL_REPEAT.
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!
- glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER,GL_LINEAR_MIPMAP_LINEAR);
Linearly
interpolate pixels in the mipmaps, and then linearly interpolate
between mipmaps. The smoothest version by far, and the
recommended one to avoid aliasing.
- glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER,GL_LINEAR_MIPMAP_NEAREST);
Linearly
interpolate pixels in the mipmaps, but only use the closest mipmap
level. Slightly faster than linear-linear on older hardware. - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER,GL_NEAREST_MIPMAP_NEAREST);
Use
nearest-neighbor pixels in the mipmaps, and only use the closest mipmap
level. The ugliest approach, but good for checking for mipmap
problems. - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER,GL_NEAREST_MIPMAP_LINEAR);
Use
nearest-neighbor pixels in the mipmaps, but blend between mipmap levels. Rarely useful.
Another cool and very easy feature is "anisotropic filtering",
which combines several mipmaps to get sharper results from steeply
tilted surfaces.
glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MAX_ANISOTROPY_EXT,16);
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:
- JPEG, a "lossy" format, uses discrete cosine transform.
- PNG, a lossless format, uses an encoding similar to zip.
Typical simple image formats:
- BMP, a simple format from Microsoft.
- PPM, a simple format popular on UNIX machines.
- TGA, the ancient "Targa" format, that's still suprisingly useful.
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).
|
Name
|
Stores
|
Compression
|
Advantages
|
Disadvantages
|
jpg
|
JPEG
|
RGB
|
Lossy (DCT)
|
Amazingly tight compression.
|
libjpeg is big, and no alpha channel. Can cause artifacts on sharp edges.
|
png
|
Portable Network Graphics
|
RGBA
|
Lossless
|
Good compression, can represent alpha channel properly.
|
libpng is big, and not present by default on Windows machines.
|
bmp
|
Windows Bitmap
|
RGB
|
None (usually)
|
Builtin editors on Windows. Simple format. Also known as "pcx", which is the same format.
|
Files are big. No alpha channel.
|
tga
|
TARGA
|
RGBA
|
Runlength
|
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.
|
ppm
|
Portable Pixel Map
|
RGB
|
None
|
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.