CMSC 23700 Lab 6: Deferred Rendering

Lab 6 Objectives

In this week's lab, you will learn about the mechanisms needed implement deferred rendering in OpenGL, in particular:


Background

Refer back to your notes on the theory behind deferred rendering as discussed in class. The project 4 description gives background information about the math and rendering process associated with implementing deferred rendering. Please look over this information to make sure you fully understand before trying to implement it in lab.

No Seeded Code

There is no seeded code for this lab. You can use the mechanisms below to begin implementing deferred rendering into your Project 4. Remember, the information below only describes how to setup the g-buffer and stencil operations. You need to think about how to best organize these features into your source code.


G-Buffer Setup

As described in the project description, the G-Buffer is a collection of textures used to store lighting-relevant information that will be used during the final lighting pass. Usually, it contains a texture per vertex attribute along with additional information. We separate the attributes and write them into the different textures all at once using Multiple Render Targets (MRT).

Since we are going to use MRT to populate the G-Buffer, the textures will need to be stored inside a FBO. We can create and bind to a FBO by doing the following:

Glint fboId;

// Creates a framebuffer object and assigns the id to fboId
glGenFramebuffers(1, & fboId);

// Make the the framebuffer in "fboId" the current framebuffer (GL_FRAMEBUFFER).  
glBindFramebuffer(GL_FRAMEBUFFER, fboId);
Note: If you are unfamiliar with FBOs they were discussed inside the section "Framebuffers & Depth Buffers" in lab 5.

Once you have a FBO created, we can then begin allocating textures that will live inside the FBO. Use the cs237::texture2D type to create your textures for your vertex attributes or other information that will represent the G-Buffer. Take a look at the constructor for it inside the documentation code. The texture creation code should look similar to this:

/*Note: width and height are the size you want the texture to be, which 
should be the window size. */
cs237::texture2D tex = new cs237::texture2D (
            GL_TEXTURE_2D, GL_RGB32F, width, height,
            GL_RGB, GL_FLOAT);

This setup will be the same for all your vertex attribute textures inside the FBO. The depth values require that both the internal texture format and image formate be a GL_DEPTH_COMPONENT.

Next, we need to attach the textures to the frambuffer. We have only initialized the textures but haven't attached them to the framebuffer. You can attach the textures by using the glFramebufferTexture function. You will attach the textures to the color attachments that are part of a framebuffer by using the color attachment enumerations. For example, the first color attachment is GL_COLOR_ATTACHMENT0 . If you want to get further attachments just add the index of the attachment you want to GL_COLOR_ATTACHMENT0. For example, to get the third color attachment it would be GL_COLOR_ATTACHMENT0 + 2. Here is an example of attaching the previous "tex" to the second color attachment:

// the last parameter represents the mip-map level, which can be 0 in all cases. 
// You could also use the predefined attachment GL_COLOR_ATTACHMENT1 
 CS237_CHECK(glFramebufferTexture(GL_FRAMEBUFFER,
             GL_COLOR_ATTACHMENT0 + 1, tex->Id(), 0));
Note: The depth texture will need to use the GL_DEPTH_ATTACHMENT instead of GL_COLOR_ATTACHMENT.

In order to use MRT, we need to enable writing all textures attached to the FBO. We do that by supplying an array of attachment locations to the glDrawBuffers function. In other words, we are stating to OpenGL which color attachments we'll use (for this particular FBO) for rendering. Using our example from before, we can enable the second attachment using the following code:

GLenum attachments[] = {GL_COLOR_ATTACHMENT1};
//The first argument to glDrawBuffers is the number of items in the array 
CS237_CHECK(glDrawBuffers(1, attachments));

Finally make sure to check the status and unbind your FBO once you are done initialzing it:

if (status != GL_FRAMEBUFFER_COMPLETE) {
    std::cerr << "FBO  error, status = " << status << std::endl;
    exit (EXIT_FAILURE);
}

// restore default FBO
CS237_CHECK( glBindTexture (GL_TEXTURE_2D, 0) );
CS237_CHECK (glBindFramebuffer(GL_FRAMEBUFFER, 0));

Tips for G-Buffer Pass & Contents Viewing


Multi-Render Targets

The geometry pass will have its own shader program that will output the information needed to populate the G-buffer. The vertex shader will perform the usual transformations and pass the results to the fragment shader. Note: I kept my "out" variables in world-space coordinates because I perform my light calculations in world-space.

The fragment shader is responsible for doing MRT. Normally, the fragment shader outputs a single vec4, which represents the color for a given pixel. Instead the fragment shader for the geometry pass's shader will output multiple vectors that represent information that will be stored in the G-Buffer. Each of these vectors goes to a corresponding index in the array that was previously set by the glDrawBuffers function. Thus, for each FS invocation we are writing into the textures of the G buffer. For example:

/* This means that all the position information will be stored to the attachment that was first in the 
 * glDrawBuffers attachments array 
 */
layout (location = 0) out vec3 pOut;

/* This means that all the normal information will be stored to the attachment that was second in the 
 * glDrawBuffers attachments array 
 */
layout (location = 1) out vec3 nOut;

in vec3 f_norm;
in vec3 f_worldPos;

void main ()
{
   pOut = f_worldPos;
   nOut = normalize(f_norm);
}

Stencil Buffer Operations

As mentioned in the project, you want to further reduce lighting work by implementing a technique similar to stencil shadows. This work is done using the Stencil buffer and its operations. In a similar manner to the depth test, the stencil test can be used to discard fragments prior to fragment shader execution. It works by comparing the value at the current fragment location in the stencil buffer with a reference value. There are several comparison functions available:

Based on the result of both the stencil test as well as the depth test you can define an action known as the stencil operation on the stored stencil value. The following operations are available:

You can setup these operations by using the glStencilFunc and glStencilOpSeparate functions. For example:

//This will make the stencil test succeed always (i.e., only the depth test matters). 
glStencilFunc(GL_ALWAYS, 0, 0);

//Sets the front and back facing polygons for the stencil operation 
glStencilOpSeparate(GL_BACK, GL_KEEP, GL_ZERO, GL_KEEP);
glStencilOpSeparate(GL_FRONT, GL_KEEP, GL_DECR, GL_KEEP);

Remember you can enable/disable, clear, and cull the faces by using the OpenGL functions: