Open GL Super Bible

Previous Table of Contents Next


Drawing Terrain

Our terrain drawing controls consist of a toolbar dialog window with five buttons that select the current type of terrain. To draw the terrain, you just click and drag in the main window (see Figure 12-4).


Figure 12-4  Textured terrain editing window

The heart of the drawing interface is in the DrawTerrain function. It uses the OpenGL selection mechanism to determine which terrain points are under the mouse pointer. Instead of drawing the terrain to the screen, selection rendering records “hits” inside the selection area (in this case, the mouse pointer) to a buffer you provide. In DrawTerrain, we record the (x,y) location of the terrain in the selection buffer, as in a “paint-by-numbers” book (see Figure 12-5). OpenGL selection is covered in more detail in Chapter 19.


Figure 12-5  Picking a terrain cell

Once we have the (x,y) terrain locations, we then reset the height and type of these points in the draw_cell function (Listing 12-4).

Listing 12-4 The draw_cell function

void
draw_cell(int x, /* I - Terrain X location */
          int y) /* I - Terrain Y location */
{
 /*
  * Range check the terrain location…
  */

  if (x < 0 || x >= TERRAIN_SIZE ||
      y < 0 || y >= TERRAIN_SIZE)
    return;

  if (TerrainType[y][x] == TerrainCurrent)
    return;      /* Already the right type */

  TerrainType[y][x] = TerrainCurrent;

 /*
  * Force a redraw…
  */
  InvalidateRect(SceneWindow, NULL, TRUE);

 /*
  * Set the height of the terrain 'cell’.  For water, the
  * height is constant at WATER_HEIGHT.  Other other types,
  * we add a random pertubation to make the terrain more
  * interesting/realistic.
  */

  switch (TerrainCurrent)
  {
    case IDC_WATER :
        TerrainHeight[y][x] = WATER_HEIGHT;
        break;
    case IDC_GRASS :
        TerrainHeight[y][x] = GRASS_HEIGHT + 0.1 * (rand() % 5);
        break;
    case IDC_TREES :
        TerrainHeight[y][x] = TREES_HEIGHT + 0.1 * (rand() % 5);
        break;
    case IDC_ROCKS :
        TerrainHeight[y][x] = ROCKS_HEIGHT + 0.1 * (rand() % 5);
        break;
    case IDC_MOUNTAINS :
        TerrainHeight[y][x] = MOUNTAINS_HEIGHT + 0.15 * (rand() % 5);
        break;
  };
}

For the IDC_WATER terrain type, the point height is just set to WATER_HEIGHT (0.0). For other types, we add a small amount of random “jitter” to make the terrain look more realistic. Once the selected cell is drawn, we recompute the lighting normals using the new height values in UpdateNormals. Each lighting normal is calculated using the points above and to the right of the current point with the following formula:

N = lighting normal
H = height of current point
Hu = height of point above
Hr = height of point to the right

Nx = (Hr - H) / |N|
Ny = 1 / |N|
Nz = (Hu - H) / |N|

This is just a simplification of the cross product of adjacent terrain grid-cells. Once all the normals are recalculated, the scene is redrawn.

Drawing the Scene

Now that we’ve taken care of the drudge work, we can concentrate on displaying the terrain. You’ll remember that besides displaying a pretty textured image, we also want to fly through this terrain. To accomplish this, we need to draw the terrain without textures—basically because texture mapping on a standard PC is too slow for animation. When the user isn’t flying around (or drawing, for that matter), we want to draw with the textures. We will take care of this with a little conditional code and a few lighting parameters.

Also, because drawing the textured scene will be slower than the fly-through scene, we need to provide some feedback to the user that our program is doing something. For simplicity, we’ll just draw to the front buffer (the visible one) when texturing, and to the back buffer (the invisible one for animation) when flying or drawing. This way, when the program updates the textured scene, the user will see the image being drawn. You’ll learn more about buffers in Chapter 15.

The RepaintWindow function handles redrawing the terrain for the user. It starts off by selecting the front or back buffer (as described just above). Then it clears the color and depth bits, as follows:

glViewport(0, 0, rect->right, rect->bottom);

glClearColor(0.5, 0.5, 1.0, 1.0);

glEnable(GL_DEPTH_TEST);

if (Moving || Drawing)
{
  glDisable(GL_TEXTURE_2D);
  glDrawBuffer(GL_BACK);
}
else
{
  glEnable(GL_TEXTURE_2D);
  glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_DECAL);
  glDrawBuffer(GL_FRONT);
};

glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

After this, RepaintWindow draws in the sky. For performance reasons, the sky is only drawn when the user is not flying over or drawing the terrain. Since the background is cleared to a light blue, this isn’t really a problem. The sky is shaped like a pyramid and has the SKY.BMP texture image mapped to it for a nice, cloudy blue sky.

Once the sky is drawn, RepaintWindow starts drawing the terrain. The algorithm used is quite simple and basically generates strips of quadrilaterals (squares) along the terrain points. Each strip uses a different texture or lighting material color, so we have to issue glBegin/glEnd calls for each one. See Figure 12-6 for a graphical depiction of the algorithm.


Figure 12-6  The terrain-drawing algorithm

As you can see, this algorithm won’t track the terrain exactly, but it is fast and simple to implement. It scans the terrain from left to right and from bottom to top, and starts a new GL_QUAD_STRIP primitive whenever the terrain type changes. Along the way it assigns lighting normals and texture coordinates for each point on the terrain.


Previous Table of Contents Next