Black Art of 3D Game Programming, Chapter 16: Voxel Graphics

From Dpfileswiki

Jump to: navigation, search

Have you had enough polygons yet? I know I have! Let’s take a little break in this chapter and look at the new and powerful technology of voxel graphics. Although we briefly discussed the subject in the first chapter of the book, it would be nice to try generating a simple voxel display just to get some idea of how it’s done. This is exactly what we are going to do in this chapter. We’re going to write a simple, near real-time voxel engine that will create a recognizable voxel display. Of course, it would take a whole book to really learn voxel graphics and generate a real-time voxel engine (such as those seen in Nova Logic’s Commanche and Armored Fist). However, we will at least cover one method of generating a voxel display based on the technique of ray casting. We’ll also learn to speed things up a little and how to add sprites and other objects to the voxel world.

Contents

Voxel Primer

Voxel graphics is basically a technique of generating terrain-type data for mountains, ocean floors, and so forth. Figure 16-1 is a typical voxel display. Voxel means “volume pixel”; hence, voxel graphics is the science of rendering objects that have volume and can be broken up into little cubes or volume elements.

Image:.gif
FIGURE 16-1 Typical voxel display.

As Cybersorcerers, we really don’t care too much about what voxel graphics is supposed to be used for, we just want to abuse the technique for our needs, which of course, are games. We want to find a simple way to generate 3D terrain based on some kind of data set. The data might be contour maps, simple 2D textures, or whatever, but somehow we want to turn the data into a 3D image. We will generate this image in a completely new way; that is, instead of using polygons and transformations, we are going to use other techniques that are more adapted for the type of “rough” data set we are trying to render.

It turns out that the technique of ray casting (which you may be familiar with) can be used. Ray casting is the method used by Wolfenstein 3D and Blake Stone to generate the realistic displays for the games. We’ll talk more about ray casting later in the chapter, but now let’s take a look at exactly what we are going to use as data for our voxel engine.

Generating the Voxel Database

Voxel data consists of height data along with color data. For example, Figure 16-2 shows a contour map of some region of the planet. The different contours are marked with heights. This is one example of a data set that we might want to look at with a voxel engine. Another example of possible voxel data is shown in Figure 16-3. Here we see an infrared satellite scan of some area of Earth. Even though this data is 2D in nature, we can interpret the different intensities as the height of each region and thus extrapolate a 3D image from the 2D color image. In essence, the type of voxel data we are going to be using is called height data in a 2D array. The

Image:.gif
FIGURE 16-2 Topological contour map.

2D array holds a bird’s-eye view of the data, and each element of the array contains a height and color of the terrain at the element’s location.

Figure 16-4 shows a simple 8x8 height array. Each element of the array holds the height above sea level of the voxel element and its color. So the question is, where can we obtain voxel data? There are two basic sources of data: scanned data and algorithmically generated data. Let’s take a look at both.

Image:.gif
FIGURE 16-3 Infrared satellite scan.
Image:.gif
FIGURE 16-4 An 8x8 height map.

Using Scanned Terrain Maps

Our voxel engine is going to use 2D bitmaps for the voxel data. The 2D bitmaps will be interpreted in such a way that the color of each pixel is not only the color of the voxel data at that point, but the height from 0 to 255. For example, Figure 16-5 shows a voxel data set that originally was an earthquake intensity satellite scan. I took the image and tweaked it with Adobe Photoshop to make the scan more colorful. If we wanted to, we could support both a height and color map. For example, we could use the image of Figure 16-5 as just the color map, and a second 2D array of the same dimensions would be used as a height map. This gives a bit more free-

Image:.gif
FIGURE 16-5 2D color map.

dom, but we are going to keep things simple and use a single 2D bitmap for both the color and height. Of course, this means that the largest variance in height and color can range from 0 to 255 using 8 bits per pixel, but this will do. The only problem with using a 2D image as both the color information and the height information is that the relationship between color indices and heights may not be a good one. For example, say that we have an aerial picture of an island surrounded by water. There would be blue around the island, browns and whites around the beach, and browns and greens near the more inland parts of the island. We would naturally want the blues to have small heights, while the browns and greens have larger heights since they should be at a higher level than sea level. Unfortunately, this may not be true based on the palette used to scan the image. It may, in fact, be that the blues have very high color index numbers and the greens have very low numbers, which would make the terrain look inverted when rendered using the color indices as both the height and color of each voxel. Thus, care must be taken in palette selection so that there is a one-to-one correspondence between colors and desired heights. We could use a look-up table to map the colors to the proper heights, but this added look-up would slow things down. Anyway, before learning how to actually draw the voxel terrain, let’s take a quick look at totally algorithmic ways of generating the data.

Fractal and Random Terrain Maps

Using scanned images is great for voxel data, but sometimes it’s nice to be able to parametrically generate the data. This can be accomplished with various algorithms that can process a 2D array and set the elements of the array up in such a way that they look like a terrain or whatever the desired height distribution was. There are many ways to do this, such as with fractals, random numbers, or scripts. For example, we have all seen fractals and fractal mountainscapes. We could use the data generated by a fractal program as the heights of a 2D array. A typical fractal technique used to generate fractal mountains is called a plasma fractal and is very simple to implement.

Plasma fractals are usually based on some geometric primitive such as a triangle. The triangle is recursively subdivided, and during each subdivision, the smaller triangles have a bit of randomness added to them. Figure 16-6 shows this process in action. The only problem with this is that we don’t really have polygons, but single points, and it’s a bit harder to implement a plasma fractal with this type of data. Having tried it, the results didn’t look as good as the scanned data, so I threw it out. But let’s take a quick look at how it’s done.

We begin with a 2D terrain array, say 256x256 elements, as shown in Figure 16-7. We initialize the terrain heights to some value and subdivide the array into four smaller arrays in which the endpoints are distorted a bit up or down, as shown in Figure 16-8. Then we recursively call the algorithm again with the four smaller subquadrants. This is repeated until the size of each subquadrant is a single pixel. The results of this will be a plasma fractal terrain.

Image:.gif
FIGURE 16-6 Fractalization of a triangle.
Image:.gif
FIGURE 16-7 Phase one of recursive subdivision.
Image:.gif
FIGURE 16-8 Recursively subdividing a 2D height map.

Finally, the second way to generate a terrain is simply to use random numbers and fill the 2D terrain array with these numbers. However, there must be some kind of logic to make sure that the random values vary smoothly or else the data will look like noise when rendered, or like “grass.”

That’s about it for the data part of the voxel engine—let’s see how we actually draw the voxel data.

Ray Casting

There are two methods used in computer graphics to generate scenery. The first and most common method is synthesis, using math and simple models to generate 3D images. This is the method we have been using in the previous chapters of the book—that is, polygons. The second method is more natural and is based on the physical interactions of light and energy in an environment or, more precisely, ray tracing and radiosity. Ray tracing is a method of tracing the rays of light from the light sources in a scene off the objects in the scene, keeping track of their color, and testing whether they intersect the view plane of the viewer. Figure 16-9 shows a simple ray-tracing diagram. Ray tracing is nice because in one stroke it removes hidden surfaces and performs lighting, shadows, transparency, and rendering. But it’s very slow and tends to look synthetic. A relatively new technique is called radiosity, which deals with the radiant heat transfer of phonons (heat particles) in a closed system. It works by assuming that each object in a scene gives off energy,

Image:.gif
FIGURE 16-9 A simple ray-tracing scene.

reflects energy, and absorbs energy. By tracking the energy transfer from object to object and waiting for a steady state, hyper-realistic lighting can be achieved. However, that’s about all radiosity does for us. It is not a rendering technique, it is simply a lighting technique. But once the radiosity has been computed, the renderer can render any view of the room or environment.

Well, that’s all great, but what is ray casting? Ray casting is the little brother of ray tracing. First, ray casting is forward ray tracing, and it traces rays from the viewpoint out into the viewing volume, as shown in Figure 16-10. Second, ray casters usually are written for special cases based on geometric constraints. For example, if you have played Wolfenstein 3D by id, I’m sure you realized that the world is made of cubes. This is entirely true. However, computing the intersections between cubes and rays is very easy since cubes lie on a regular array. For example, Figure 16-11 shows a series of rays casted out from the viewer into a regular grid. Furthermore, the points of intersection are marked. Computing these intersection points is very easy. As a matter of fact, we don’t actually need to compute them! All we need to do is step the ray one block at time and look at the square it is in. If there is a solid

Image:.gif
FIGURE 16-10 Ray casting in action.

cube there, then there must have been an intersection. Using this technique, a full 3D textured image can be drawn by simply casting 320 rays (one for each column of the screen) and testing their intersections with the cubic regular environment. If you are interested in learning more about ray casting, I suggest you pick up The Waite Group’s Gardens of Imagination by Christopher Lampton. It covers everything about the topic you might want to know.

Anyway, we aren’t going to use ray casting to draw walls, but we are going to use it to draw the slivers of voxel terrain. What we’re going to do is called height mapping. Let’s check it out.

Height Mapping

Height mapping is a technique of casting rays out from a viewpoint and seeing where they intersect the ground or terrain being scanned. Then from this information, a 3D display can be generated. Take a look at Figure 16-12. Here we see a side

Image:.gif
FIGURE 16-11 Ray casting in a Wolfenstein world.

view or cross section of a mountain or terrain of some sort along with a view plane and viewer at a given viewing position. As you see, some of the terrain will be visible to some rays, while other portions won’t be. This is the key to using height mapping to generate a voxel display. All we need to do is cast a series of rays from the viewpoint and scan along a single direction. Then we rotate a little and perform the scan again until we have scanned our entire field of view. We then use the results of the scan to draw the proper display.

In case this is a little confusing, let’s make sure we understand what we’re going to do. We are going to use the 2D terrain data as the ground under us. Then we are going to place the viewing plane, which is the mode 13h screen of 320x200, somewhere above the terrain. Then we are going to place the viewpoint somewhere in front of the view plane. Now for the interesting part: For each of the 320 columns of the screen, we are going to cast 50 to 100 rays, one through each row (depending on the viewing angle). Each of the rays is aligned to the current column, but pierces the view plane at rows 50, 51, 52, 53 intersecting the terrain. The intersection points will be used to compute the color and height of each voxel strip. For example, if we hit an element that has a height of 30, it will have some perspective-

Image:.gif
FIGURE 16-12 Casting rays to generate a voxel display.

adjusted height when displayed. The key to understanding the process is that we must perform two for loops like this:

for (column=0; column<320; column++)
for (row=150; row<200; row++)
{
// 1. cast a ray from the viewpoint thru the current column and row
// and test its intersection with the terrain
// 2. record the height and color of the voxel intersected with
// 3. draw the voxel strip
} // end for row

Steps 2 and 3 may be merged, but you get the idea. Now, how should we generate each ray? This can be a problem because of a few factors. First, the player’s viewpoint may be anywhere in the universe. Second, the player must be able to turn and rotate, so this means that there is a viewing angle or yaw. Take a look at Figure 16-13 to see this. Referring to the figure, we see that the player can be any-

Image:.gif
FIGURE 16-13 Moving around the voxel landscape.

where within the bounds of the universe while looking in any direction. The position in the universe isn’t that hard to deal with, but the angle can be. Let’s take a look at a trick to make this easier.

Rotating the Ray

One of the biggest problems with ray tracing, ray casting, and height mapping is generating the scanning rays quickly enough; but luckily there are tricks to do this. One of them is to use look-up tables. Before we do that, we need to talk about field of view again. If you recall, field of view is the angular displacement from looking straight that you can see to the sides and above and below; in other words, your peripheral vision. In computer graphics, we don’t have the luxury of physics, so we have to simulate everything. And just as we computed how to implement various fields of view for a polygon engine, we must do the same in the context of voxel graphics.

I have found that 60 degree fields of view work pretty well, so that’s what we are going to use. However, in our voxel engine, we’ll only get control of the horizontal field of view; the vertical field of view is a function of viewing distance, height, and row per screen. If we want a 60 degree field of view to be drawn within the extents of 320 columns, this means that there are 320 columns per 60 degrees, or 1920 columns for 360 degrees, since 360/60=6 and 6*320 is 1920. As a result, we need to make a look-up table that has all the rotations of a ray in 1920 little angular increments. Figure 16-14 shows this graphically. We must subdivide a circle into 1920 subarcs, and then create look-up tables such that each element represents the value of a function that can be used to rotate a ray to any direction within the 1920 positions of the subdivided circle. Since I’m trying to make things easy, I decided to create both a SIN and COSINE table that are each 1920 elements long. Using these tables, we forget about the normal 360 degrees in a circle and pretend that there are 1920 degrees, based on look-up table degrees. There are now 1920 degrees in a complete circle. If we need to rotate a ray to 690 degrees, we simply access the SIN and COSINE arrays and do the following:

ray_x = cos_look[690];
ray_y = sin_look[690];

Then (ray_x,ray_y) would contain a unit vector pointing in the desired direction. We could then scale this by any factor that we wish, and the resulting point can be used as the scanning tip into the terrain data relative to the viewer’s position. Figure 16-15 shows this graphically. So we spray out a series of rays for each of the 320 columns based on a circular scan to do the height mapping, but unfortunately, this will introduce a type of distortion. Let’s take a look at it.

Image:.gif
FIGURE 16-14 Dividing a circle unit.
Image:.gif
FIGURE 16-15 Rotating a ray into position for scanning.

Fixing the Distortion

Figure 16-16 shows a typical sequence of scanning positions from a top view. As you can see, the scanning positions follow a circular path. The results of this will be that the view, when rendered, will look as if it’s seen through a spherical lens. To remove this distortion, we must multiply by some factor. This factor can be derived as follows: when we cast out the ray for each column from 0 to 319, column 160 is basically our view direction. Since we are casting out a 60 degree field of view, there are 30 degrees to the left and 30 degrees to the right of the center column. This is shown in Figure 16-17. Since we know that the distortion is spherical, we can deduce that multiplication by the inverse of a sin or cosine wave should cancel it out. And in fact this is what we must do. As the rays are casted from column 0 to 319, we multiply the length of each ray by the inverse cosine (secant) of the angle relative to the center. In other words, the rays from 0 to 159 are multiplied by the values of 1/cosine(-30...0 degrees), and the lengths from 160 to 319 are multiplied by the values of 1/cosine(0...30 degrees).

So I think we understand how we cast each ray for each column, but how do we cast through all the rows of each column? We’ll cover that next.

FIGURE 16-16 � � � � � � Casting along a circular path

FIGURE 16-17 � � � � � � The details to uncover the actual distortion

Finding the Height of the Voxel

For any given column of the video display, a series of rays must be casted through each row, and their intersections with the terrain must be found. We could do this just as we casted the rays through the column, but there is a simpler way. What we are going to do is this: For each of the 320 columns in the display, we will rotate a ray into position. Then we will use similar triangles to compute where this ray would intersect the terrain if it were forced to pierce each of the rows of the particular column of interest. Figure 16-18 shows this. The question is, how can we compute the base of each of the triangles so that we can use this point to inquire into the terrain data to see the color and height of the voxel? The key is to use the geometry of the system to our advantage. Take a look at Figure 16-19. Here we see a typical ray being casted through a given row (the column is irrelevant). All we are interested in is computing the length of the base of the triangle labeled ray_length.

We know the height of the viewer above the terrain (height), and we know the distance of the viewer from the viewing plane (distance). We indeed have enough information to compute the value of ray_length. But how? We are going to use the

FIGURE 16-18 � Height casting FIGURE 16-19 � � � � � � Casting a ray down

method of Similar Triangles, a geometry rule that states: If the angles of a pair of triangles are equal, then the lengths of the sides are either all equal or all proportional. For example, in Figure 16-20, we see that there are actually two triangles in the diagram. One is labeled A and the other B. We also see that A and B are similar,

FIGURE 16-20 � � � � � � Finding the value of ray_length using similar triangles

therefore, we can deduce the following relationship. The length of the base on the larger triangle B divided by the length of the base of the smaller triangle A is proportional to the height of the larger triangle B divided by the height of the smaller triangle A. Or, mathematically we have

ray_length height _________ = ______ distance delta

or:

ray_length = distance * height / delta

What exactly is delta? The value of delta can be computed in terms of other variables, such as the height of the viewing plane and the height of the player. It turns out that delta is

delta = height - (SCREEN_HEIGHT - row)

All these minus signs are due to the fact that the Y axis is inverted in mode 13h; that is, positive Y is downward instead of upward. In any case, once we have the proper ray_length for the current row, we can multiply it by the current columns ray_x and ray_y, along with the distortion compensation, to compute the final scanning point in the terrain, something like this,

ray_length = sphere_cancel[column] * (distance*height)/(height-(SCREEN_HEIGHT - row));

xr = player_x + ray_length * cos_look[curr_ang];
yr = player_y - ray_length * sin_look[curr_ang];

where curr_angle varies from -30 to 30 degrees over the 320 columns of the ray cast.

Notice the multiplication of sphere_cancel[] to compensate for the spherical distortion we spoke of earlier. Once the values of xr and yr have been computed, we can then use these to access the 2D terrain data, and we finally have the voxel that we are looking for. At this point, we simply need to compute the height of the strip and render it.

Scaling the Strips

If we wanted to, we could cast two rays for each voxel; one to find the bottom and one to find the top, as shown in Figure 16-21. However, since we know the height of each voxel strip, we need only find its bottom and then draw the voxel strip with the proper scale based on the distance of the voxel from the player’s point of view. Remember the perspective equations we have been using to project 3D polygons onto the 2D viewing plane of the screen? Well, if we analyze the math a bit, we come up with an interesting little tidbit: the scale of an object is inversely proportional to its distance from the viewing plane. Mathematically, we have

scale = k/object(z)

FIGURE 16-21 � � � � � � Casting two rays per voxel to find its top and bottom

where k is simply a scaling factor and object(z) means the Z distance of the object from the viewing plane. Hence, in our system, object(z) would simply be ray_length.

Putting It All to Work

Let’s see if we can’t write a simple voxel display demo using the knowledge and techniques outlined so far. Let’s begin by loading a 320x200 PCX file and using it as the height and color data. The heights will range from 0 to 255 and so will the colors. The PCX image should be of some kind of terrain data or contour map. Luckily, I found one; then, using Adobe Photoshop, I played with the colors and made multiple versions of the map that look like terrain of Earth, Mars, and other exotic places.

Once we have the terrain map loaded, we will enter into a simple event loop that queries the keyboard and allows us to change the view position, height, and distance from the virtual viewing plane. These variables will then be fed into the terrain generator, and the voxel display will be rendered in order of farther voxels to nearer voxels. Since we will be using a single bitmap, we will have to wrap around the bitmap in both the X and Y directions, but this can be achieved with the MOD operator or logical ANDing. The complete terrain generator program is called VOXEL.C and the executable is named VOXEL.EXE. The source is shown in Listing 16-1.

LISTING 16-1 Demo of single bitmap voxel

// VOXEL.C - Single texture based, ray casted voxel engine

// I N C L U D E S ///////////////////////////////////////////////////////////

#include <io.h>
#include <conio.h>
#include <stdio.h>
#include <stdlib.h>
#include <dos.h>
#include <bios.h>
#include <fcntl.h>
#include <memory.h>
#include <malloc.h>
#include <math.h>
#include <string.h>
#include "black3.h"
#include "black4.h"
#include "black5.h"

// D E F I N E S /////////////////////////////////////////////////////////////

#define WORLD_X_SIZE 320 // width of universe
#define WORLD_Y_SIZE 200 // height of universe

// constants used to represent angles for the ray casting a 60 degree field of view

#define ANGLE_0 0
#define ANGLE_1 5
#define ANGLE_2 10
#define ANGLE_4 20
#define ANGLE_5 25
#define ANGLE_6 30
#define ANGLE_15 80
#define ANGLE_30 160
#define ANGLE_45 240
#define ANGLE_60 320
#define ANGLE_90 480
#define ANGLE_135 720
#define ANGLE_180 960
#define ANGLE_225 1200
#define ANGLE_270 1440
#define ANGLE_315 1680
#define ANGLE_360 1920

// there are 1920 degrees in our circle or 360/1920 is the conversion factor
// from real degrees to our degrees

#define ANGULAR_INCREMENT ((float)0.1875)

// conversion constants from radians to degrees and viceversa

#define DEG_TO_RAD ((float)3.1415926/(float)180)
#define RAD_TO_DEG ((float)180/(float)3.1415926)

// G L O B A L S ////////////////////////////////////////////////////////////

pcx_picture image_pcx;   // general pcx image
int play_x=1000,         // the current world x of player
    play_y=1000,         // the current world y of player
    play_z=150,
    play_ang=ANGLE_90,   // the current viewing angle of player
    play_dist = 70,
    mountain_scale=10;   // scaling factor for mountains

float play_dir_x,        // the direction the player is pointing in
      play_dir_y,
      play_dir_z,
cos_look[ANGLE_360],     // cosine look up table
sin_look[ANGLE_360],     // sin look up table
sphere_cancel[ANGLE_60]; // cancels fish eye distortion

// F U N C T I O N S ////////////////////////////////////////////////////////

void Line_VDB(int y1,int y2,int x,int color)
{
// draw a vertical line, note that a memset function can no longer be
// used since the pixel addresses are no longer contiguous in memory
// note that the end points of the line must be on the screen   
 
unsigned char far *start_offset; // starting memory offset of line

int index, // loop index
    temp;  // used for temporary storage during swap

// make sure y2 > y1

if (y1>y2)
   {
   temp = y1;
   y1 = y2;
   y2 = temp;
   } // end swap

// compute starting position

start_offset = double_buffer + ((y1<<8) + (y1<<6)) + x;

for (index=0; index<=y2-y1; index++)

// set the pixel

*start_offset = (unsigned char)color;

// move downward to next line

start_offset+=320;

} // end for index

} // end Line_VDB

//////////////////////////////////////////////////////////////////////////////

int Initialize(char *filename)
{
// this function builds all the look up tables for the terrain generator and
// loads in the terrain texture map

int ang;         // looping variable

float rad_angle; // current angle in radians

// create sin and cos look up first

for (ang=0; ang<ANGLE_360; ang++)
    {
    // compute current angle in radians

    rad_angle = (float)ang*ANGULAR_INCREMENT*DEG_TO_RAD;

    // now compute the sin and cos

    sin_look[ang] = sin(rad_angle);
    cos_look[ang] = cos(rad_angle);

    } // end for ang

  // create inverse cosine viewing distortion filter

for (ang=0; ang<ANGLE_30; ang++)
    {
    // compute current angle in radians

    rad_angle = (float)ang*ANGULAR_INCREMENT*DEG_TO_RAD;

    // now compute the sin and cos

sphere_cancel[ang+ANGLE_30] = 1/cos(rad_angle);
sphere_cancel[ANGLE_30-ang] = 1/cos(rad_angle);

} // end for ang

// initialize the pcx structure

PCX_Init((pcx_picture_ptr)&image_pcx);

// load in the textures

return(PCX_Load(filename, (pcx_picture_ptr)&image_pcx,1));

} // end Initialize

/////////////////////////////////////////////////////////////////////////////

void Draw_Terrain(int play_x,
                  int play_y,
                  int play_z,
                  int play_ang,
                  int play_dist)
{
// this function draws the entire terrain based on the location and orientation
// of the player's viewpoint

int curr_ang, // current angle being processed
    xr,yr, // location of ray in world coords
    x_fine,y_fine, // the texture coordinates the ray hit
    pixel_color, // the color of textel
    ray, // looping variable
    row, // the current video row being processed
    row_inv, // the inverted row to make upward positive
    scale, // the scale of the current strip

    top, // top of strip
    bottom; // bottom of strip

float ray_length; // the length of the ray after distortion compensation

// start the current angle off -30 degrees to the left of the player's

// current viewing direction

curr_ang = play_ang - ANGLE_30;

// test for underflow

if (curr_ang < 0)
    curr_ang+=ANGLE_360;

// cast a series of rays for every column of the screen

for (ray=1; ray<320; ray++)
    {

// for each column compute the pixels that should be displayed

// for each screen pixel, process from top to bottom
for (row = 100; row<150; row++)
{

// compute length of ray

row_inv = 200-row;

// use the current height and distance to compute length of ray.

ray_length = sphere_cancel[ray] * ((float)(play_dist*play_z)/
(float)(play_z-row_inv));

// rotate ray into position of sample

xr = (int)((float)play_x + ray_length * cos_look[curr_ang]);
yr = (int)((float)play_y - ray_length * sin_look[curr_ang]);

// compute texture coords

x_fine = xr % WORLD_X_SIZE;
y_fine = yr % WORLD_Y_SIZE;

// using texture index locate texture pixel in textures

pixel_color = image_pcx.buffer[x_fine + (y_fine*320)];

// draw the strip

scale = (int)mountain_scale*pixel_color/(int)(ray_length+1);

top = 50+row-scale;

bottom = top + scale;

Line_VDB(top,bottom,ray,pixel_color);

// Write_Pixel_DB(ray,50+row,pixel_color);

} // end for row

// move to next angle

if (++curr_ang >= ANGLE_360)
curr_ang=ANGLE_0;

} // end for ray

} // end Draw_Terrain

// M A I N //////////////////////////////////////////////////////////////////

void main(int argc, char **argv)

{
char buffer[80];

int done=0; // exit flag

float speed=0; // speed of player

// check to see if command line parms are correct

if (argc<=2)
{
// not enough parms

printf("\nUsage: voxopt.exe filename.pcx height");
printf("\nExample: voxopt voxterr3.pcx 10\n");

// return to DOS

exit(1);

} // end if

// set the graphics mode to mode 13h
Set_Graphics_Mode(GRAPHICS_MODE13);

// create a double buffer
Create_Double_Buffer(200);

// create look up tables and load textures

if (!Initialize(argv[1]))
{

printf("\nError loading file %s",argv[1]);
exit(1);

} // end if

// install keyboard driver
Keyboard_Install_Driver();

// set scale of mountains
mountain_scale = atoi(argv[2]);

// draw the first frame

Draw_Terrain(play_x,
continued on next page

             play_y,
             play_z,
             play_ang,
             play_dist);

Display_Double_Buffer(double_buffer,0);

// main event loop

while(!done)
{

// reset velocity

speed = 0;

// test if user is hitting keyboard

if (keys_active)
{
// what is user trying to do

// change viewing distance

if (keyboard_state[MAKE_F])
play_dist+=10;

if (keyboard_state[MAKE_C])
play_dist-=10;

// change viewing height

if (keyboard_state[MAKE_U])
play_z+=10;

if (keyboard_state[MAKE_D])
play_z-=10;

// change viewing position

if (keyboard_state[MAKE_RIGHT])
    if ((play_ang+=ANGLE_5) >= ANGLE_360)
    play_ang-=ANGLE_360;

if (keyboard_state[MAKE_LEFT])
   if ((play_ang-=ANGLE_5) < 0)
   play_ang+=ANGLE_360;

// move forward

if (keyboard_state[MAKE_UP])
speed=20;

// move backward

if (keyboard_state[MAKE_DOWN])
speed=-20;

// exit demo

if (keyboard_state[MAKE_ESC])
done=1;

// compute trajectory vector for this view angle

play_dir_x = cos_look[play_ang];
play_dir_y = -sin_look[play_ang];
play_dir_z = 0;

// translate viewpoint

play_x+=speed*play_dir_x;
play_y+=speed*play_dir_y;
play_z+=speed*play_dir_z;

// draw the terrain

Fill_Double_Buffer(0);

Draw_Terrain(play_x,
play_y,
play_z,
play_ang,
play_dist);

// draw tactical

sprintf(buffer,"Height = %d Distance = %d ",play_z,play_dist);
Print_String_DB(0,0,10,buffer,0);

sprintf(buffer,"Pos: X=%d, Y=%d, Z=%d ",play_x,play_y,play_z);
Print_String_DB(0,10,10,buffer,0);

Display_Double_Buffer(double_buffer,0);

} // end if

} // end while

// reset back to text mode
Set_Graphics_Mode(TEXT_MODE);

// remove the keyboard handler
Keyboard_Remove_Driver();

} // end main