Generating Mesh Shadows On Terrain Using OpenGL
From GPWiki
The wiki is now hosted by GameDev.NET at wiki.gamedev.net. All gpwiki.org content has been moved to the new server. However, the GPWiki forums are still active! Come say hello.
[edit] IntroductionIf you have ever tried to search the internet for tutorials on generating pre-baked mesh shadows on terrain, you probably had no luck. There are few if any resources on this subject. This tutorial demonstrates an efficient method for generating mesh shadows on terrain using OpenGL. The technique demonstrated here is similar to shadow mapping or projective shadow textures. The only difference is that we must perform the shadow mapping on a large scale and store the information in a single texture for use later. [edit] AlgorithmThe basic algorithm for this method is as follows:
To demonstrate this, consider the follow scene. Now suppose our light source direction is directed down at the terrain, lets say a vector direction of (-1, -1, -1), as shown in the image above on the right. Then using the above algorithm we would first setup an orthographic projection, then transform the scene using the light's direction vector. Then finally make sure the terrain is within view. We then get the first image shown below. We then transform the scene to be directly above the terrain and take the previous image and project it onto the terrain itself, as shown in the second image below. Our final result is shown below. [edit] Implementation DetailsThere are a few details to consider before diving into the source code. First, we must take into account that it is probably a good idea to break apart the rendering of scene meshes into different pieces or sections. This is because we probably want a lightmap resolution of 2048x2048 or greater, and our window viewport more than likely will not be this size. And since all rendering is done to the frame buffer, we must divide up our lightmap rendering into different sections that fit inside the viewport. Now we must figure out how much of the terrain each lightmap section or 'chunk' covers. For instance if our terrain is 256x256 vertices, and we wish to generate a lightmap resolution of 1024x1024. Then each 128x128 lightmap chunk covers exactly 32x32 vertices of the terrain. So we declare and initialize the following variables along with the following function header: void MeshShadows(unsigned char *lightmap, int lightmapSize, unsigned char shadowColor[3], float lightDir[3]) { // variable initialization int lightmapChunkSize = 128; if(lightmapSize < lightmapChunkSize) lightmapChunkSize = lightmapSize; float terrainDivisions = lightmapSize / lightmapChunkSize; int terrainChunkSize = Terrain.size() / terrainDivisions; Now we need to allocate memory for each lightmap chunk. unsigned char *chunk = new unsigned char[(lightmapChunkSize)*(lightmapChunkSize)*3]; Then we need to create an OpenGL texture object for each chunk. This is faster than simply copying the frame buffer's pixels to an array. Instead the frame buffer is copied directly to GPU memory using glCopyTexSubImage2D. This is must faster than using glReadPixels. // create shadow texture GLuint shadowChunk; glGenTextures(1, &shadowChunk); glBindTexture(GL_TEXTURE_2D, shadowChunk); glEnable(GL_TEXTURE_2D); glCopyTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, 0, 0, lightmapChunkSize, lightmapChunkSize, 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); Now we need to create variables for figuring out where we are on the terrain. Specifically what 'part' of the terrain we need to render shadows for. So we declare the following: int terrainCol = 0; int terrainRow = 0; float x0 = 0; float y0 = 0; float x1 = terrainChunkSize; float y1 = terrainChunkSize; Now we setup our loops to loop through each terrain section or 'division'. for(terrainRow = 0; terrainRow < terrainDivisions; terrainRow++) { for(terrainCol = 0; terrainCol < terrainDivisions; terrainCol++) { // setup orthogonal view glViewport(0, 0, terrainChunkSize * Terrain.size(), terrainChunkSize * Terrain.size()); glMatrixMode(GL_PROJECTION); glLoadIdentity(); glOrtho(0, terrainChunkSize, 0, terrainChunkSize, -10000, 10000); glMatrixMode(GL_MODELVIEW); // apply light's direction to modelview matrix gluLookAt(lightDir[0], lightDir[1], lightDir[2], 0, 0, 0, 0, 1, 0); What we have done is setup the viewport to orient around the area (0, 0) - (width, height), where width and height both equal terrainChunkSize * Terrain.size(). We then apply the light's direction vector to the model view matrix to orient the scene properly. The next step is to re-orient the viewport so that our current terrain section or 'chunk' is within the entire viewport. To do this we loop through each vertex in the current terrain chunk and find the maximum and minimum screen projected coordinates. // loop through all vertices in terrain and find min and max points with respect to screen space float minX = 999999, maxX = -999999; float minY = 999999, maxY = -999999; double X, Y, Z; // get pointer to terrain vertices float *vertices = Terrain.vertices()->lock(); for(int i = y0-1; i < y1+1; i++) { if(i < 0) continue; for(int j = x0-1; j < x1+1; j++) { if(j < 0) continue; int index = i * Terrain.size() + j; // get screen coordinates for current vertex static GLint viewport[4]; static GLdouble modelview[16]; static GLdouble projection[16]; static GLfloat winX, winY, winZ; glGetDoublev( GL_MODELVIEW_MATRIX, modelview ); glGetDoublev( GL_PROJECTION_MATRIX, projection ); glGetIntegerv( GL_VIEWPORT, viewport ); gluProject(vertices[index*3+0], vertices[index*3+1], vertices[index*3+2], modelview, projection, viewport, &X, &Y, &Z); if(X < minX) minX = X; if(X > maxX) maxX = X; if(Y < minY) minY = Y; if(Y > maxY) maxY = Y; } } The reason we must project every single vertex onto the screen is that simply taking the corners of the terrain chunk and projecting these will not suffice. One vertex in the center of the chunk may have a high y coordinate value, and so when projected will give a much greater screen value than one of the corners. // copy min and max values static float minX2, minY2, maxX2, maxY2; minX2 = minX; minY2 = minY; maxX2 = maxX; maxY2 = maxY; // clear screen glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); glViewport(0, 0, lightmapChunkSize, lightmapChunkSize); // orient viewport so that terrain chunk fits inside glMatrixMode(GL_PROJECTION); glLoadIdentity(); glOrtho(minX, maxX, minY, maxY, -10000, 10000); glMatrixMode(GL_MODELVIEW); // apply light's direction vector to model view transformation glLoadIdentity(); gluLookAt(lightDir[0], lightDir[1], lightDir[2], 0, 0, 0, 0, 1, 0); The next step is that we need to 'occlude' any scene meshes hidden by the terrain itself. Because of the way projective texturing works, if the light source is looking towards a hill on the terrain and a scene mesh is behind it, the shadow will appear on the hill, even though it should not. In the image below, the light source direction is given by the yellow arrrow. As you can see when we orient the scene to be from the light source's view, the hill should occlude the tree. Now if we simply render all the scene meshes without occluding them, our final projected texture onto the terrain will give the following result. As you can see in the second image, the shadow cast by the tree is correct. But because of projective texture the shadow also appears in the wrong place on the terrain. So the next part of the code renders the terrain to the depth buffer only, so that when the scene meshes are rendered, any part behind the hill fails the z test. // disable writing to the color buffer glColorMask(false, false, false, false); // render terrain Terrain.render(); // enable writing to the color buffer glColorMask(true, true, true, true); // render scene meshes '''BLACK''' RenderAllSceneMeshes(); The next step is to copy what we have just rendered to the screen to the previously created OpenGL texture object. // bind shadowChunk texture and copy frame buffer data glBindTexture(GL_TEXTURE_2D, shadowChunk); glCopyTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 0, 0, lightmapChunkSize, lightmapChunkSize); glBindTexture(GL_TEXTURE_2D, 0); The next step is to reposition the viewport to be directly above our current terrain chunk and project the texture we just created onto the terrain. glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); glViewport(0, 0, terrainChunkSize * Terrain.size(), terrainChunkSize * Terrain.size()); glMatrixMode(GL_PROJECTION); glLoadIdentity(); glOrtho(0, 0, terrainChunkSize, terrainChunkSize, -10000, 10000); glMatrixMode(GL_MODELVIEW); // rotate view so that xz plane becomes the xy plane glLoadIdentity(); glRotatef(90, 1, 0, 0); Again we must find the min and max values for when the current terrain chunk's vertices are projected onto the screen. Remember, this is because we need the terrain chunk to fit entirely within the window's viewport. But this time instead of looping through each vertex, we can simply check the vertices at each edge of the terrain chunk. This is due to the fact that the piece of terrain we are looking at is completely flat in the viewport since we are staring directly down at it. // reset max and min values minX = minY = 999999; maxX = maxY = -999999; glGetDoublev( GL_MODELVIEW_MATRIX, modelview ); glGetDoublev( GL_PROJECTION_MATRIX, projection ); glGetIntegerv( GL_VIEWPORT, viewport ); // project each corner onto the screen // the corners are represented by the x0, y0, x1 and y1 values gluProject(x0, y0, modelview, projection, viewport, &X, &Y, &Z); if(X < minX) minX = X; if(X > maxX) maxX = X; if(Y < minY) minY = Y; if(Y > maxY) maxY = Y; gluProject(x1, y0, modelview, projection, viewport, &X, &Y, &Z); if(X < minX) minX = X; if(X > maxX) maxX = X; if(Y < minY) minY = Y; if(Y > maxY) maxY = Y; gluProject(x1, y1, modelview, projection, viewport, &X, &Y, &Z); if(X < minX) minX = X; if(X > maxX) maxX = X; if(Y < minY) minY = Y; if(Y > maxY) maxY = Y; gluProject(x0, y1, modelview, projection, viewport, &X, &Y, &Z); if(X < minX) minX = X; if(X > maxX) maxX = X; if(Y < minY) minY = Y; if(Y > maxY) maxY = Y; // resize and re-orient the viewport glViewport(0, 0, terrainChunkSize * Terrain.size(), terrainChunkSize * Terrain.size()); glMatrixMode(GL_PROJECTION); glLoadIdentity(); glOrtho(minX, minY, maxX, maxY, -10000, 10000); glMatrixMode(GL_MODELVIEW); // rotate view so that xz plane becomes the xy plane glLoadIdentity(); glRotatef(90, 1, 0, 0); Now that we have the terrain chunk directly in the viewport, the next step is to project the created shadow texture onto the terrain chunk. This involves setting up projective texturing using OpenGL. Since projective texturing is not the main scope of this tutorial, the inner workings of it will not be discussed. // setup projective texturing float PS[] = {1, 0, 0, 0}; float PT[] = {0, 1, 0, 0}; float PR[] = {0, 0, 1, 0}; float PQ[] = {0, 0, 0, 1}; 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); 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); // setup texture matrix glBindTexture(GL_TEXTURE_2D, shadowChunk); glMatrixMode(GL_TEXTURE); glLoadIdentity(); glTranslatef(0.5, 0.5, 0); glScalef(0.5, 0.5, 1); glOrtho(minX2, maxX2, minY2, maxY2, -10000, 10000); gluLookAt(lightDir[0], lightDir[1], lightDir[2], 0, 0, 0, 0, 1, 0); glMatrixMode(GL_MODELVIEW); As you can see, we needed our copied min and max values for setting up the texture projection matrix when projecting the shadow texture onto the terrain. The next step is to render the terrain to the viewport using the projected shadow texture and copy the frame buffer back into the shadow texture. // render the terrain Terrain.render(); glBindTexture(GL_TEXTURE_2D, shadowChunk); glCopyTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 0, 0, lightmapChunkSize, lightmapChunkSize); // disable projective texturing glDisable(GL_TEXTURE_GEN_S); glDisable(GL_TEXTURE_GEN_T); glDisable(GL_TEXTURE_GEN_R); glDisable(GL_TEXTURE_GEN_Q); // reset texture matrix glMatrixMode(GL_TEXTURE); glLoadIdentity(); glMatrixMode(GL_MODELVIEW); The next step involves copying the data from the shadow chunk texture into our actual lightmap. We loop through each pixel in the shadow chunk data and copy it to the appropriate location in the lightmap. This involves simply taking our a and b values from our loops and converting these into lightmap coordinates. // get shadow texture data glGetTexImage(GL_TEXTURE_2D, 0, GL_RGB, GL_UNSIGNED_BYTE, pixels); for(int a = 0; a < lightmapChunkSize; a++) { for(int b = 0; b < lightmapChunkSize; b++) { int a2 = a + lightmapChunkSize * terrainRow; int b2 = b + lightmapChunkSize * terrainCol; lightmap[(a2 * lightmapSize + b2) * 3 + 0] = pixels[(a * lightmapChunkSize + b) * 3 + 0]; lightmap[(a2 * lightmapSize + b2) * 3 + 1] = pixels[(a * lightmapChunkSize + b) * 3 + 1]; lightmap[(a2 * lightmapSize + b2) * 3 + 2] = pixels[(a * lightmapChunkSize + b) * 3 + 2]; } } And now we simply finish up the inner and outer loops. At the end we free up any memory we created inside the function. } } // increment which section on the terrain we are looking at x0 += terrainChunkSize; x1 += terrainChunkSize; } x0 = 0; x1 = terrainChunkSize; y0 += terrainChunkSize; y1 += terrainChunkSize; } // free memory glDeleteTextures(1, &shadowTexture); delete [] pixels; } And that's it. Here's a complete source code listing. [edit] Complete Source Codevoid MeshShadows(unsigned char *lightmap, int lightmapSize, unsigned char shadowColor[3], float lightDir[3]) { // variable initialization int lightmapChunkSize = 128; if(lightmapSize < lightmapChunkSize) lightmapChunkSize = lightmapSize; float terrainDivisions = lightmapSize / lightmapChunkSize; int terrainChunkSize = Terrain.size() / terrainDivisions; unsigned char *chunk = new unsigned char[(lightmapChunkSize)*(lightmapChunkSize)*3]; // create shadow texture GLuint shadowChunk; glGenTextures(1, &shadowChunk); glBindTexture(GL_TEXTURE_2D, shadowChunk); glEnable(GL_TEXTURE_2D); glCopyTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, 0, 0, lightmapChunkSize, lightmapChunkSize, 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); int terrainCol = 0; int terrainRow = 0; float x0 = 0; float y0 = 0; float x1 = terrainChunkSize; float y1 = terrainChunkSize; for(terrainRow = 0; terrainRow < terrainDivisions; terrainRow++) { for(terrainCol = 0; terrainCol < terrainDivisions; terrainCol++) { // setup orthogonal view glViewport(0, 0, terrainChunkSize * Terrain.size(), terrainChunkSize * Terrain.size()); glMatrixMode(GL_PROJECTION); glLoadIdentity(); glOrtho(0, terrainChunkSize, 0, terrainChunkSize, -10000, 10000); glMatrixMode(GL_MODELVIEW); // apply light's direction to modelview matrix gluLookAt(lightDir[0], lightDir[1], lightDir[2], 0, 0, 0, 0, 1, 0); // loop through all vertices in terrain and find min and max points with respect to screen space float minX = 999999, maxX = -999999; float minY = 999999, maxY = -999999; double X, Y, Z; // get pointer to terrain vertices float *vertices = Terrain.vertices()->lock(); for(int i = y0-1; i < y1+1; i++) { if(i < 0) continue; for(int j = x0-1; j < x1+1; j++) { if(j < 0) continue; int index = i * Terrain.size() + j; // get screen coordinates for current vertex static GLint viewport[4]; static GLdouble modelview[16]; static GLdouble projection[16]; static GLfloat winX, winY, winZ; glGetDoublev( GL_MODELVIEW_MATRIX, modelview ); glGetDoublev( GL_PROJECTION_MATRIX, projection ); glGetIntegerv( GL_VIEWPORT, viewport ); gluProject(vertices[index*3+0], vertices[index*3+1], vertices[index*3+2], modelview, projection, viewport, &X, &Y, &Z); if(X < minX) minX = X; if(X > maxX) maxX = X; if(Y < minY) minY = Y; if(Y > maxY) maxY = Y; } } // clear min and max values static float minX2, minY2, maxX2, maxY2; minX2 = minX; minY2 = minY; maxX2 = maxX; maxY2 = maxY; // clear screen glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); glViewport(0, 0, lightmapChunkSize, lightmapChunkSize); // orient viewport so that terrain chunk fits inside glMatrixMode(GL_PROJECTION); glLoadIdentity(); glOrtho(minX, maxX, minY, maxY, -10000, 10000); glMatrixMode(GL_MODELVIEW); // apply light's direction vector to model view transformation glLoadIdentity(); gluLookAt(lightDir[0], lightDir[1], lightDir[2], 0, 0, 0, 0, 1, 0); // disable writing to the color buffer glColorMask(false, false, false, false); // render terrain Terrain.render(); // enable writing to the color buffer glColorMask(true, true, true, true); // render scene meshes '''BLACK''' RenderAllSceneMeshes(); // bind shadowChunk texture and copy frame buffer data glBindTexture(GL_TEXTURE_2D, shadowChunk); glCopyTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 0, 0, lightmapChunkSize, lightmapChunkSize); glBindTexture(GL_TEXTURE_2D, 0); glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); glViewport(0, 0, terrainChunkSize * Terrain.size(), terrainChunkSize * Terrain.size()); glMatrixMode(GL_PROJECTION); glLoadIdentity(); glOrtho(0, 0, terrainChunkSize, terrainChunkSize, -10000, 10000); glMatrixMode(GL_MODELVIEW); // rotate view so that xz plane becomes the xy plane glLoadIdentity(); glRotatef(90, 1, 0, 0); // reset max and min values minX = minY = 999999; maxX = maxY = -999999; glGetDoublev( GL_MODELVIEW_MATRIX, modelview ); glGetDoublev( GL_PROJECTION_MATRIX, projection ); glGetIntegerv( GL_VIEWPORT, viewport ); // project each corner onto the screen // the corners are represented by the x0, y0, x1 and y1 values gluProject(x0, y0, modelview, projection, viewport, &X, &Y, &Z); if(X < minX) minX = X; if(X > maxX) maxX = X; if(Y < minY) minY = Y; if(Y > maxY) maxY = Y; gluProject(x1, y0, modelview, projection, viewport, &X, &Y, &Z); if(X < minX) minX = X; if(X > maxX) maxX = X; if(Y < minY) minY = Y; if(Y > maxY) maxY = Y; gluProject(x1, y1, modelview, projection, viewport, &X, &Y, &Z); if(X < minX) minX = X; if(X > maxX) maxX = X; if(Y < minY) minY = Y; if(Y > maxY) maxY = Y; gluProject(x0, y1, modelview, projection, viewport, &X, &Y, &Z); if(X < minX) minX = X; if(X > maxX) maxX = X; if(Y < minY) minY = Y; if(Y > maxY) maxY = Y; // resize and re-orient the viewport glViewport(0, 0, terrainChunkSize * Terrain.size(), terrainChunkSize * Terrain.size()); glMatrixMode(GL_PROJECTION); glLoadIdentity(); glOrtho(minX, minY, maxX, maxY, -10000, 10000); glMatrixMode(GL_MODELVIEW); // rotate view so that xz plane becomes the xy plane glLoadIdentity(); glRotatef(90, 1, 0, 0); // setup projective texturing float PS[] = {1, 0, 0, 0}; float PT[] = {0, 1, 0, 0}; float PR[] = {0, 0, 1, 0}; float PQ[] = {0, 0, 0, 1}; 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); 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); // setup texture matrix glBindTexture(GL_TEXTURE_2D, shadowChunk); glMatrixMode(GL_TEXTURE); glLoadIdentity(); glTranslatef(0.5, 0.5, 0); glScalef(0.5, 0.5, 1); glOrtho(minX2, maxX2, minY2, maxY2, -10000, 10000); gluLookAt(lightDir[0], lightDir[1], lightDir[2], 0, 0, 0, 0, 1, 0); glMatrixMode(GL_MODELVIEW); // render the terrain Terrain.render(); glBindTexture(GL_TEXTURE_2D, shadowChunk); glCopyTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 0, 0, lightmapChunkSize, lightmapChunkSize); // disable projective texturing glDisable(GL_TEXTURE_GEN_S); glDisable(GL_TEXTURE_GEN_T); glDisable(GL_TEXTURE_GEN_R); glDisable(GL_TEXTURE_GEN_Q); // reset texture matrix glMatrixMode(GL_TEXTURE); glLoadIdentity(); glMatrixMode(GL_MODELVIEW); // get shadow texture data glGetTexImage(GL_TEXTURE_2D, 0, GL_RGB, GL_UNSIGNED_BYTE, pixels); for(int a = 0; a < lightmapChunkSize; a++) { for(int b = 0; b < lightmapChunkSize; b++) { int a2 = a + lightmapChunkSize * terrainRow; int b2 = b + lightmapChunkSize * terrainCol; lightmap[(a2 * lightmapSize + b2) * 3 + 0] = pixels[(a * lightmapChunkSize + b) * 3 + 0]; lightmap[(a2 * lightmapSize + b2) * 3 + 1] = pixels[(a * lightmapChunkSize + b) * 3 + 1]; lightmap[(a2 * lightmapSize + b2) * 3 + 2] = pixels[(a * lightmapChunkSize + b) * 3 + 2]; } } } } // increment which section on the terrain we are looking at x0 += terrainChunkSize; x1 += terrainChunkSize; } x0 = 0; x1 = terrainChunkSize; y0 += terrainChunkSize; y1 += terrainChunkSize; } // free memory glDeleteTextures(1, &shadowTexture); delete [] pixels; } For a demonstration of this technique visit Freeworld3D.org. The terrain editing software Freeworld3D uses this technique to render mesh shadows onto the terrain and is quite fast at doing it. |












