Textures and Framebuffer Objects

2010, Dr. Lawlor, CS 481/681, CS, UAF

The key trick in a lot of modern graphics applications is "multi-pass rendering": we run one shader, and then read the results in a subsequent shader.  The key data structure here is a texture--in GLSL, about the only thing you can read efficiently is texture pixels!

Setting up a Texture

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.

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) on the current bound texture, or from the CPU side with 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.
	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.  

Deferred Shading Raytracing

The basic idea is to first render the ray-geometry intersection point P to a texture, then read adjacent pixels of the texture to determine the local geometry tangent vectors and hence surface normal.  See the example code for the gory details.

This general notion of rendering to a texture, then reading various pixels of the texture to get work done, is extremely common and powerful!