CS155 Computer Graphics

Reflection Lab

OpenGL does not have built-in support for reflections but there is technique to produce them.

  1. Create project:

    Download the zip file reflections.zip, unzip, open the visual studio solution, compile and run the program. You'll see a red floor, a blue cube, and a white background. (You might have to move the camera a bit to see the floor.)



  2. Created "reflected" cube:

    We can create a "reflected" cube by drawing a version of our original cube that is scaled by (1,-1,1). Add the appropriate code to display; be sure to push/pop the modelview matrix before the scale/after drawing the reflected cube. The results aren't impressive so far, but you should see a second cube on the other side of the floor.



  3. Blend "reflected" cube and the floor:

    Next we want to blend the "reflected" cube with the floor. Or do we want to blend the floor with the "reflected" cube? In the first case we'll have to worry about the order the sides of the cube are rendered (as we saw in the last tutorial). Let's try it the other way.

    1. If you are drawing the floor before the reflected cube, reverse the order
    2. Change the alpha on the floor to .50.
    3. Enable blending before drawing the floor and disable it afterwards
    4. Set up the blending function using GL_SRC_ALPHA and GL_ONE_MINUS_SRC_ALPHA.

    Recompile and run the program. You should now see the "reflected" cube on the floor. There are a couple of problems. First, the floor is now semi- transparent -- we can see the white background through it. Second, if you look under the floor you'll still see the other cube. We can solve both of these problems using the stencil buffer. We'll implement the approach we discussed in lecture.



  4. Eliminate "reflected" cube below floor:

    The stencil buffer is another buffer supported by OpenGL and GLUT that is useful for "tagging" pixels in a scene. We need to clear the stencil buffer just like we do the frame and depth buffers. Add the following to init.

    	// initialize stencil clear value
    	glClearStencil(0.0);
    
    In display, add the stencil buffer to the clear command.
    	glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);
    

    Next we want to draw the top of floor (provided it is visible) into the stencil buffer but not into the frame or depth buffers. (Why? We'll come back to this later.) To disable drawing to the frame and depth buffers add the following immediately after the glClear command in display.
    	// Don't update color or depth. 
    	glDisable(GL_DEPTH_TEST);
    	glColorMask(GL_FALSE, GL_FALSE, GL_FALSE, GL_FALSE);
    
    
    The glColorMask command allows us to turn off writing to, respectively, the red, green, blue, and alpha channels of the frame buffer. By default, these are all set to GL_TRUE.

    Next we need to enable stencil testing:

    	// enable stencil buffering
    	glEnable(GL_STENCIL_TEST);
    
    There are two functions that control the behavior with respect to the stencil buffer. Add the following:
    	// define stencil test
    	glStencilFunc(GL_GREATER , 1, 3);
    	
    	// define stencil operations
    	glStencilOp(GL_REPLACE, GL_REPLACE, GL_REPLACE);
    
    We'll come back to these functions and their effects in a moment.

    As discussed in lecture, we need to distinguish pixels that correspond to the top of the floor. Because we have to be able to handle various views -- from above and below -- we need to tell OpenGL to only draw the top of the floor into the stencil buffer. We can do this with back-face culling.

    	// cull back faces
    	glEnable(GL_CULL_FACE);
    	glCullFace(GL_BACK);
    

    With back-face culling enabled, OpenGL ignores any polygons that are facing away from the camera. Recall that the front face is determined by the order in which the vertices are defined; in our case the front faces of the floor polygons are facing up.

    Next we have to draw the floor.

    
    	// draw the floor
    	drawFloor()
    
    
    OpenGL processes each polygon of the floor in turn. For each, it first determines whether it is front or back-facing. If back-facing (i.e. the view is from the bottom), OpenGL ignores the polygons. If, on the other hand, the top of the floor is visible (i.e. the front faces), OpenGL projects the polygon vertices into screen space then uses its scan line algorithm to determine the pixels that are "in" the polygon. For each "in" pixel, OpenGL performs various tests and, depending on their outcome and the OpenGL state, writes to its various buffers.

    The pixel tests include the depth test (if enabled) and stencil test (if enabled). In our case only the stencil test is performed. The actual test is specified by glStencilFunc(passFunc,refVal,mask) command. When a pixel is tested, the reference value, is compared to the current value for that pixel in the stencil buffer. (The third argument, mask allows us to look at specific bits in the stencil buffer but we don't need that capability; using a mask of 3 means we'll consider the two low order bits of the stencile buffer, which is all we'll need in this tutorial.) In our case, the stencil buffer is cleared to 0 so our stencil test compares 1 to 0. The passFunc we have specified is GL_GREATER. Thus the stencil test will pass since 1>0.

    After performing the pixel tests, OpenGL checks its state to determine whether to write to the frame, depth, and stencil buffers.

    1. Frame buffer: In general, if the depth passes (or is disabled) and the stencil test passes (or is disabled), OpenGL will write to the frame buffer for all channels for which the colorMask is true. We've set them all to false, so nothing is written to the frame buffer.
    2. Depth buffer: In general, if depth-testing is enabled and the depth-test passes, OpenGL will write to the depth buffer. In our case, depth-testing is disabled so nothing is written to the depth buffer.
    3. Stencil buffer: The glStencilOp command specifies how the stencil buffer is changed depending on the outcomes of the stencil test and depth test as shown below.

      In our case, if the top of the floor is visible, then for all pixels "in" the floor, the stencil test will pass. Since the depth test is disabled, it too passes. So the only outcome we really care about is the third one; nevertheless we have to specify operations for all three cases. We use GL_REPLACE. This causes the value in the stencil buffer to be overwritten (replaced) by refVal.

    Therefore, after the drawFloor() command the frame buffer and depth buffers are unchanged. If the bottom of the floor is visible the stencil buffer is unchanged. But if the top of the floor is visible, the corresponding values in the stencil buffer are set to 2.

    To complete this section of code we need to reset OpenGL's state as follows:

    	// Re-enable update of color and depth. Disable culling. 
    	glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE);
    	glEnable(GL_DEPTH_TEST);
    	glDisable(GL_STENCIL_TEST);
    	glDisable(GL_CULL_FACE);
    
    The final step is to only draw the "reflected" cube for pixels that are tagged (i.e. stencil value is 1). Add the following just before drawing the "reflected" cube.
    	// draw reflected cube but only where top of floor
    	glEnable(GL_STENCIL_TEST);
    	glStencilFunc(GL_EQUAL, 1, 3);  
    	glStencilOp(GL_KEEP, GL_KEEP, GL_KEEP);
    
    Now our stencil test compares the reference value of 1 to the value in the stencil buffer. For tagged locations, this test will pass, for all other locations it will fail. Thus for a pixel of the "reflected" cube that passes the depth test (is not occluded by the other cube), the frame buffer is updated with its color and the depth buffer is updated with its depth. Becase glStencilOp specifies GL_KEEP, the values of the stencil buffer are kept; i.e. not overwritten.

    Finally we need to disable stencil tests after drawing the "reflected" cube. Add the following:

    	glDisable(GL_STENCIL_TEST);
    
    Recompile the program and run it. The "reflected" cube under the floor should no longer be visible.

  5. Next we want to fix the blending problem; i.e. we don't want to blend the floor except for pixels corresponding to the rendered "reflected" cube. Our strategy is the following:
    1. Tag the pixels corresponding to the "reflected" cube in the stencil buffer. Change the glStencilOp so that pixels corresponding to the rendered "reflected" cube have their stencil value reset to 2 (instead of keeping 1).
    2. Change the current blended floor code so that it only works for pixels with stencil value of 2.
    3. Draw the floor a second time without blending for those pixels with stencil valaue less than 2.



  6. Now that you understand the process, answer the following questions: When we first draw the floor into the stencil buffer, why is it important to disable writing to the frame buffer? Why is it important to disable depth testing?


Updated 10/2011.