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