| DevMaster.net |
| Home | Forums | 3D Engines Database | Wiki | Articles/Tutorials | Game Dev Jobs | IRC Chat Network | Contact Us | |
| Real-time Shadowing Techniques |
|
|
|
06/10/2003
|
||
Plane Projected ShadowsPlane projected shadows is a technique that uses 2 rendering passes to project a mesh unlit on a plane, so that it looks like a shadow. A screenshot of a demo I made for this:
In the first pass, the scene is rendered (light-source, torus and plane). Then in the second pass, the shadow is rendered via plane projection. See the Shadow Projection in OpenGL article for a mathematical theory of how it works. Firstly we must create an array in which we put the data for the shadow projection matrix. With this matrix we'll project the scene on the plane later on, and create the shadow this way. We must also define our light-position: GLfloat lightPos[] = { -20.0f, 50.0f, 0.0f, 0.0f };
We need this position to calculate the projection matrix. We also need to define three points lying on the projection plane. GLfloat points[3][3] = {{ -50,-50, 50 }, { 50,-50, 50 },{ 50,-50,-50 }};
With this data, we can determine our shadow projection matrix, with the following function: void MakeShadowMatrix(GLfloat points[3][3], GLfloat lightPos[4], GLfloat destMat[4][4]) { GLfloat planeCoeff[4]; GLfloat dot; // Find the plane equation coefficients // Find the first three coefficients the same way we find a normal. calcNormal(points,planeCoeff); // Find the last coefficient by back substitutions planeCoeff[3] = - ((planeCoeff[0]*points[2][0]) + (planeCoeff[1]*points[2][1]) + (planeCoeff[2]*points[2][2])); // Dot product of plane and light position dot = planeCoeff[0] * lightPos[0] + planeCoeff[1] * lightPos[1] + planeCoeff[2] * lightPos[2] + planeCoeff[3] * lightPos[3]; // Now do the projection // First column destMat[0][0] = dot - lightPos[0] * planeCoeff[0]; destMat[1][0] = 0.0f - lightPos[0] * planeCoeff[1]; destMat[2][0] = 0.0f - lightPos[0] * planeCoeff[2]; destMat[3][0] = 0.0f - lightPos[0] * planeCoeff[3]; // Second column destMat[0][1] = 0.0f - lightPos[1] * planeCoeff[0]; destMat[1][1] = dot - lightPos[1] * planeCoeff[1]; destMat[2][1] = 0.0f - lightPos[1] * planeCoeff[2]; destMat[3][1] = 0.0f - lightPos[1] * planeCoeff[3]; // Third Column destMat[0][2] = 0.0f - lightPos[2] * planeCoeff[0]; destMat[1][2] = 0.0f - lightPos[2] * planeCoeff[1]; destMat[2][2] = dot - lightPos[2] * planeCoeff[2]; destMat[3][2] = 0.0f - lightPos[2] * planeCoeff[3]; // Fourth Column destMat[0][3] = 0.0f - lightPos[3] * planeCoeff[0]; destMat[1][3] = 0.0f - lightPos[3] * planeCoeff[1]; destMat[2][3] = 0.0f - lightPos[3] * planeCoeff[2]; destMat[3][3] = dot - lightPos[3] * planeCoeff[3]; } I did not create this function myself, but it is available on the CD-ROM with the book OpenGL Superbible (Waite Group Press). What this function does is calculate via which matrix we can project a 3Dscene to a plane. We firstly define this projection-matrix, before we start to render, as follows: MakeShadowMatrix(points, lightPos, shadowMat); If you look at the function above, you see that the matrix is being stored in the shadowMat array. Let’s go to the render function: void Render()
{
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
glMatrixMode(GL_MODELVIEW);
glLoadIdentity();
glTranslatef(0,10,-150);
glRotatef(rot2,0,0.1f,0);
glRotatef(rot3,0,0,0.1f);
glPushMatrix();
//Draw the scene
glEnable(GL_LIGHTING);
glEnable(GL_LIGHT0);
DrawShadowCasters(false);
DrawLightPos();
DrawPlane();
glDisable(GL_LIGHTING);
glDisable(GL_DEPTH_TEST);
glDisable(GL_LIGHTING);
glPushMatrix();
// Multiply with the shadow projection matrix.
glMultMatrixf((GLfloat *)shadowMat);
glScalef(2,0,2);
DrawShadowCasters(true);
glPopMatrix();
glEnable(GL_DEPTH_TEST);
glEnable(GL_LIGHTING);
glPopMatrix();
rot2+=0.1f;
// show frames per second
fontDrawString(10, Settings.scrheight-20, "FPS: %0.1f", UpdateFPS());
glFlush();
glSwapBuffers();
}
So, we first render the actual scene, which is lighted. Then we turn off depth-testing, because otherwise we would get geometry fighting. I mean by this that OpenGL will not be sure which of the two, the shadow or the plane, should be on top. If we turn off depth-testing, things that render last lie the most on top. Also, lighting is being turned off, because we want a black shadow. We multiply the current matrix with our shadow-projection matrix and render the scene again (that is, only the torus), this time with no lights on. This way, the shadow is rendered on the plane. Projected ShadowsAnother technique to show shadows is projected shadows. Here, a texture is created from the point of view of the light source. This texture is a black and white texture, containing the shadows. So instead of a flat rendered mesh like with plane projected shadows, rendering to texture is used. A huge advantage of this technique when looking at plane projected shadows, is that you can cast a shadow on a mesh located in the shadow itself. Here is a screenshot of projected shadows:
The white dot is the light source. The teapot is the shadow caster. You can clearly see that the shadow also falls on the torus (donut). If you take a good look you can see pixel blocks on the shadow, which shows that the shadow is made with a projected texture. The texture we're going to use to project the shadow is initialized in a way that is quite similar to the way used with "rendering to texture": // Create the shadow texture object glGenTextures(1, &shadow); glEnable(GL_TEXTURE_2D); glBindTexture(GL_TEXTURE_2D, shadow); glCopyTexImage2D(GL_TEXTURE_2D, 0, GL_LUMINANCE, 0, 0, S_SIZE, S_SIZE, 0); glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_MODULATE); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP); // Prepare the texture matrix for the projected texture glMatrixMode(GL_TEXTURE); glLoadIdentity(); glTranslatef(0.5, 0.5, 0); glScalef(0.5, 0.5, 1); gluPerspective(90, 1, 1, 1000); glPushMatrix(); glMatrixMode(GL_MODELVIEW); // Set up TexGen glEnable(GL_TEXTURE_GEN_S); glEnable(GL_TEXTURE_GEN_T); glEnable(GL_TEXTURE_GEN_R); glEnable(GL_TEXTURE_GEN_Q); glTexGeni(GL_S, GL_TEXTURE_GEN_MODE, GL_EYE_LINEAR); glTexGeni(GL_T, GL_TEXTURE_GEN_MODE, GL_EYE_LINEAR); glTexGeni(GL_R, GL_TEXTURE_GEN_MODE, GL_EYE_LINEAR); glTexGeni(GL_Q, GL_TEXTURE_GEN_MODE, GL_EYE_LINEAR); Only here, we go a step further, and prepare the texture-matrix for projected textures by scaling and positioning them in the right way. After that, we also define the way the shadow texture will be mapped onto the rendered scene via texGen. The update of the shadow texture goes like this: void UpdateShadow() { // Set a square viewport for the texture glViewport(0, 0, S_SIZE, S_SIZE); glMatrixMode(GL_PROJECTION); glLoadIdentity(); gluPerspective(90, 1, 1, 1000); glMatrixMode(GL_MODELVIEW); // Render the teapot black-on-white glDisable(GL_LIGHTING); glColor3f(0, 0, 0); glClearColor(1, 1, 1, 1); glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); glLoadIdentity(); gluLookAt(cam[0],cam[1],cam[2],cam[3],cam[4],cam[5],cam[6],cam[7],cam[8]); DrawTeapot(); glBindTexture(GL_TEXTURE_2D, shadow); glCopyTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 0, 0, S_SIZE, S_SIZE); // Re-enable regular lighting glEnable(GL_LIGHTING); glColor3f(1, 1, 1); glClearColor(0, 0, 0, 1); // Reset view Resize(Settings.scrwidth, Settings.scrheight); } As you can see, the scene is rendered here without lighting from the light's position, after which a render tot texture is done. After updating the shadow texture, the final rendering is done, like this: void Render() { //first we update the shadow UpdateShadow(); glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); glLoadIdentity(); glTranslatef(-15, 10, -100); glRotatef(30,1,0,0); // Draw the light source glDisable(GL_LIGHTING); glBegin(GL_POINTS); glVertex4f(cam[0],cam[1],cam[2],1); glEnd(); glEnable(GL_LIGHTING); DrawLine(); // Set the TexGen planes glTexGenfv(GL_S, GL_EYE_PLANE, PS); glTexGenfv(GL_T, GL_EYE_PLANE, PT); glTexGenfv(GL_R, GL_EYE_PLANE, PR); glTexGenfv(GL_Q, GL_EYE_PLANE, PQ); // Draw the shadow caster glMaterialfv(GL_FRONT_AND_BACK, GL_DIFFUSE, GREEN); glMaterialfv(GL_FRONT_AND_BACK, GL_SPECULAR, WHITE); DrawTeapot(); // Draw the shadow receivers using the projected shadow texture glEnable(GL_TEXTURE_2D); glBindTexture(GL_TEXTURE_2D, shadow); glMatrixMode(GL_TEXTURE); glPopMatrix(); glPushMatrix(); // Set the projector's position and orientation gluLookAt(cam[0],cam[1],cam[2],cam[3],cam[4],cam[5],cam[6],cam[7],cam[8]); glMatrixMode(GL_MODELVIEW); glMaterialfv(GL_FRONT_AND_BACK, GL_DIFFUSE, RED); glMaterialfv(GL_FRONT_AND_BACK, GL_SPECULAR, BLACK); DrawFloor(); glMaterialfv(GL_FRONT_AND_BACK, GL_DIFFUSE, BLUE); glMaterialfv(GL_FRONT_AND_BACK, GL_SPECULAR, WHITE); DrawDonut(); glDisable(GL_TEXTURE_2D); } Shadow casting objects are rendered first. After that, the texture-matrix created at the initialization is called to project the shadow-texture correctly on the objects receiving the shadow. The big problem with projected shadows is that you need to render hierarchically to render shadow-receiving objects correctly with their shadow. This means a lot of extra calculation. Also, self-shadowing is not possible. Shadow MappingShadow mapping is a fairly recent technique, that is, it is not until recently that it is possible to do it in hardware. This technique renders the scene from the light-source to the texture. One important difference is that we render a depth-texture and other data is rendered to the texture than, after that, with projected shadows. To generate a shadow-map texture, we need an extension this time, which is GL_SGIX_depth_texture. You can use the extgl library. Here is a screenshot of a shadow mapping demo from SGI. I adapted this demo, because it was not suitable for compilation on visual C++ and windows. The original version can be found here: http://www.sgi.com/software/opengl/advanced96/tomcat/shadowmap.c
On this screenshot you can see the shadow mapping demo. You can clearly see that shadow mapping also does self shadowing. In this converted example from SGI, the shadow mapping is not optimal. You can clearly see errors in the mapped shadow texture. But we are talking about an absolute minimum configuration to render simple shadow-maps here. With shadow mapping, texture coordinates are generated using texgen, which are identical to the vertex coordinates. Using the texture matrix, these coordinates are transformed back to light coordinates. The depth values are available as texture coordinates. Here is a screenshot of a shadow-mapping demo from the nVidia developer’s site. With this demo, you can clearly see the beautiful and correct self shadowing. This demo does use a lot more extensions and advanced OpenGL features than my demo does however.
Vertex ProjectionVertex projection works in a way very similar to plane projected shadow, with one difference, which is that you calculate the shadow yourself here.
Using this technique, you project each vertex of your object to the ground. This means that self-shadowing and shadow-receivers are not possible with this technique. This technique is very simple, and performs very well for objects with not too many vertices. The formula is as follows: void shadow_poly(Point3D p[], Point3D s[], Point3D l, int num_verts) { for (int i=0; i < num_verts; i++) { s[i].x = p[i].x – (p[i].y / l.y) – l.x; s[i].z = p[i].z – (p[i].y / l.y) – l.z; } } Here, s is the array with the projected vertices. P is the array with the original vertices, and I is the position of the light. So you calculate new X and Z coordinates from the shadow. For Y, you use the Y-position of your plane. So this is very simple. Note that you can only project onto a plane. With objects that have more vertices, you'd have to also use occlusion culling from the position of the light source, in such a way that you don’t calculate too many vertices. Shadow VolumesShadow volumes are an increasingly popular way to render correct shadows in real-time. In OpenGL, these kinds of shadows are calculated via the stencil buffer. So what is a shadow volume? I’ll explain this using the following figure:
With shadow volumes, the volume of the shadow is determined. In fact, a mesh is defined that determines the shadow. On this picture, that is the pyramid. It starts from the point where the shadow is cast. With the volume and the stencil buffer, we can determine from the spectator's position what needs to be lit and what needs to be unlit. A good thing to know is that the use of the stencil-buffer doesn't cost anything extra in performance, as long as depth testing is turned on. A consequence of this technique is that when you use high-resolution meshes, a lot of calculations need to be done to determine the shadow volume, which causes an enormous performance drop. For real-time rendering of games this will most times not be a big problem, because in games the geometry, like objects, buildings, etc, are always heavily optimized for performance. Because of that, in most games no high resolution meshes will be used. Here is a screenshot from a demo from nVidia, demonstrating infinite shadow volumes. The volume was made visible using the visible blended yellow planes you can see behind the mesh.
A PowerPoint presentation about the stencil buffer and shadow volumes can be found at http://developer.nvidia.com/docs/IO/1407/ATT/stencil.ppt Comparison of Various Shadowing MethodsI just discussed several techniques to render shadows. There are a lot more real-time and non-real-time techniques to do that, but I discussed only the most important and most used techniques for real-time rendering. All of the discussed shadow techniques also have the possibility to be used for dynamic light sources. Now, I will present a comparison of the techniques I discussed:
All techniques clearly have their good and bad sides. Which means it is very important to study which method would work best for your product. For example, it is useless to program for hours on a scene that calculates shadow volumes for only one quad casting a shadow. In this case, you'd better use vertex projection. The easiest and quickest way to cast correct shadows with self-shadowing and shadow receivers, is depth mapping (= shadow mapping). This method requires only some minimal calculations. The big consequence is that if you want shadows with a good resolution, you need a fairly big texture. And a bigger texture also means a serious drop of performance. Also, the extensions needed for shadow mapping are not available on all hardware. The method creating the most beautiful and correct shadows is shadow volumes. The big problem with shadow volumes is the huge amount of calculations that you need to do for meshes with a high resolution. But once you determined the volume, you don’t need to re-calculate it, unless you’ve got a dynamic light-source, or a moving mesh. But why would one use this technique for static light-sources? You'd better use lightmaps. Here is a link where John Carmack, from the almost legendary Id Software, gives his vision and preference around stencil shadows vs. depth maps (shadow mapping): http://www.hardcore3d.net/e_hardcore3d/Res/OpenGL/Shadow/CarmackOnShadowVolumes.txt So one of the most important persons in the area of real-time rendering for games prefers shadow volumes above the low-res shadow maps. This is clear because of the more correct and beautiful shadows you get via shadow volumes. However, you can combine different methods, to get a hybrid method. Hybrid ShadowsHybrid shadows is the rendering of shadows using combined shadowing techniques. These mixed techniques can be a good way between the two mixed ones.
A good example of this is shadow volume reconstruction. This technique combines the two most important techniques we discussed, which are depth shadow mapping and shadow volumes. Here, via the depth buffer a depth texture is rendered (see first picture above), after which the contour of the shadow on the texture is determined using the depth values on the texture (second picture above). With this contour and the light position, you can reconstruct a volume, which can be used as a shadow volume (third picture above). This hybrid technique is not as beautiful and correct as shadow volumes, but it is way more beautiful than shadow mapping. It is a bit slower than shadow mapping, but much quicker than shadow volumes. Which means it is a perfect combination. The details of the shadow volume will lie in the generated shadow map. You can get the edges of the shadow better via edge detection. |
|
|
| © 2003-2004 DevMaster.net. All Rights Reserved. Terms of Use & Privacy Policy | Want to write for us? Click here |