Render to texture

Overview

As I was trying out various techniques, I found the operation of rendering to textures fundamentally useful in many cases. In this post I will introduce my interface of generating an FBO, binding a color buffer and a RBO, and drawing with new FBO’s texture in OpenGL.

FBO

The Frame Buffer Object is a buffer containing drawing data in one. This usually means a color buffer with colors and a Render Buffer Object with depth and stencil info. Since all these are buffers, encapsulating FBO info looks like:

class FBO
{
    GLuint m_fbo_handle;
    GLuint m_color_handle;
    GLuint m_depth_stencil_handle;
}

With color handle we keep a reference to color texture that we draw to, while with RBO storing depth and stencil data we can still commit depth and stencil test in the normal pipeline.

With type defined, we need a configuration on buffers before it is actually used, and that requires configurations on color buffer and RBO naturally:

void ConfigureFBO()
{
  glGenFramebuffers(1, &m_fbo_handle);
  glBindFramebuffer(GL_FRAMEBUFFER, m_fbo_handle);

  ConfigureColor();
  ConfigureDepthStencil();
}

void ConfigureColor()
{
  glGenTextures(1, &m_color_handle);
  glBindTexture(GL_TEXTURE_2D, m_color_handle);
  glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, NULL);
  glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, m_color_handle, 0);
}

void ConfigureDepthStencil()
{
  glGenRenderbuffers(1, &m_depth_stencil_handle);
  glBindRenderbuffer(GL_RENDERBUFFER, m_depth_stencil_handle);
  glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH24_STENCIL8, width, height);
  glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_RENDERBUFFER, m_depth_stencil_handle);
}

Draw to it!

Given FBO configured now we can directly draw to it as usual:

// 1. bind the fbo instance using its handle, done by your renderer
renderer->BindFBO(fbo_instance);

// 2. define shader you would normally use to draw real-time to //scene, deciding what is in the fbo; again, use the renderer in your //infrastructure
renderer->UseShader(shader_instance);

// 3. Draw calls of your renderer, this time drawing to the fbo

This allows you to draw to texture.

Application

This trick alone seems very straightforward, and it is. The point is it can be used in so many places for various modern rendering techniques. Here are some examples.

Post-processing

Post-processing gives you fancy screen effects like kernel effects. The only thing you need to do is to apply post-processing shader on the above texture you generated:

// 1. define shader used
renderer->UseShader(pp_shader);

// 2. bind texture to the one you just drew
renderer->BindTexture(fbo_instance->GetColorTarget());

// 3. draw the texture across screen
renderer->DrawQuad();

Since you bound to another FBO before, you may want to bind back to the default one with glBindFramebuffer(GL_FRAMEBUFFER, 0) before you do above.

Bloom

The bloom is a special kind of post-processing that needs two targets. The original target drawing the scene normally, and the other one with “bright” blurred fragments which you blend the original one with.

This second target with a special requirement on fragments is where rendering to texture becomes useful for this case. Effectively, I set up a second color attachment for the FBO:

GLuint colorBuffers[2];
glGenTextures(2, colorBuffers);
for (unsigned int i = 0; i < 2; ++i)
{
    // bind color buffer i to the FBO
    // in this case the color buffer member of FBO is modified to hold two/multiple buffers
}

And then we just need to draw with the shader that will also pick the “bright” fragments into the second target:

renderer->UseShader(pbr_bloom);

// draw the pbr scene as usual...
// pick fragments along the way

Finally, we blend the original target with the blurred target:

// 1. use blend shader
renderer->UseShader(bloom_blend);

// 2. bind original texture at index 0
renderer->BindTexture(fbo_instance, 0);

// 3. bind blurred texture
renderer->BindTexture(blurred_target);

// 4. draw the texture across the screen
renderer->DrawQuad();

This gives effect like the following:

Deferred Shading

In deferred shader, we render to multiple buffers of FBO (or G-Buffer in this case), instead of one or two buffers. We then use data in those buffers to calculate the lighting scene pixel by pixel (or only those in lighting volumes if those are used). This is by nature rendering to the screen quad again, similar to situations above.

Now hopefully you see how rendering to texture is a useful operation/methodology used in many techniques.

Leave a Reply