Graphics – MGP – Rendering Geometry to an FBO

7 10 2010

The title of this post might seem a little strange both in content and intention. Hopefully by the end of it I will have explained what I mean by “rendering geometry to an FBO” and why we would want to do it.

First the why
Now that we have a reasonable approximation of a glass the next and final step is to make it melt. This involves simulating the process of heating up glass, letting gravity cause the heated (and now less viscous) glass to flow, and then letting the glass cool over time into its new shape.

This sounds pretty complicated (it isn’t) but most importantly it requires us to be able to take the position of each vertex in the glass, do something with it, and then store the new position for the next time we want to draw it. The naïve approach would be to keep all the info in main memory, do our thing, and then update the graphics card with the new positions every time. This has a few issues though:

  1. Even a fast computer is slow
  2. We end up transferring lots to the graphics card
  3. It isn’t anywhere near as cool as getting the GPU to do it!

And now the how…
Back in the post on tessellation we described how we were uploading the data onto the graphics card so that each time we wanted to draw this we didn’t have to upload it again.

So the idea behind rendering the geometry to an FBO and reading it back out is that we can use the existing vertex/normal/colours buffers as ‘textures’, run calculations on the graphics card within a shader, and then read the information back out into the same buffers to continue the rendering process as we have previously.

Based on this we can break down what we have to do into 4 steps: create an FBO to render into, interpret the existing buffers as textures, run a shader over them, and copy the result back into the original buffers.

1. Create an FBO
I’m not going to post the exact code I use for setting everything up exactly for the FBOs as there is a bit more going on (and this post is going to be long enough) but here are the basics. This code will set up a variable number of colour attachments to take advantage of the multiple render targets that FBOs can have.

// Create a reference to an fbo
glGenFramebuffersEXT(1, (GLuint*)&FBOId);
// bind to the new fbo
// Create the texture representing the results that will be written to
for( int i=0;i<num_draw_buffers;i++)
	glGenTextures(1, (GLuint*)&TexId[i]);
	glBindTexture(GL_TEXTURE_RECTANGLE_ARB, TexId[i]);
	glTexImage2D(GL_TEXTURE_RECTANGLE_ARB, 0, GL_RGB_FLOAT32_APPLE,  width, height, 0, GL_RGB, GL_FLOAT, 0x0);
// Check the final status of this frame buffer
int status = glCheckFramebufferStatusEXT(GL_FRAMEBUFFER_EXT);
	valid = false;
	valid = true;
// Unbind FBO so we can continue as normal
glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, 0);

The other setup we need to do is to allocate texture references that we can eventually associate with the buffers.

But first: a non-obvious issue. Buffers for holding Vertex, Normal and Colour information are one dimensional and reasonably free of space limitations. Textures are different and different graphics cards/drivers have different limitations. Thus we need to break up our 1D buffer into a 2D buffer where the width is no larger than the max texture width supported by the card.

// calculate the width and height of the fbo we need
if( numberVerts < max_width )
	renderWidth = numberVerts;
	renderHeight = 1;
	renderOverrun = 0;
else {
	renderWidth = max_width;
	renderHeight = numberVerts / max_width + 1;
	renderOverrun = max_width * renderHeight - numberVerts;

And now that we have our dimensions we can allocate the required texture references. As you may notice the data portion of the call is NULL which means we aren’t actually providing data, just allowing for a reference to a texture of the given size.

We need to use GL_RGB_FLOAT32_APPLE as our internal storage method as we need reasonably high precision that would be otherwise lost. We will also be using GL_TEXTURE_RECTANGLE_ARB instead of the standard 2D because we need the precision when accessing the elements of the texture.

glGenTextures(1, (GLuint*)&vertexTex);
glBindTexture(GL_TEXTURE_RECTANGLE_ARB, vertexTex);
glTexImage2D(GL_TEXTURE_RECTANGLE_ARB, 0, GL_RGB_FLOAT32_APPLE,  renderWidth, renderHeight, 0, GL_RGB, GL_FLOAT, 0x0);

2. Interpret the existing Buffers as Textures
This step is one of the simplest and involves only 3 steps for each buffer. One just has to bind the texture, bind the buffer and ‘update’ the texture from the buffer. The trick is not to provide data when updating the texture which tells the card to use the bound buffer as the data source instead.

glBindTexture(GL_TEXTURE_RECTANGLE_ARB, vertexTex);
glBindBuffer(GL_PIXEL_UNPACK_BUFFER, vertexBufferRef);
glTexSubImage2D( GL_TEXTURE_RECTANGLE_ARB,	0, 0, 0, renderWidth, renderHeight, GL_RGB, GL_FLOAT, NULL );

3. Render the data into the FBO
Rendering the data into the FBO involves setting up one quad that covers the entire frame so that each fragment corresponds to one vertex. Thus each time the fragment shader runs it will advect one vertex (and update colours and normals). For this to happen we have to change the frame buffer we are rendering into, bind the textures we updated in Step 2, enable the shader, render our quad, and then disable everything again.

// Step 1: Change the frame buffer and set up the view port to be rendering 
glViewport(0,0, width, height);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);	// Clear Screen And Depth Buffer	// OUCH!! Big performance hit
glDrawBuffers(numTexId, dbuffers);

// Step 2: bind the textures
glActiveTextureARB( GL_TEXTURE0_ARB );

// Step 3: enable the shader

// Step 4: render the quad - yes it is immediate mode. yes that is bad
glMultiTexCoord2f(GL_TEXTURE0, 0,0); 
glVertex2f(0, 0);
glMultiTexCoord2f(GL_TEXTURE0, renderWidth,0); 
glVertex2f(renderWidth, 0);
glMultiTexCoord2f(GL_TEXTURE0, renderWidth,renderHeight); 
glVertex2f(renderWidth, renderHeight);
glMultiTexCoord2f(GL_TEXTURE0, 0,renderHeight); 
glVertex2f(0, renderHeight);

// Step 5: turn everything back to normal
glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, 0);

4. Read the FBO render targets back into the Textures
This is also an easy step as we are just reversing the process taken in Step 2. The difference is that we are now binding the render target as our source (instead of the buffer) and we read into the buffer using glReadPixels instead of glTexSubImage2D. Once again using NULL for the ‘data’ portion tells it to use the attached read buffer.

// bind to the FBO so we can reference its render targets
glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, fbo->getID());
// read the output of each render target back into the buffers provided
glReadPixels(0, 0, renderWidth, renderHeight, GL_RGB, GL_FLOAT, 0);

And the final output!
If something didn’t work then the final output will be nothing at all (doh!) but if it did you will be greeted with the impressive sight of… exactly what we had when we started. But we now are ready to add the melting.


Addendum: Actually, because I haven’t specified what the shaders are for rendering into the FBO nothing will be passed on till the next rendering step so nothing will be shown… till the next post




Leave a Reply

Fill in your details below or click an icon to log in: Logo

You are commenting using your account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: