Lab 6: Deferred Rendering

Lab 6 Objectives

In this week's lab, you will learn about the mechanisms needed to help towards implementing deferred rendering in OpenGL, in particular:
  • G-Buffer setup
  • Multiple Render Targets (MRTs)
  • Stencil Buffer operations


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 proccess assoicated 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 orgranize 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 seperate 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 4.

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 similiar to this:

/*Note: width and height are the size you want the texture to be, which 
should be the window size, but again rounded up to the next closest 
power of 2. */ 
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 attachements 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's 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 attachements 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

  • Before running the Geometry pass make sure to bind the current framebuffer to your g-buffer's fbo (i.e. glBindFramebuffer(...))
  • Make sure to clear the g-buffer's fbo.
  • You can view the contents of the the G-Buffer as described in the project 4 description. You just need to make sure that you bind your textures inside your G-Buffer to active texture units and samplers in your testing renderers.

    Another way to view your G-Buffer results is to use a method OpenGL provides that allows you to copy over the contents of a framebuffer into another framebuffer. This is done using the glBlitFramebuffer function. You will need to read more into the parameters passed into the function. Here's an example of using the function:

    //Bind back to the screen's framebuffer 
    BindFramebuffer(); 
     
    //Clear the screen's depth and color buffers 
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
      
    // gFBO = framebuffer id for the G-Buffer. Declared somewhere in your code
    // This allows for reading out of the G-Buffer's fbo  
    glBindFramebuffer(GL_READ_FRAMEBUFFER, gFBO);
    
    /* The following code will place the texture assigned to GL_COLOR_ATTACHMENT0
     * in the top left quadrant of the screen. 
     */ 
    GLsizei HalfWidth = (GLsizei)(WindowWidth / 2.0f);
    GLsizei HalfHeight = (GLsizei)(WindowHeight / 2.0f);
    glReadBuffer(GL_COLOR_ATTACHMENT0); 
    glBlitFramebuffer(0, 0, WindowWidth , WindowHeight,
                        0, 0, HalfWidth, HalfHeight, GL_COLOR_BUFFER_BIT, GL_LINEAR);
    

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 perfrom 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 attachements 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 attachements 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. Also stencil buffer is connected with the stencil test , which is a per-fragment operation. 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:
  • Always pass
  • Always fail
  • Less/greater than
  • Less/greater than or equal
  • Equal
  • Not equal
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:
  • Keep the stencil value unchanged
  • Replace the stencil value with zero
  • Increment/decrement the stencil value
  • Invert the bits of the stencil value
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:
  • glEnable(...);
  • glDisable(...);
  • glClear(....);
  • glCullFace(...);