Site hosted by Angelfire.com: Build your free website today!

REAL TIME STENCIL SHADOWS WITH MULTIPLE LIGHTS


First, stencil shadows don't look realistic to me, but it's cool anyway and we didn't find the way to make soft shadows in real time at this rendering speed.

Ok... Why this tutorial? You don't know? It's simple : John Carmack and his team from id Software released a game called : Doom III this month. Oh, really? And what is so cool about this game? It's the shadows in real time of course. Now, I'm reading a lot of posts on forums over the internet from people asking questions on how to make real-time shadows in a game the way Doom3 did it. It's simple enough (for a person that totally understands OpenGL, linear mathematics and has a IQ of 140). Ok, I'm joking lol, it's more like IQ of 120.

Ok, since I have played and finished Doom 3, I made my own real time shadow engine looking like the one from that game. I know how they make it, and you want to know how to make it. After answering some posts and explaining the concept a couple of times, I decided to create this tutorial. The concept I will use is the same used in Doom 3 for shadow casting. It's called: Carmack's Reverse. He started first by making it like everyone tried, and came up with a bug: when the point of view goes inside a shadow volume, a bunch of ugly bugs appear on the shadows. So John Carmack (Technical Director and Doom3 Engine programer from id) came up with a solution: doing the opposite. That's simple, but it works.

This shadow engine uses stencil buffer, which is very slow. That's why Doom 3 runs slowly on many computers.

In this tutorial i will not talk about BSP optimisation, Per-Pixel Bump mapping, silhouhette definition, but only about real-time shadows like the ones used in Doom 3.

It's up to you to create a map with a 3D program and load it into your engine. I loaded something very simple. Here is my scene with a simple orange light in the center:

My light attenuation looks great, I know, I use a technic of mine to fake per pixel lighting. Do your lighting like you want. I suggest using the standart openGL lighting for now.

Ok, first I want to say something about shadows that you might think you know, but you probably don't. You think we will define the shadow area, and then draw those shadows. Do shadows get created like that in real life? No. In real life you see what the light sees. Shadows are just there because light can't get there. That is exactly what we will do here. We will of course define our shadow area, BUT, we will draw the lighting everywhere except where we defined our shadows. This way, we will be able to draw lighting from another light over a shadow that is already there. This is something we can't do with the other method. Here is how we will do it, first without code, just words.

So... We will first create three functions in our engine.

* The first will draw all the scene on the depth buffer (z-buffer) only.

* The second will compute the shadows for all lights and then apply the lights on the walls, except where we found shadows.

* The last function will draw all the scene without lighting, but with texture and modulate it with the scene already here.

That's it. It's done. So good luck, ciao.

...

I'm joking of course. :) I willl now explain each of these functions.

DrawOnDepthBuffer();

That's the first step. We render all the scene (all that we can see) without texture, and without drawing it onto the color buffer, (For those who don't know what a color buffer is, it's what you see on the screen) but we will enable the writing to the depth buffer. So now we have the scene, written in the z-buffer. Now, if you draw something, it will be occluded by the walls of the map just where they are, but you just won't see them.

 

DrawLighting();

Here, we will compute the shadows, the lighting and finally, draw them, all in this function. We start by passing in a loop all of the lights of the map (Only lights that can be seen by the player to avoiding lag :P). For each light, we have to pass all the triangles of the map in another loop (Now you should start to understand why Doom 3 runs so slowly :P). Now you have to project each one of those triangles away from the light, thus creating a shadow volume. So that's kind of simple... You take every vertex of the triangle, and you project it in the direction opposite to the light, to the distance of the light radius, or to the infinite if you want. So now you have your three new points. You create three quads around the original triangle to create a volume... but what is a "VOLUME"? That means something with a volume, so something without a hole. So you have to cap the end and the beginning of your new object with the original triangle and the projected triangle. Oh and, all the faces of the volume must face towards the outside, just like a real volume.

Now that you have your volume, you have to enable the stencil buffer test (stencil buffer is a buffer where you can increment or decrement its value and then specify to draw on a specific location on the screen). I'll explain it in more detail soon. So you draw only the back faces of your volume and you increment the stencil buffer if the depth test fails. After, you draw the front faces of your volume and decrement the stencil buffer if the depth test fails. That will determine all your shadows and then you just have to draw your lighting where there is a 0 on the stencil buffer.

Explanation of what is happening here:

What we got here is a light (the yellow thing), a green floor and a green triangle half-way between the light and the floor.

 

We now define our shadow volume for the triangle. (the red lines are the shadow volume) That includes the bottom and top cap.

 

Now that we have the shadow volume, we increment the back faces in the stencil buffer only if the depth test fails. So, imagine drawing the back faces of this volume; the two rear quads. We would probably get something like this :

 

We can clearly see the red quads (the back faces). But, we have to increment the stencil buffer where we see the quad. If it's 0, we put 1 everywhere its red :P. But what did I say? The Carmack's Reverse thing? We increment if the depth test fails, so where the quads are, but only the part that is hidden by the floor! We now have in the stencil buffer 1 in this area:

 

I put 0s and 1s to see the state of the stencil buffer. Now, I will draw the Front faces and Decrement the value from the z-buffer. Only if depth test fails again:

 

You can see that I drew the top cap, and its z-test failed against the triangle. It's important to erase the shadow if the triangle is highlighted. Here, 0 minus 1 gives 0. This value can't be negative. So now we only have the shadow zone with a 1 in it. That can be more than 1, depending of the complexity of the scene, but the important thing is we get 0 where there is no shadow. Now we draw our lighting everywhere we have 0:

 

But why do we draw a bottom cap? Because if you get Inside the shadow volume, and you look down, you will not see the shadow where the cap should be. That's why we're making caps. It avoids a lot of bugs. This way, the shadow is perfect.

So if we update my scene, we now get this:

 

DrawTextures();

This one is easy. You redraw your scene with texture On, lighting Off, and you modulate it with the current scene. Now my scene looks like this (I know, my textures are crapy. lol):

 

Now let's get into the Code. Yeah! Don't try to copy and paste it in your code; it will not work :P. I suggest putting this in a class named "clMap" or something like that ;). It's much more easier to finish a project when you have a clean code.


//
// The function call to draw the map
//

void DrawMap()
{
    // Drawing on the z-buffer
    DrawOnDepthBuffer(); // PASS 1

    // Drawing the lighting, shadow computation
    DrawLighting(); // PASS 2

    // Applying the textures
    DrawTexture(); // PASS 3
}


//
// We're drawing the scene only on the z-buffer
//

void DrawOnDepthBuffer()
{
    glEnable(GL_DEPTH_TEST); // Activate the depth test
    glEnable(GL_CULL_FACE); // Activate the culling
    glCullFace(GL_BACK);   // We are drawing front face
    glDisable(GL_TEXTURE_2D); // no texture here
    glDisable(GL_BLEND);   // no blending
    glDepthMask(GL_TRUE);  // Writing on z-buffer
    glColorMask(GL_FALSE, GL_FALSE, GL_FALSE, GL_FALSE);  // No writing on color buffer

    DrawScene(); // Your function to draw your scene
}


//
// Here is the big thing: creating our shadow volumes, updating the stencil buffer and drawing your lighting
//

void DrawLighting()
{
    glBlendFunc(GL_ONE, GL_ONE); // The blending function scr+dst, to add all the lighting
    glDepthMask(GL_FALSE);  // We stop writing to z-buffer now. We made this in the first pass, now we have it
    glEnable(GL_STENCIL_TEST); // We enable the stencil testing

    for (/*For all lights in your scene*/)
    {
        glDisable(GL_BLEND); // We don't want lighting. We are only writing in stencil buffer for now
        glClear(GL_STENCIL_BUFFER_BIT); // We clear the stencil buffer
        glDepthFunc(GL_LESS); // We change the z-testing function to LESS, to avoid little bugs in shadow
        glColorMask(GL_FALSE, GL_FALSE, GL_FALSE, GL_FALSE); // We dont draw it to the screen
        glStencilFunc(GL_ALWAYS, 0, 0); // We always draw whatever we have in the stencil buffer

        for (/*For all triangles in your scene*/)
        {
            // NOTE: If you draw a shadow volume for EVERY single triangle, it will
            // lag a lot. The common way to do this is to determine the silhouette of the object and
            // create the shadow volume from that. Here it's just for learning or for a very little scene ;)


            ComputeShadowVolume(); // Compute the shadow volume for this triangle

            glCullFace(GL_FRONT); // We are drawing the back faces first
            glStencilOp(GL_KEEP, GL_INCR, GL_KEEP); // We increment if the depth test fails

           
DrawShadowVolume(); // Draw the shadow volume of this triangle

            glCullFace(GL_BACK); // We are now drawing the front faces
            glStencilOp(GL_KEEP, GL_DECR, GL_KEEP); // We decrement if the depth test fails

            DrawShadowVolume(); // ** Edited ** Draw the shadow volume of this triangle
        }

        // We draw our lighting now that we created the shadows area in the stencil buffer
        glDepthFunc(GL_LEQUAL); // we put it again to LESS or EQUAL (or else you will get some z-fighting)
        glCullFace(GL_BACK); // we draw the front face
        glEnable(GL_BLEND); // We enable blending
        glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE); // We enable color buffer
        glStencilOp(GL_KEEP, GL_KEEP, GL_KEEP); // Drawing will not affect the stencil buffer
        glStencilFunc(GL_EQUAL, 0x0, 0xff); // And the most important thing, the stencil function. Drawing if equal to 0

        // Now, draw your scene with the lighting method you want
       
glEnable(GL_LIGHTING); // Let's use openGL lighting
       
glLight(GL_LIGHT0, .... ... ..); // set all the light parameters (color, attenuation, but no ambient)
       
DrawScene(); // Now draw your scene (we're still not writing to the depth buffer)
       
glDisable(GL_LIGHTING); // We dont want lighting anymore
    } // Now we do all of this for each light in the scene

    glDisable(GL_STENCIL_TEST); // Now we can disable the stencil buffer test
}
 


//
// Modulate the texture with the rest
//

void DrawTexture()
{
    glColor4f(1,1,1,1); // giving them white color
    glCullFace(GL_BACK); // Drawing front faces
    glDepthFunc(GL_LEQUAL); // we put it again to LESS or EQUAL (or else you will get some z-fighting).. annoying lol
    glEnable(GL_BLEND); // We enable blending
    glBlendFunc(GL_DST_COLOR,GL_ZERO); // We modulate: scr*dst
    glEnable(GL_TEXTURE_2D); // This time, we enable textures

    DrawScene(); // Drawing our scene with textures On

    glDepthMask(GL_TRUE); // And finally, we enable writing to z-buffer.
                          // If this is not Enabled, when you will Clear your depth buffer bit, it will not work
}


Doing it this way, if you put more than one light in your scene, it will work perfectly, like in the Doom 3 engine. Here is my updated scene with two lights:

 


Tutorial created by David St-Louis, proofread by Daniel Draghici
Montréal (Québec) Canada

E-mail : duktroa@hotmail.com