Generating Mesh Shadows On Terrain Using OpenGL

Introduction

If 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.

This technique renders mesh shadows using an orthographic projection along with a directional light. The algorithm is described below.

Algorithm

The basic algorithm for this method is as follows:

• Set up an orthographic projection
• Resize the viewport so that the entire terrain is within the viewing window
• Transform the scene using the light's direction vector
• Render all scene meshes to the screen
• Copy frame buffer to a texture
• Reset modelview transformation to look straight down at terrain
• Render terrain using the texture we just created
• Copy frame buffer to final shadow map texture

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.

Implementation Details

There 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.

A section size of 128x128 is well suited for this purpose.

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
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);
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.

So now minX, minY, maxX and maxY represent the minimum and maximum screen coordinates for the current terrain chunk. The next step is to resize the viewport so that this current terrain chunk fits inside of it. We need to make a copy of the min and max values. You will see why later on.

// 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);
glOrtho(minX, maxX, minY, maxY, -10000, 10000);
glMatrixMode(GL_MODELVIEW);

// apply light's direction vector to model view transformation
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
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);
glOrtho(0, 0, terrainChunkSize, terrainChunkSize, -10000, 10000);
glMatrixMode(GL_MODELVIEW);

// rotate view so that xz plane becomes the xy plane
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);
glOrtho(minX, minY, maxX, maxY, -10000, 10000);
glMatrixMode(GL_MODELVIEW);

// rotate view so that xz plane becomes the xy plane
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

glMatrixMode(GL_TEXTURE);
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();

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);
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
delete [] pixels;
}

And that's it. Here's a complete source code listing.

Complete Source Code

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;

unsigned char *chunk = new unsigned char[(lightmapChunkSize)*(lightmapChunkSize)*3];

// create shadow texture
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);
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);
glOrtho(minX, maxX, minY, maxY, -10000, 10000);
glMatrixMode(GL_MODELVIEW);

// apply light's direction vector to model view transformation
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
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);
glOrtho(0, 0, terrainChunkSize, terrainChunkSize, -10000, 10000);
glMatrixMode(GL_MODELVIEW);

// rotate view so that xz plane becomes the xy plane
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);
glOrtho(minX, minY, maxX, maxY, -10000, 10000);
glMatrixMode(GL_MODELVIEW);

// rotate view so that xz plane becomes the xy plane
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

glMatrixMode(GL_TEXTURE);
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();

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);
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