Have you ever wondered what shadows are? Well we all know that shadows are the indication that the light from a source has hit an obstacle. Then we see the shape of the obstacle on the surface where we normally expected to see the light. This means that the shadow is actually a projection of the object on the surface.
This is exactly the same as the projection of an object on the screen. Just imagine instead of using textures and colours we draw using only black. We would see only the shape of the object just like a shadow generated by a light in line with the screen and the object. All this is handled by the transformation matrix that is generated with the simple calls like 'glLoadIdentity' and 'glTranslatef'.
You understand that all you have to do in order to draw the shadow is to calculate and apply this projection matrix.
let's take a look at how to create the shadow matrix. This is a 4x4 matrix like any other matrix we use in OpenGL. In order to create the matrix we need the position of the light and the equation of the plain the shadow will fall on. The light position is something we all know. So I will describe the plane equation. This equation is of the form 'a*x + b*y + c*z + d = 0'. Our goal is to calculate the four coefficients (a, b, c, d). All we need is three points on the plane that are not on the same line. By applying their coordinates and solving the system we obtain the required coefficients. There is also a simpler method to calculate them. a, b and c are the coordinates of the normal vector of the plane while d is given by the formula 'd = -(a * Point3.x + b * Point3.y + c * Point3.z)'
Here is the code to generate the a shadow matrix.
// Given three points on a plane in counter clockwise order, calculate the unit normal void clf_getNormalVector(clf_vector3D& vP1, clf_vector3D& vP2, clf_vector3D& vP3, clf_vector3D& vNormal) { clf_vector3D vV1, vV2; vV1 = vP2 - vP1; vV2 = vP3 - vP2; // the cross product of two vectors is a vector perpendicular to their plane, the normal vector vNormal = crossProduct(vV1, vV2); vNormal.normalize(); // normal vector must be unit length, so normalize it } // Gets the three coefficients of a plane equation given three points on the plane. void clf_getPlaneEquation(clf_vector3D& vPoint1, clf_vector3D& vPoint2, clf_vector3D& vPoint3, clf_vector4& vPlane) { // Get normal vector from three points. The normal vector is the first three coefficients // to the plane equation... clf_vector3D norm; clf_getNormalVector(vPoint1, vPoint2, vPoint3, norm); vPlane[0] = norm.x; vPlane[1] = norm.y; vPlane[2] = norm.z; // Final coefficient found by back substitution vPlane[3] = -(vPlane[0] * vPoint3.x + vPlane[1] * vPoint3.y + vPlane[2] * vPoint3.z); } void clf_matrix4x4::MakeShadowMatrix(clf_vector3D vPoints[3], clf_vector4& vLightPos) { clf_vector4 planeEq; clf_getPlaneEquation(vPoints[0], vPoints[1], vPoints[2], planeEq); float a = planeEq[0]; float c = planeEq[2]; float d = planeEq[3]; float dx = vLightPos[0]; float dy = vLightPos[1]; float dz = vLightPos[2]; // Now build the projection matrix data[0] = b * dy + c * dz; data[1] = -a * dy; data[2] = -a * dz; data[3] = 0.0; data[4] = -b * dx; data[5] = a * dx + c * dz; data[6] = -b * dz; data[7] = 0.0; data[8] = -c * dx; data[9] = -c * dy; data[10] = a * dx + b * dy; data[11] = 0.0; data[12] = -d * dx; data[13] = -d * dy; data[14] = -d * dz; data[15] = a * dx + b * dy + c * dz; }
The shadow matrix needs to be calculated every time the light moves relative to the projection plane. Then when we draw the frame we draw the plane, we disable the light and the depth test apply the shadow matrix and draw the object, and then we restore the drawing mode and draw the object itself.
void frame_render() { // clear the screen glClearColor (0.f, 0.f, 0.2f, 1.f); // background colour glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); // clear screen and depth buffer glMatrixMode (GL_MODELVIEW); // select the modelview matrix glLoadIdentity (); // reset // position the viewer g_cam.apply(); // enable lighting and position the light g_light.enable(); g_light.apply(); // draw the floor glColor3f(1.0f, 1.0f, 1.0f); clf_enable_textures(g_tex_floor); g_floor.render(); // draw the object shadow // save current drawing state glPushAttrib(GL_ALL_ATTRIB_BITS); // prepare environment clf_disable_textures(); glDisable(GL_DEPTH_TEST); glDisable(GL_LIGHTING); // save transformation matrix glPushMatrix(); // Multiply by shadow projection matrix (apply shadow transformation) glMultMatrixf(shadow_mat); // and now apply the object transformations (translation and rotation) glTranslatef(pos.x,pos.y,pos.z); glRotatef(angley, 0,1,0); // set color to black glColor3f(.0f, .0f, .0f); // and draw drawBox(size.x, size.y, size.z); // Restore the projection to normal glPopMatrix(); // restore drawing mode glPopAttrib(); // draw the object clf_enable_textures(g_tex_box); g_object.render(); }
It is not that hard to draw the shadow of an object after all. The only drawback is that it is time consuming because you draw everything twice. This is the reason why it was not used so much in the past when the hardware was not so powerful. On more modern implementations light and shading is handled by the shaders. This is more complicated and will be covered in a later article. The current article was meant to be an introduction to the use of shading and shadows.
You can download the full demo project here.