DevMaster.net  
Home | Forums | 3D Engines Database | Wiki | Articles/Tutorials | Game Dev Jobs | IRC Chat Network | Contact Us

OpenGL > General


Projective Shadows using Stencil Buffer          
09/08/2003

Introduction

In this article, I will explain how to render projective shadows in OpenGL using the stencil buffer. I will also talk about the advantages and disadvantages of this method. I do not guarantee that everything in this article is right and if you find anything wrong, please e-mail me.

Advantages

  • Casts a shadow onto a plane with good-looking results
  • Very fast for small scenes with one light and one plane to project the shadow onto

Disadvantages

  • Don’t allow concave objects to cast shadows on themselves or objects to cast shadows on each other.
  • Produces hard shadows. You can create soft shadows by moving the light source slightly and make multiple passes (a process known as jittering) and blending the results.
  • You can create shadows for several lights by making one pass for each light source, but the cost of doing this becomes high fast.
  • The same process as above can be made to process the shadows on several planes.

The Principles

Just as a reminder, Webster’s definition of a shadow is: “shade cast upon a surface by something blocking light” if you didn’t know :) The stencil buffer is not the same buffer as the Z buffer and depth buffer. The stencil buffer is used to restrict drawing to certain portions of the screen. The following are the steps to take when implementing projective shadows with stencil buffer:

  1. Limit the rendering of the shadows to the surfaces you want with the use of the stencil buffer.
  2. Create a shadow-projection matrix based on the position of the light and the plane equation of the surface to render to.
  3. Turn off lights and texture mapping, and set the color to black (we want a black shadow).
  4. Turn off depth test.
  5. Enable blending.
  6. Render all objects that you want to cast shadows.
  7. Remember to restore old settings.

Implementation

Step 1: Limiting the rendering of the shadows

We need to limit the area the shadow is cast on by enabling the stencil test. We don’t want the shadow to come outside the surface the shadow will be cast on.

Figure 1 – The shadow has been made red to show that it also is drawn outside the surface that it should

Figure 2 – A proper shadow that’s only cast on the surface

To only cast the shadow on the surface, we draw the surface in the stencil buffer and put a 1 every were we draw. After step one, we will only write to areas in the color buffer were the stencil buffer has a one until we disable the stencil test.

// turning off writing to the color buffer and depth buffer so we only 
// write to stencil buffer
glColorMask(GL_FALSE, GL_FALSE, GL_FALSE, GL_FALSE);
glDepthMask(GL_FALSE);

// enable stencil buffer
glEnable(GL_STENCIL_TEST);

// write a one to the stencil buffer everywhere we are about to draw
glStencilFunc(GL_ALWAYS, 1, 0xFFFFFFFF);

// this is to always pass a one to the stencil buffer where we draw
glStencilOp(GL_REPLACE, GL_REPLACE, GL_REPLACE);

// render the plane which the shadow will be on
// color and depth buffer are disabled, only the stencil buffer
// will be modified
DrawFloor(0,0,0);

// turn the color and depth buffers back on
glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE);
glDepthMask(GL_TRUE);

// until stencil test is diabled, only write to areas where the
// stencil buffer has a one. This is to draw the shadow only on
// the floor.
glStencilFunc(GL_EQUAL, 1, 0xFFFFFFFF);

// don't modify the contents of the stencil buffer
glStencilOp(GL_KEEP, GL_KEEP, GL_KEEP);

Step 2: Create a shadow-projection matrix based

The idea behind the algorithm is to cast a shadow from the object we want to make a shadow from, onto a plane from the lights perspective. The 4x4 matrix we get from this algorithm must be multiplied with the 'model view' matrix before we render the object. The code for the algorithm:

void SetShadowMatrix(float fDestMat[16], float fLightPos[4], float fPlane[4])
{
    float dot;
    
    // dot product of plane and light position
    dot = fPlane[0] * fLightPos[0] + 
          fPlane[1] * fLightPos[1] + 
          fPlane[2] * fLightPos[2] + 
          fPlane[3] * fLightPos[3];

     // first column
    fDestMat[0] = dot - fLightPos[0] * fPlane[0];
    fDestMat[4] = 0.0f - fLightPos[0] * fPlane[1];
    fDestMat[8] = 0.0f - fLightPos[0] * fPlane[2];
    fDestMat[12] = 0.0f - fLightPos[0] * fPlane[3];

    // second column
    fDestMat[1] = 0.0f - fLightPos[1] * fPlane[0];
    fDestMat[5] = dot - fLightPos[1] * fPlane[1];
    fDestMat[9] = 0.0f - fLightPos[1] * fPlane[2];
    fDestMat[13] = 0.0f - fLightPos[1] * fPlane[3];

    // third column
    fDestMat[2] = 0.0f - fLightPos[2] * fPlane[0];
    fDestMat[6] = 0.0f - fLightPos[2] * fPlane[1];
    fDestMat[10] = dot - fLightPos[2] * fPlane[2];
    fDestMat[14] = 0.0f - fLightPos[2] * fPlane[3];

    // fourth column
    fDestMat[3] = 0.0f - fLightPos[3] * fPlane[0];
    fDestMat[7] = 0.0f - fLightPos[3] * fPlane[1];
    fDestMat[11] = 0.0f - fLightPos[3] * fPlane[2];
    fDestMat[15] = dot - fLightPos[3] * fPlane[3];
}

Step 3: Turn off lights and texture mapping

We disable texture mapping so we don’t see the shadow with textures on it. We disable lights so the shadow is not lit.

Because the shadow is supposed to be drawn at the same level as the surface it is on, something that’s called “Z fighting” can occur, where the shadow can partially be behind the surface. This is because the depth values don’t have infinite precision. Solutions to this include the use of polygon offsets, stencil test and disabling the depth buffer. We have here disabled the depth buffer which will draw the shadow above everything else.

Step 4: Turn off depth test

If depth test is enabled, you can test if a new color that arrives for a pixel is closer to the window than the one already in the depth buffer. If it passes, it then replaces the value already in the depth buffer. For projective shadows, depth test must be off.

Step 5: Enable blending

We now enable blending so the shadow is blended with the texture of the surface. Blending in OpenGL allow you to have effects such as transparency into the scene. With transparency, you can simulate water, window and other objects that you can see through.

Step 6: Render all objects that you want to cast shadows

We simply draw the object that is casting the shadow.

Step 7: Restore old settings

Restore any changes made to draw the shadow.

After these seven steps, the object is then drawn normally and this is what the results look like:

Figure 3 – the final program

Summarize

I’ve used a lot of time on this article and it has helped me understand how to do one of the many types of implementations of shadows in OpenGL and I hope it has helped you too. Please e-mail me at arne9184@yahoo.com and tell me if this was helpful to you or if you have any comments or questions.

References

  • Kevin Hawkins, Dave Astle, OpenGL Game Programming, ISBN 0-7615-3330-3
  • Mason Woo, Jackie Neider, Tom Davis, Dave Shreiner, OpenGL Programming Guide Third Edition (Red Book), ISBN 0-2016-0458-2



Download source code for this article
Discuss this article in the forums
Print article

© 2003-2004 DevMaster.net. All Rights Reserved. Terms of Use & Privacy Policy Want to write for us? Click here