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 can move the camera around to see under 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 after the floor-drawing code; 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:

    In order to see the reflected cube in the floor we need to use blending. OpenGL supports blending when rendering an object; rather than simply overwriting a pixel in the frame buffer you can combince the new value with the current value.

    To blend the cube with the floor do the following:

    1. First we need to enable blending. Add the following code before drawing the reflected cube.
      glEnable(GL_BLEND);
      
      After drawing the reflected cube we need to disable blending using the following code.
      glDisable(GL_BLEND);
      
    2. We also have to specify the blending weights. To do this we first need to set the alpha channel of the cube color. You may have noticed that colors can have a fourth channel; this alpha channel specifies the opacity of the color with 1 being fully opaque and 0 being fully transparent. Add the following code before drawing the reflected cube:
      cubeDiffuse[3]=0.5;
      cubeAmbient[3]=0.5;
      glMaterialfv(GL_FRONT_AND_BACK, GL_DIFFUSE, cubeDiffuse);
      glMaterialfv(GL_FRONT_AND_BACK, GL_AMBIENT, cubeAmbient);
      
    3. We have to tell OpenGL how to use the alpha channel of the cube. The glBlendFunc function takes two parameters; the first is the blending weight to use for the source (the object being rendered) and the second is the blending wieght for the destination (the frame buffer). OpenGL provides enormous flexibility for setting these factors; you can read more about them online. For our purposes the following suffices:
      glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
      
      When a pixel for the reflected cube is drawn to the frame buffer its value is averaged with what is already there.
    4. We are almost done! As our code now stands, however, the reflected cube will not be drawn when it is occluded by the floor, as in the initial camera configuration. We need to disable GL_DEPTH_TEST before drawing the reflected cube. (Be sure to enable it afterwards.)

    Run your program. You should see the reflected cube in the floor but the effect is far from perfect. One problem is that the reflected cube seems semi-transparent; you can see through the front faces to ones that are occluded. And the reflected cube is blended with the original, making the original seem semi-transparent. These problems arise because depth buffering is turned off. (Make sure you are clear on this.)

    There is, however, an easy fix; we can change the drawing order. After we draw the original cube, we can draw the reflected cube with depth buffering and no blending. This will elimate the problems arising from having depth buffering off. After both the original and reflected cubes are drawn, we can draw the floor with depth buffering off and blending on. We only want to blend with the reflected cube but this can be handled by using an appropriate blending function. Refactor your code accordingly but use the following blending function:

    glBlendFund(GL_ONE_MINUS_DEST_ALPHA,GL_DEST_ALPHA);
    
    Note that our clear color has an alpha of 0 so the floor is not blended with the background! And the original cube has an alpha of 1 so the floor is not blended with any of those pixels.

    Recompile and run the program. You should now see the "reflected" cube on the floor.

  4. Using the stencil buffer:
    There are still a couple of problem. Move the camera and look under the floor. From the underside we can see the 3d "reflected" cube. We can also see the original cube through the floor. To solve these problems we'll use the stencil buffer as discussed in class.

    The stencil buffer is another buffer supported by OpenGL and GLUT that is useful for "tagging" pixels in a scene. When drawing a pixel of the original or reflected cube we need to know whether the pixel location corresponds to background, top of the floor, or bottom of the floor.


    We'll use the stencil buffer to tag pixels corresponding to the background with 0, the top of the floor with 1, and the bottom of the floor with 2. First we'll work on the top of the floor.
    1. To initialize the stencil buffer to 0, we clear it just like the frame and depth buffers. Add the following to the init function.

      	
      // 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);
      
    2. Next we'll add code to only draw the top and not the bottom of the floor to the stencil buffer. Immediately before drawing the original cube add the following:
      // draw top of floor to stencil buffer
      // cull back faces
      glEnable(GL_CULL_FACE);
      glCullFace(GL_BACK);
      

      With backface culling turned on, OpenGL ignores and drawing commands for back faces. How does it know what are the front faces and what are back faces? Recall the graphics convention that the front of a triangle (and also quads) is the side from which the vertices are ordered counter-clockwise.

    3. We have ordered the drawing of the two cubes and the floor to accommodate blending. We don't want to screw that up. So when we draw the floor to the stencil buffer we don't want to change the frame or depth buffers. After the backface culling code add the following.
      	
      // Don't change frame buffer or depth 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.

    4. Next we have to enable the stencil test.
      	
      // enable stencil test
      glEnable(GL_STENCIL_TEST);
      
    5. When a pixel of the top of the floor is ready to be drawn, OpenGL conducts the stencil test in which it compares the corresponding value in the stencil buffer to a constant defined by the following command:
      // define stencil test
      glStencilFunc(GL_GREATER, 1, 3);
      
      The first argument is the test to apply and the second argument is the constant. In this case the stencil test will pass if the constant, 1, is greater than the value in the stencil buffer. Since we initalize the stencil buffer to 0, this test will always pass. The third argument is a mask; we can use different bits of the stencil buffer in different ways; for now we only need the two low order bit so a mask of 011 = 3 is perfect.

    6. The glStencilOp function tells OpenGL when and how the stencil buffer should be modified based on the outcome of the stencil test as well as the depth test. There are three cases:
      1. the stencil test fails
      2. the stencil test passes but the depth test fails
      3. the stencil test and the depth test pass (or the depth test is disabled)
      For each case, we can specify how we want the stencil buffer modified. Add the following
      // define stencil operations
      glStencilOp(GL_KEEP, GL_KEEP, GL_REPLACE);
      
      This tells OpenGL that in cases (a) and (b), the stencil buffer should keep its current value. In case (c), it should be replaced by the constant defined in the glStencilFun call; in our case a 1. The only case that can arise in this example is the final one because depth testing is disabled and the stencil test is designed to always pass while drawing top of floor pixels. The first two parameters of glStencilOp could be changed without effect.

    7. Now add the floor drawing code.
      drawFloor();
      
      As the floor is being rendered, the pipeline discovers pixels that would be "turned on" for the top floor. For each such pixel the stencil test is performed. Since the stencil buffer is initialized to 0 and our stencil test specifies a constant of 1, the stencil test passes. Since the depth test is disabled we are in case (c) above. The stencil operation says to replace the value in the stencil buffer for that pixel with a 1. Since the depth test and frame buffer are turned off, they are not changed. Therefore, the only thing that happens during this call to drawFloor is that the pixels corresponding to the top of the floor in the stencil buffer are set to 1. Note, if our current view is from below, no changes are made to the stencil buffer.

    8. Now we need to repeat the process for the bottom of the floor but setting the stencil buffer to 2.
      glCullFace(GL_FRONT);
      glStencilFunc(GL_GREATER, 2, 3);
      drawFloor();
      
    9. Now we can draw the original cube. First we need to re-enable the frame and depth buffers.
      glEnable(GL_DEPTH_TEST);
      glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE);
      
      As we draw the cube, we only want to draw pixels where the corresponding value in the stencil buffer is 0 or 1. The current setting for the glStencilFunc is exactly what we need so we don't have to reset it. We don't, however, want to change the stencil buffer so add:
      glStencilOP(GL_KEEP,GL_KEEP,GL_KEEP);
      
    10. Now we can use our existing code to draw the original cube. Afterwards we need to disable the stencil test.
      glDisable(GL_STENCIL_TEST);
      
    11. Compile and run your code. The original cube should look the same as before when viewed from above but should no longer be visible through the bottom of the floor. We still have to fix the problems with the reflected cube

    12. Before drawing the reflected cube, we want to reset glStencilFunc so that we only draw to pixels corresponding to the top of the floor. Do that then move the command that disables the stencil test after drawing the reflected cube. Re-compile and run your code. All should be well.


Updated March 2013