OpenGL Handles, and how Stuff lives on the GPU
CS 481 Lecture, Dr. Lawlor
(This is a sort of meta-introduction, where you're supposed to replace "Thingy" with an actual specific object like "Texture", "Program", "Buffer", and so on as we'll discuss below.)
Here's the deal with thingys. You allocate an OpenGL handle to an on-graphics card thingy by calling a gen function:
void glGenThingys(int n_thingys, GLuint *thingy_handle);
Here's a typical call:
static GLuint myThingy=0;
if (myThingy==0) {
glGenThingys(1,&myThingy);
... set up your new Thingy ...
}
Once you've made a thingy, to use it you need to bind it as the current thingy. All subsequent thingy-related
calls will then apply to your thingy. The default, fixed-function
OpenGL operation uses handle zero, so it's good practice to bind thingy
zero back before you exit. The bind call is usually something
like:
glBindThingy(myThingy); // start using myThingy
... render using your Thingy ...
glBindThingy(0); // back to fixed-function OpenGL
Once bound, you need to set up your new thingy. OpenGL provides about fifty functions for setting up your thingy, of the form:
glSomethingThingy(GL_THINGY,GL_THINGY_SOMETHING, GL_FOR_GOODNESS_SAKES_GET_TO_THE_DANG_POINT);
Eventually, you may need to delete your thingy. There's a corresponding glDeleteThingys call to free up that space on the graphics card. However, I can't recommend creating and deleting thingys
every frame--creating and deleting any of these objects is usually
fairly expensive (like milliseconds), so it's faster to create things
once and re-use them many times.
glDeleteThingys(1,&myThingy);
Texture == Thingy
A Texture is a 1D, 2D, 3D, or cubemap array of color pixels. It's
implemented as a solid rectangular block of pixels sitting in GPU
memory. Texture state includes how to handle out-of-bounds
pixels (GL_TEXTURE_WRAP_axis), how to shrink and enlarge the texture (GL_TEXTURE_MIN_FILTER/MAG_FILTER), and so on.
Textures are kinda weird in that you can have several different
textures bound at once, to different texture "units", which are
numbered 0 through some small integer. You have to "activate" a
texture unit before binding a texture to it. Typical GPUs
nowadays support 4-8 texture units.
static GLuint monkeyTex=0;
if (monkeyTex==0) { /* first-time initialization */
glGenTextures(1,&monkeyTex); /* make a texture handle */
glActiveTexture(GL_TEXTURE3); /* we'll bind to texture unit 3 */
glBindTexture(GL_TEXTURE_2D,monkeyTex);
readTextureFromFile("monkey.bmp"); /* pixels go into monkeyTex */
/* Texture state applies to the currently bound texture */
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_MAX_ANISOTROPY_EXT,8);
glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MIN_FILTER,
GL_LINEAR_MIPMAP_LINEAR);
glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MAG_FILTER,GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D,GL_GENERATE_MIPMAP_SGIS, GL_TRUE);
}
... bind up a GLSL program
/* The monkey texture is bound to texture unit 3 */
glUniform1iARB(glGetUniformLocationARB(prog,"myMonkey"),3);
... In GLSL:
uniform sampler2D myMonkey;
... vec4 t = texture2D(myMonkey,vec2(monkeyCoords));
Pitfalls:
- If you've turned on mipmaps, like above, you've got to make sure
all the mipmap levels are loaded. If you've only loaded up mipmap
level 0 (for example, with a glTexImage2D or glCopyTexSubImage2D call),
OpenGL just *ignores* your texture, resulting in a white object (fixed
function) or black/clear object (GLSL). Make sure all your mipmap
levels are there, or disable mipmaps (GL_TEXTURE_MIN_FILTER of just
GL_LINEAR).
Vertex Buffer Object == Thingy
Vertex Buffer Objects (VBOs) are a way to store geometry information on
the graphics card, just like Textures let you store raster information
on the graphics card. This is part of the ARB_vertex_buffer_object extension.
A VBO describes a series of glVertex (and optionally glColor, glNormal,
and glTexCoord) calls. The parameters for the calls start in a
CPU array, and get copied into graphics card memory.
You create a Vertex Buffer Object with (guess what!) glGenBuffersARB. You then have to glBindBufferARB
the buffer, and then you can then copy data in with
glBufferDataARB. You describe what your data contains
using calls to glVertexPointer (and optionally glColorPointer, glNormalPointer, and glTexCoordPointer), which each take the same four parameters:
- size, the number of floats per vertex (usually 3; it's hardcoded to 3 for glNormalPointer!)
- type, the data type (usually GL_FLOAT, occasionally GL_SHORT to save space, or even GL_BYTE for colors)
- stride, the distance in bytes between each vertex's data. This is usually sizeof(yourVertexStruct).
- pointer, the distance in bytes from the start of the buffer
to the corresponding vertex data. This is zero for the first
element, 12 for the second vec3 (because you have to skip over the
first), 24 for the third vec3, and so on. This is passed as a
"void *", and back in the pre-vertexbuffer days could point directly to
CPU data, but now you pass the buffer data separately.
Here's how you'd create a vertex buffer object to store vertex locations and colors, then render it:
static GLuint vb=0;
if (vb==0) { /* set up the vertex buffer object */
glGenBuffersARB(1,&vb); /* make a buffer */
glBindBufferARB(GL_ARRAY_BUFFER_ARB,vb);
/* Copy our vtx array (on the CPU) into our new GPU buffer */
glBufferDataARB(GL_ARRAY_BUFFER_ARB,sizeof(myVertex)*vtx.size(),
&vtx[0],GL_STATIC_DRAW_ARB);
/* Tell OpenGL how our array is laid out */
glEnableClientState(GL_VERTEX_ARRAY);
glVertexPointer(3,GL_FLOAT,sizeof(vtx[0]), (void *)0); /* myVertex.xyz is first thing in struct */
glEnableClientState(GL_COLOR_ARRAY);
glColorPointer (3,GL_FLOAT,sizeof(vtx[0]), (void *)12); /* myVertex.color starts 12 bytes after struct start */
glBindBufferARB(GL_ARRAY_BUFFER_ARB,0); /* back to plain OpenGL */
}
/* Draw all our (GPU) points.
This is way faster than looping over vtx and calling glVertex many times! */
glBindBufferARB(GL_ARRAY_BUFFER_ARB,vb);
glDrawArrays(GL_POINTS,0,vtx.size());
glBindBufferARB(GL_ARRAY_BUFFER_ARB,0);
You can also create an "element buffer" to store vertex indices.
For example, to make a triangle from vertices zero, seven, and
thirteen, you'd put {0,7,13} into an element buffer. Element
buffers allow many triangles to point to the same vertex, which saves
that vertex many trips through your vertex shader. You upload the
index data with glBufferDataARB (just like vertex buffer objects), and
then use glDrawElements to look up your indices into your (already bound) vertex array:
static GLuint eb=0;
if (eb==0) { /* set up the element buffer object */
glGenBuffersARB(1,&eb); /* make a buffer */
glBindBufferARB(GL_ELEMENT_ARRAY_BUFFER_ARB,vb);
/* Copy our idx array (on the CPU) into our new GPU buffer */
glBufferDataARB(GL_ARRAY_BUFFER_ARB,sizeof(int)*idx.size(),
&idx[0],GL_STATIC_DRAW_ARB);
glBindBufferARB(GL_ELEMENT_ARRAY_BUFFER_ARB,0); /* back to plain OpenGL */
}
glBindBufferARB(GL_ARRAY_BUFFER_ARB,vb); /* vertex data */
glBindBufferARB(GL_ELEMENT_ARRAY_BUFFER_ARB,eb); /* index data */
glDrawElements(GL_TRIANGLES,idx.size(),GL_UNSIGNED_INT,0);
glBindBufferARB(GL_ARRAY_BUFFER_ARB,0);
glBindBufferARB(GL_ELEMENT_ARRAY_BUFFER_ARB,0);
Framebuffer Object == Thingy
A "Framebuffer Object" (FBO) is a place you can render stuff.
This consists of a color texture, an optional depth texture (for the
depth buffer), and possibly other weirder things like stencils (one
byte per pixel, used for certain shadow algorithms) or multiple render
targets (one fragment shader, many output colors!). The gory
details are in EXT_framebuffer_object.
Framebuffer Objects are a handy way to do offscreen rendering which allows:
- Drawing directly to a texture (instead of drawing to the screen
then using a glCopyTexSubImage2D, which has to copy pixels at some
lowish cost)
- Drawing an image bigger than your current window size (you're limited only by the texture size).
- Drawing to a high-precision target, like a floating-point output
buffer. This is useful for non-rendering or "general-purpose GPU"
(GPGPU) stuff on the graphics card.
As usual, you make a new Framebuffer Object with glGenFramebuffersEXT,
bind in a Framebuffer Object with glBindFramebufferEXT, and then
glFramebufferTexture2DEXT can attach texture objects (as raw GLuint
handles!) to the current framebuffer. You can then reset the
rendering size with glViewport, and start rendering away! Note
that you can also render a few things, switch the destination buffer
with glFramebufferTexture2DEXT, and then render more stuff; this
"buffer swap" is actually a bit faster than binding in a new
framebuffer object.
/* Framebuffer output texture */
static GLuint frameTex=0;
int w=256,h=256; /* size of our texture */
if (frameTex==0) {
glGenTextures(1,&frameTex); /* make a texture handle */
glBindTexture(GL_TEXTURE_2D,frameTex);
glTexImage2D(GL_TEXTURE_2D,0,
GL_RGBA8, w,h, /* data format and size (pixels) */
0,GL_LUMINANCE,GL_FLOAT,0); /*<- no data needed, just size */
glGenerateMipmapEXT(GL_TEXTURE_2D); /*<- most cards *require* all mipmap levels to be present! */
}
/* Framebuffer object */
static GLuint fbo=0;
if (fbo==0) { /* set up framebuffer object */
glGenFramebuffersEXT(1,&fbo);
glBindFramebufferEXT(GL_FRAMEBUFFER_EXT,fbo);
glFramebufferTexture2DEXT(GL_FRAMEBUFFER_EXT,
GL_COLOR_ATTACHMENT0_EXT, GL_TEXTURE_2D, frameTex, 0);
glBindFramebufferEXT(GL_FRAMEBUFFER_EXT,0); /* back to normal */
}
/* Render into our framebuffer object */
glBindFramebufferEXT(GL_FRAMEBUFFER_EXT,fbo);
if (glCheckFramebufferStatusEXT(GL_FRAMEBUFFER_EXT != GL_FRAMEBUFFER_COMPLETE_EXT)
printf("Framebuffer object is unhappy! Oh no!\n");
glViewport(0,0,w,h);
glDisable(GL_DEPTH_TEST); /* or else attach a depth texture to GL_DEPTH_ATTACHMENT_EXT! */
... rendered stuff will go into the frameTex texture now! ...
glBindFramebufferEXT(GL_FRAMEBUFFER_EXT,0); /* back to normal rendering */
glViewport(0,0,glutGet(GLUT_WINDOW_WIDTH),glutGet(GLUT_WINDOW_HEIGHT));
glEnable(GL_DEPTH_TEST);
glBindTexture(GL_TEXTURE_2D,frameTex);
glGenerateMipmapEXT(GL_TEXTURE_2D); /* build mipmaps of rendered data */
Pitfalls:
- Your card might only support rendering into certain texture
formats. In particular, you often need all the mipmap levels in
order to render into a texture.
- You can't render into a texture you're currently reading from.
- Creating a new framebuffer object is really slow. Like
milliseconds slow. Make one at the start of the program, and
re-use it; don't re-create the framebuffer object every frame.
GLSL Program == Thingy (kinda)
A compiled GLSL program is a rather unusual OpenGL object in a few ways:
- It's not a GLuint, it's a "GLhandleARB" (but it's the same idea).
- You "Use" it, not "Bind" it (but it's the same idea).
- You can create it without even "Use"ing it (odd in OpenGL!).
static GLhandleARB p=0;
if (p==0) {
p=glCreateProgramObjectARB();
GLhandleARB vo=glCreateShaderObjectARB(GL_VERTEX_SHADER_ARB);
GLhandleARB fo=glCreateShaderObjectARB(GL_FRAGMENT_SHADER_ARB);
glShaderSourceARB(vo,1,&"//This is my vertex shader ... GLSL here ...",NULL);
glShaderSourceARB(fo,1,&"//This is my fragment shader ... GLSL here ...",NULL);
glCompileShaderARB(vo); glCompileShaderARB(fo); /* FIXME: error check! */
glAttachObjectARB(p,vo); glAttachObjectARB(p,fo);
glLinkProgramARB(p); /* FIXME: error check! */
glDeleteObjectARB(vo); glDeleteObjectARB(fo); /* don't leak memory! */
}
glUseShaderObjectARB(p);
/* render stuff with our GLSL code here! */
glUseShaderObjectARB(0); /* Back to ordinary OpenGL */