


AVRIL Programmers Guide
Version 1.0
August 8, 1994

Bernie Roehl



Part I -- An Introduction

This document is divided into two parts: an introduction to AVRIL and a
technical reference.

What is AVRIL?

     AVRIL is A Virtual Reality Interface Library, a software package
that allows you to create and interact with virtual worlds.  It consists of a
fast polygon-based rendering engine and a set of support routines that
make virtual world creation a reasonably straightforward process.

     AVRIL is designed to be fast, portable and easy to use.  It's written
entirely in ANSI C, except for the very low-level routines that actually
write to the display.  The 386 version also has a few short assembly-
language routines to handle some of the fixed point math.  The API
(Applications Program Interface) is simple and well-documented, which
should make applications easy to develop.

     Most important, AVRIL is free for non-commercial use.  The
problem with most current VR libraries is that they're very, very
expensive; most of them cost more than the computers they run on! 
AVRIL is intended to give everyone with an interest in VR an opportunity
to develop some simple applications without having to invest huge sums of
money.

What does "free for non-commercial use" mean?

     It means that if you're only writing programs for your own use, or
to give away free to others, you can use AVRIL without paying anything.

Who developed AVRIL?

     AVRIL was developed by Bernie Roehl, between November of
1993 and April of 1994.  It's designed to be somewhat backward-
compatible with an earlier rendering engine called REND386 that was
developed by Bernie Roehl and Dave Stampe.

So what makes AVRIL different from REND386?

     From the beginning, we knew that REND386 would never run on
anything but 386-based computers; that's why we called it "REND386" in
the first place.  REND386 was fast, but it achieved its speed at the price of
portability; large parts of the code were hand-translated to 386 assembly
language.  This obviously reduced the portability of the software, as well
as making it more difficult to maintain.

     AVRIL, by contrast, is written entirely in C.  It's fast because the
algorithms are well-chosen and carefully written.  While it's not quite as
fast overall as REND386, there are actually some situations where it's
faster; once it's been optimized a bit, the speed should be comparable. 
Since it's written in C, AVRIL is also much easier to maintain than
REND386 was.

Using AVRIL

     AVRIL is very easy to use.  Rather than spend a lot of time
discussing the details of how it works, let's start by creating a simple
AVRIL program:

/* EXAMPLE1 -- a cube */

/* Written by Bernie Roehl, April 1994 */

#include "avril.h"

void main()
     {
     vrl_Object *cube;
     vrl_Light *light;
     vrl_Camera *camera;

     vrl_SystemStartup();

     cube = vrl_ObjectCreate(vrl_PrimitiveBox(100, 100, 100, NULL));
     vrl_ObjectRotY(cube, float2angle(45));

     light = vrl_LightCreate();
     vrl_LightRotY(light, float2angle(45));
     vrl_LightRotX(light, float2angle(45));

     camera = vrl_CameraCreate();
     vrl_CameraRotX(camera, float2angle(45));
     vrl_CameraMove(camera, 0, 500, -500);

     vrl_SystemRun();
     }

Notice that the only file we had to #include was "avril.h"; that file contains
prototypes for all the AVRIL functions, along with a number of useful
macros.  The avril.h file #includes <stdio.h> (since it references the FILE
* type) so there's no need for you to do so yourself.

     The program shown above simply creates a cube, a light source and
a virtual camera.  All the AVRIL routines and data types have names
beginning with "vrl_"; this ensures that they won't conflict with any
routines you write.  The vrl_SystemStartup() routine does all the system
initialization; the source code for all the vrl_System functions is found in
system.c, in case you're curious as to how they work.  We'll be looking at
them in detail later.

     Once the initialization is done, the program creates the cube by
calling a routine that generates a primitive box shape; the sides are all 100
units in length.  After it's been created, the cube is rotated 45 degrees
around the vertical (Y) axis.  The float2angle() routine converts a floating-
point number into an internal format used for storing angles.

     A directional light source is then created, and rotated 45 degrees in
each of X and Y.  Next, a virtual camera is created, rotated and moved
into position.  Finally, vrl_SystemRun() is called; vrl_SystemRun() sits in a
loop, checking for keyboard or mouse activity and doing the rendering as
needed.

     To compile and link the program using Borland C++, you would
give the following command:

     bcc -ml example1.c input.c avril.lib

This compiles example1.c and input.c and links them with the AVRIL
library.  The current default version of the low-level display routines in
avril.lib use the old REND386 "vd256.rvd" driver, which must be in the
current directory or in your PATH when the compiled program is run. 
There will soon be other display routines that you can use with AVRIL.

The routines in input.c are discussed in a later section.

Sharing Shapes

     Our first example was pretty straightforward; let's try something
more complex.

/* EXAMPLE2 -- several asteroids, sharing the same geometry */

/* Written by Bernie Roehl, April 1994 */

#include <stdlib.h>

#include "avril.h"

void main()
     {
     FILE *infile;
     vrl_Light *light;
     vrl_Camera *camera;
     vrl_Shape *asteroidshape = NULL;
     int i;

     vrl_SystemStartup();
     
     vrl_WorldSetHorizon(0);    /* turn off horizon */
     vrl_WorldSetSkyColor(0);   /* black sky */
     vrl_WorldSetAmbient(float2factor(0.1));  /* low ambient light */

     infile = fopen("asteroid.plg", "r");
     if (infile)
           {
           asteroidshape = vrl_ReadPLG(infile);
           fclose(infile);
           }

     light = vrl_LightCreate();
     vrl_LightRotY(light, float2angle(45));
     vrl_LightRotX(light, float2angle(45));
     vrl_LightSetIntensity(light, float2factor(0.9));

     camera = vrl_CameraCreate();
     vrl_CameraMove(camera, 0, 100, -50);

     for (i = 0; i < 10; ++i)
           {
           vrl_Object *obj = vrl_ObjectCreate(asteroidshape);
           vrl_ObjectMove(obj, rand() % 1000, rand() % 1000, rand() %
1000);
           }

     vrl_SystemRun();
     }

This program illustrates a useful memory-saving feature of AVRIL.  The
shape of an object (i.e., its geometric description) is separate from the
information about its location and orientation.  Any number of objects can
share the same geometric description, saving substantial amounts of
memory.  A geometric description is called a vrl_Shape, and consists of a
set of vertices, facets and other information.

     The program shown above begins by turning off the horizon (it's
on by default) and setting the sky color to 0 (black).  The sky color is used
as the screen clear color if there's no horizon.  Next, the file "asteroid.plg"
is loaded; AVRIL supports the PLG file format, described in Appendix C. 
The vrl_ReadPLG() function returns a pointer to a vrl_Shape (the same
data type that was returned by the vrl_PrimitiveBox() function in our first
example).

     A light source and camera are again set up, and ten virtual objects
are created using the shape that was loaded by vrl_ReadPLG().  Notice that
the file only had to be read once, and that the vertices and facets making
up an asteroid are only stored once in memory.  Each of the asteroids is
moved to a random location in an imaginary box 1000 units on a side.

     As you move around, you'll notice that the appearance of an
asteroid changes depending on how far away you are from it; if you get
close enough, it's a rough, craggy surface.  The "asteroid.plg" file stores
multiple representations of the object, and AVRIL automatically selects
one of those representations based on distance.  This can speed up the
rendering process by allowing fewer vertices and facets to be used when
an object is far away.

Making Maps

     AVRIL not only separates geometry from location/orientation
information, it also stores surface descriptions separately.  Each object has
a "surface map" associated with it, which stores pointers to actual
vrl_Surface descriptors.  Each surface has a type, a hue and a brightness;
in our examples, the surface type is always SURFACE_FLAT (meaning
that flat shading is used).  The hue is what most people think of as the
"color", and the brightness is how much light the surface reflects back to
the eye.  The higher the brightness value and the more directly that light is
striking the surface, the more intense the color.

     You can assign surface maps to objects, and change them whenever
you like.  Our third example program uses two different surface maps,
called map1 and map2:

/* EXAMPLE3 -- surface maps */

/* Written by Bernie Roehl, April 1994 */

#include <stdlib.h>

#include "avril.h"

void main()
     {
     FILE *infile;
     vrl_Light *light;
     vrl_Camera *camera;
     vrl_Shape *colorthing = NULL;
     vrl_Surfacemap *map1, *map2;
     int i;

     vrl_SystemStartup();
     
     map1 = vrl_SurfacemapCreate(6);
     map2 = vrl_SurfacemapCreate(6);
     for (i = 0; i < 6; ++i)
           {
           vrl_SurfacemapSetSurface(map1, i, vrl_SurfaceCreate(i + 1));
           vrl_SurfacemapSetSurface(map2, i, vrl_SurfaceCreate(7 + i));
           }

     infile = fopen("colorful.plg", "r");
     if (infile)
           {
           colorthing = vrl_ReadPLG(infile);
           fclose(infile);
           }

     light = vrl_LightCreate();
     vrl_LightRotY(light, float2angle(45));
     vrl_LightRotX(light, float2angle(45));

     camera = vrl_CameraCreate();
     vrl_CameraMove(camera, 0, 100, -50);

     for (i = 0; i < 10; ++i)
           {
           vrl_Object *obj = vrl_ObjectCreate(colorthing);
           if (i & 1)
                vrl_ObjectSetSurfacemap(obj, map1);
           else
                vrl_ObjectSetSurfacemap(obj, map2);
           vrl_ObjectMove(obj, rand() % 1000, rand() % 1000, rand() %
1000);
           }

     vrl_SystemRun();
     }

The program creates the two maps using the vrl_SurfacemapCreate()
function; the parameter is the number of entries the map should have.  Six
entries are then created in each map by calling vrl_SurfaceCreate(); the
parameter to that function is the hue.  The first map will use hues 1
through 6 inclusive, the second will use hues 7 through 12.  A shape is
then loaded from the file "colorful.plg"; that file uses indexed surface
descriptors (0x8000, 0x8001 etc) that refer to entries in the surface map. 
Refer to Appendix C for more details about surface descriptors.

     The light source and camera are again set up, and ten objects are
created.  Half of them (the odd-numbered ones) are assigned map1 and the
others are assigned map2.  The objects are again positioned randomly.

     Notice how half the cubes are a different color from the other half. 
Each set of surface descriptions is only stored once, and each surface map
is shared by five of the ten cubes.  All the cubes share the same vrl_Shape
information, which is only stored once.

A Real Taskmaster

     AVRIL has a pseudo-tasking facility, which allows you to add
routines to a list that gets processed continuously while the system runs. 
Each task has a function and possibly some data, as well as an indication
of how often it should be run.

     Our fourth example is more complex that the first three; it creates
several primitive shapes, sets up surface maps, and creates tasks to make
the objects move by themselves.  We'll have spinning cubes, bouncing
spheres and pulsating cylinders.  Let's look at the mainline first:

/* EXAMPLE4 -- simple object behaviours */

/* Written by Bernie Roehl, April 1994 */

#include <stdlib.h>

#include "avril.h"

static vrl_Angle spinrate;
static vrl_Time bounce_period;
static vrl_Scalar maxheight;
static vrl_Time pulse_period;

static void spin(void)
     {
     vrl_ObjectRotY(vrl_TaskGetData(), vrl_TaskGetElapsed() * spinrate);
     vrl_SystemRequestRefresh();
     }

static void bounce(void)
     {
     vrl_Object *obj = vrl_TaskGetData();
     unsigned long off;
     vrl_Scalar height;
     off = (360 * (vrl_TaskGetTimeNow() % bounce_period)) / bounce_period;
     height = vrl_FactorMultiply(vrl_Sine(float2angle(off)), maxheight);
     vrl_ObjectMove(obj, vrl_ObjectGetX(obj), height, vrl_ObjectGetZ(obj));
     vrl_SystemRequestRefresh();
     }

static void pulsate(void)
     {
     vrl_Surface *surf = vrl_SurfacemapGetSurface((vrl_Surfacemap *)
vrl_TaskGetData(), 0);
     unsigned long off;
     int brightness;
     off = (360 * (vrl_TaskGetTimeNow() % pulse_period)) / pulse_period;
     brightness = abs(vrl_FactorMultiply(vrl_Sine(float2angle(off)), 255));
     vrl_SurfaceSetBrightness(surf, brightness);
     vrl_SystemRequestRefresh();
     }

void main()
     {
     vrl_Light *light;
     vrl_Camera *camera;
     vrl_Shape *cube, *sphere, *cylinder;
     vrl_Surfacemap *cubemap, *pulsemap;
     int i;

     vrl_SystemStartup();
     
     cube = vrl_PrimitiveBox(100, 100, 100, NULL);
     sphere = vrl_PrimitiveSphere(100, 6, 6, NULL);
     cylinder = vrl_PrimitiveCylinder(100, 50, 100, 8, NULL);

     cubemap = vrl_SurfacemapCreate(1);
     vrl_SurfacemapSetSurface(cubemap, 0, vrl_SurfaceCreate(5));
     pulsemap = vrl_SurfacemapCreate(1);
     vrl_SurfacemapSetSurface(pulsemap, 0, vrl_SurfaceCreate(14));

     spinrate = float2angle(72.0 / vrl_TimerGetTickRate());  /* deg per
tick */
     bounce_period = 4 * vrl_TimerGetTickRate();  /* four-second period */
     maxheight = float2scalar(400);    /* maximum height in units */
     pulse_period =  2 * vrl_TimerGetTickRate();  /* two-second period */

     light = vrl_LightCreate();
     vrl_LightRotY(light, float2angle(45));
     vrl_LightRotX(light, float2angle(45));

     camera = vrl_CameraCreate();
     vrl_CameraRotY(camera, float2angle(5));
     vrl_CameraMove(camera, 0, 200, -4400);

     for (i = 0; i < 10; ++i)
           {
           vrl_Object *obj = vrl_ObjectCreate(NULL);
           vrl_ObjectMove(obj, rand() % 1000, rand() % 1000, rand() %
1000);
           switch (i & 3)
                {
                case 0:
                     vrl_ObjectSetShape(obj, cube);
                     break;
                case 1:
                     vrl_ObjectSetShape(obj, cube);
                     vrl_ObjectSetSurfacemap(obj, cubemap);
                     vrl_TaskCreate(spin, obj, 10);
                     break;
                case 2:
                     vrl_ObjectSetShape(obj, sphere);
                     vrl_TaskCreate(bounce, obj, 10);
                     break;
                case 3:
                     vrl_ObjectSetShape(obj, cylinder);
                     vrl_ObjectSetSurfacemap(obj, pulsemap);
                     break;
                }
           vrl_TaskCreate(pulsate, pulsemap, 10);
           }

     vrl_SystemRun();
     }

Three primitive shapes are created -- a box (100 units on a side), a sphere
(100 units in radius, with 6 facets around its "latitude" and 6 slices around
its "longitude") and a tapered cylinder (base radius 100, top radius 50,
height 100 units with 8 sides).  Two surface maps are created, each with a
single surface; one called cubemap using hue 5 and one called pulsemap
using hue 14.

     Some global variables are then set; spinrate is the rate that the
cubes should spin, in degrees per "tick".  A tick is a small unit of time; the
timer runs at 1000 ticks per second, so each tick is one millisecond.  In
case this changes, you should use the routine vrl_TimerGetTickRate() to
found out how many ticks per second the timer is running at.

     We do the float2angle() conversion here rather than in the spin()
task itself; by storing the vrl_Angle value, we avoid having to do the
conversion each time through the simulation loop.  Also notice that we
divide by the rate at which the system timer runs, in ticks per second; the
rotation rate is 72 degrees per second, so we divide by ticks per second to
get the rotation rate in degrees per tick.

     The bounce_period is 4 seconds, converted to ticks; this is the time
it takes a bouncing ball to go through one complete up-down cycle.  The
maximum height a ball will rise to is maxheight, arbitrarily set to be 400
units.  Note the conversion from floating-point to the internal "vrl_Scalar"
format.  The pulse_period is set to two seconds.

     Again, a light and camera are set up so we can view the scene, and
ten objects are created and randomly positioned.  Some of them are simple
cubes (using the default color assigned by vrl_PrimitiveBox()).  Some of
them are spinning cubes, with a single-entry surfacemap.

     A task is created to make each cube spin.  Each task has a function,
some data, and a "period" which indicates how often the task should be
run.  In this case, the function is spin(), the data is a pointer to the object
to be spun, and the period is 10 ticks.  The period doesn't affect the speed
at which the cube will spin; it only determines how often the spin()
function should be called.  The smaller the number, the more often the
routine will run and the "smoother" the motion will be; of course, running
the tasks more often takes CPU cycles away from rendering.

     The bouncing balls are handled the same way as the spinning
cubes.  The cylinders don't have a task associated with them; instead a
separate task is set up that will cause the pulsing to happen.  The data for
that task is not an object pointer, but rather a pointer to a surface map.

     The tasks themselves are quite straightforward.  The simplest is the
spin() task, which is only two lines long:

static void spin(void)
     {
     vrl_ObjectRotY(vrl_TaskGetData(), vrl_TaskGetElapsed() * spinrate);
     vrl_SystemRequestRefresh();
     }

This task gets a pointer to its data using vrl_TaskGetData(); this is a
pointer to the object associated with this task.  The task also gets the
elapsed time (in ticks) since it last ran, multiplies that value by spinrate,
and rotates the object by that amount around the vertical (Y) axis.  The
spin() function then calls vrl_SystemRequestRefresh(), which tells the
system that the screen should be refreshed (since an object has moved).

     The bounce() task is only slightly more complex; it uses the sine
function to determine the height at which the object should be positioned:

static void bounce(void)
     {
     vrl_Object *obj = vrl_TaskGetData();
     unsigned long off;
     vrl_Scalar height;
     off = (360 * (vrl_TaskGetTimeNow() % bounce_period)) / bounce_period;
     height = vrl_FactorMultiply(vrl_Sine(float2angle(off)), maxheight);
     vrl_ObjectMove(obj, vrl_ObjectGetX(obj), height, vrl_ObjectGetZ(obj));
     vrl_SystemRequestRefresh();
     }

The current time is obtained from vrl_TaskGetTimeNow(), and the %
operator is used to find the modulus (remainder) of the current time
relative to the bounce period.  That value, divided by the bounce period, is
the fraction of the bounce period that has elapsed.  We multiply that by
360 (the number of degrees in a circle) to get an offset value; we take the
sine of that value (using the fast vrl_Sine() routine) and multiply by the
maximum height value.  The vrl_FactorMultiply() routine takes a fractional
number (of the type returned by vrl_Sine()) and multiplies it by a
vrl_Scalar value to get a (smaller) vrl_Scalar value.

     We use vrl_ObjectMove() to actually position the object.  Notice
the use of vrl_ObjectGetX() and vrl_ObjectGetZ() to find the current X
and Z values of the object's location; we don't want to alter those values,
only the height.  A call to the function vrl_SystemRequestRefresh()
ensures that the screen will be redrawn with the object at its new height.

     The pulsate() task is similar to the bounce() task, but instead of
computing a height it computes a brightness and sets it as the new
brightness value of the surface.  Brightness values are in the range of 0 to
255.

Left to Our Own Devices

     AVRIL supports the use of a variety of input devices for
manipulating your viewpoint and the objects in your virtual world.  Our
next example shows one way to use them.

/* EXAMPLE5 -- manipulating a cube with the Logitech Cyberman */

/* Written by Bernie Roehl, August 1994 */

#include <stdlib.h>

#include "avril.h"
#include "avrildrv.h"

vrl_Object *cube = NULL;

static void cube_mover(void)
     {
     vrl_Device *dev = vrl_TaskGetData();
     vrl_Object *viewer = vrl_CameraGetObject(vrl_WorldGetCamera());
     vrl_Vector v;
     if (dev->buttons)
           {
           vrl_ObjectRotReset(cube);
           vrl_ObjectRotY(cube, float2angle(45));
           vrl_ObjectVectorMove(cube, vrl_VectorNULL);
           }
     vrl_ObjectRotate(cube, vrl_DeviceGetValue(dev, YROT),
           Y, VRL_COORD_OBJREL, viewer);
     vrl_ObjectRotate(cube, vrl_DeviceGetValue(dev, XROT),
           X, VRL_COORD_OBJREL, viewer);
     vrl_ObjectRotate(cube, vrl_DeviceGetValue(dev, ZROT),
           Z, VRL_COORD_OBJREL, viewer);
     vrl_VectorCreate(v, vrl_DeviceGetValue(dev, X),
           vrl_DeviceGetValue(dev, Y), vrl_DeviceGetValue(dev, Z));
     vrl_ObjectTranslate(cube, v, VRL_COORD_OBJREL, viewer);
     vrl_SystemRequestRefresh();
     }

void main()
     {
     vrl_Light *light;
     vrl_Camera *camera;
     vrl_Device *dev;

     vrl_SystemStartup();

     cube = vrl_ObjectCreate(vrl_PrimitiveBox(100, 100, 100, NULL));
     vrl_ObjectRotY(cube, float2angle(45));

     light = vrl_LightCreate();
     vrl_LightRotY(light, float2angle(45));
     vrl_LightRotX(light, float2angle(45));

     camera = vrl_CameraCreate();
     vrl_CameraRotX(camera, float2angle(45));
     vrl_CameraMove(camera, 0, 500, -500);

     dev = vrl_DeviceOpen(vrl_CybermanDevice, vrl_SerialOpen(0x2F8, 3,
2000));
     if (dev)
           {
           vrl_DeviceSetScale(dev, X, float2scalar(50));
           vrl_DeviceSetScale(dev, Y, float2scalar(50));
           vrl_DeviceSetScale(dev, Z, float2scalar(50));
           vrl_TaskCreate(cube_mover, dev, 0);
           }

     vrl_SystemRun();
     }

As you can see, there's not much to it.  Most of the code is exactly the
same as our first example;  The only difference is that just before we start
running the main loop, we open up a device.  The first parameter to the
vrl_DeviceOpen() routine is the address of a function that is responsible
for operating the device; in this case, it's called vrl_CybermanDevice, and
it read the Logitech Cyberman.  Notice that we #included the avrildrv.h
file; it has declarations for all the device functions.  When you create a
new device driver (as described in one of the appendices to this document)
you should put an entry into the avrildrv.h file for it.

     The second parameter to vrl_DeviceOpen() is a pointer to a serial
port; we could have opened the serial port, assigned it to a variable, and
passed that variable to the vrl_DeviceOpen() function, but there was no
need to in this case.

     The values 0x2F8 and 3 are the hardware address and IRQ number
of the COM2 port on a PC-compatible; this example is very platform-
specific, but we'll see shortly how to get around that.  The value 2000 is
the size of the input buffer the serial port should use.

     Assuming the device was successfully opened, we scale the X, Y
and Z translation values read by the device to be 50 units; that will be the
maximum number of world-space units per second that we can move
objects using this device.  Finally, we create a task whose data parameter
is a pointer to our newly-opened device.

     The task that does the work of moving the object is called
cube_mover().  You'll notice that unlike our first example program, we've
declared the cube object as a global variable instead of a local one; this so
that cube_mover() can access it.

     The cube_mover() task starts by getting the device pointer, and a
pointer to the object corresponding to our viewpoint.

     vrl_Device *dev = vrl_TaskGetData();
     vrl_Object *viewer = vrl_CameraGetObject(vrl_WorldGetCamera());

It then checks to see if any of the buttons are down on the device; if they
are, it resets the cube's rotations, rotates it to its original angle of 45
degrees around the Y axis, and moves it back to the origin.

     if (dev->buttons)
           {
           vrl_ObjectRotReset(cube);
           vrl_ObjectRotY(cube, float2angle(45));
           vrl_ObjectVectorMove(cube, vrl_VectorNULL);
           }

Next, cube_mover() rotates the cube.  First it does the Y axis, then the X
axis, and finally the Z axis.  In each case, it rotates the cube relative to the
viewer object by an amount that is read from the device.

     vrl_ObjectRotate(cube, vrl_DeviceGetValue(dev, YROT),
           Y, VRL_COORD_OBJREL, viewer);
     vrl_ObjectRotate(cube, vrl_DeviceGetValue(dev, XROT),
           X, VRL_COORD_OBJREL, viewer);
     vrl_ObjectRotate(cube, vrl_DeviceGetValue(dev, ZROT),
           Z, VRL_COORD_OBJREL, viewer);

The final step is to read the X, Y and Z translation values from the device,
store them in a vector, and translate (move) the object along that vector
relative to the viewer.

     vrl_VectorCreate(v, vrl_DeviceGetValue(dev, X),
           vrl_DeviceGetValue(dev, Y), vrl_DeviceGetValue(dev, Z));
     vrl_ObjectTranslate(cube, v, VRL_COORD_OBJREL, viewer);
     vrl_SystemRequestRefresh();

That's it.

An Independence Movement

     The example program above works fine.  If you have a Cyberman. 
And if it's on COM2.  And if all you want to do is move a cube. 
Wouldn't it be nice to have a little more flexibility?

     As it turns out, you can.  AVRIL supports the use of "configuration
files" that store information about a user's preferences and hardware
configuration.  Our next example uses that configuration information to
make our life simpler.

/* EXAMPLE6 -- using the configuration file to simplify setup */

/* Written by Bernie Roehl, August 1994 */

#include "avril.h"

static void object_manipulator(void)
     {
     extern vrl_Object *active_object;  /* defined in input.c */
     vrl_Device *dev = vrl_TaskGetData();
     vrl_Object *viewer = vrl_CameraGetObject(vrl_WorldGetCamera());
     vrl_Vector v;
     if (dev->buttons)
           {
           vrl_ObjectRotReset(active_object);
           vrl_ObjectVectorMove(active_object, vrl_VectorNULL);
           }
     vrl_ObjectRotate(active_object, vrl_DeviceGetValue(dev, YROT),
           Y, VRL_COORD_OBJREL, viewer);
     vrl_ObjectRotate(active_object, vrl_DeviceGetValue(dev, XROT),
           X, VRL_COORD_OBJREL, viewer);
     vrl_ObjectRotate(active_object, vrl_DeviceGetValue(dev, ZROT),
           Z, VRL_COORD_OBJREL, viewer);
     vrl_VectorCreate(v, vrl_DeviceGetValue(dev, X),
           vrl_DeviceGetValue(dev, Y), vrl_DeviceGetValue(dev, Z));
     vrl_ObjectTranslate(active_object, v, VRL_COORD_OBJREL, viewer);
     vrl_SystemRequestRefresh();
     }

void main(int argc, char *argv[])
     {
     vrl_Device *dev;
     vrl_ConfigStartup("example6.cfg");
     vrl_SystemCommandLine(argc, argv);
     dev = vrl_ConfigFindDevice("manipulator");
     if (dev)
           vrl_TaskCreate(object_manipulator, dev, 0);
     vrl_SystemRun();
     }

Our main() is shorter, and simpler.  You'll notice that we now call
vrl_ConfigStartup() instead of vrl_SystemStartup(); vrl_ConfigStartup()
reads the configuration file we specify (in this case "example6.cfg"), calls
vrl_SystemStartup() for us, then configures and initializes all the devices
(even opening the serial ports as specified in the configuration file).  The
format of the configuration file is described in Appendix B.

     The vrl_SystemCommandLine() function reads the command line,
and loads whatever PLG files, FIG files and WLD files we specify there. 
The vrl_ConfigFindDevice() function for a device that was given the name
"manipulator" in the configuration file, and if it finds one it creates a task
to move an object using the manipulation device.

     The object_manipulator() function is almost the same as
cube_mover(), but it uses an external variable called active_object.  As
we'll see later, this variable is found in input.c (where it gets set to the
object most recently selected by the mouse).

     Using this program, we can explore a virtual world, click on objects
to select them, and use the manipulator device we specify in our
configuration file to manipulate the selected object.  All with just a few
lines of code.

A Tiny Program

     AVRIL provides a number of useful utility routines that reduce the
amount of actual programming you have to do in order to create a virtual
world.  A minimal AVRIL program looks like this:

/* A very simple demo of AVRIL */

/* Written by Bernie Roehl, April 1994 */

#include "avril.h"

void main(int argc, char *argv[])
     {
     vrl_ConfigStartup(NULL);
     vrl_SystemCommandLine(argc, argv);
     vrl_SystemRun();
     }

The NULL parameter to vrl_ConfigStartup() causes it to use its built-in
default of "avril.cfg".  This example shows just how little it takes to create
a VR program using AVRIL.

Of Mice and Menus

     By now, you've probably noticed that something is missing; how
have our programs been able to respond to our keystrokes and mouse
presses?  Well, AVRIL does some of this for you automatically.  When
you call vrl_SystemRun(), you're essentially turning control of the
application over to the system.  From time to time, the system will make
calls back to your application to give you control if you need it.  (If you
don't like this approach, you're not stuck with it; the source for the
vrl_System functions is provided, so you can do things however you like).

     There are currently five places that the system calls your
application.  Just before starting its main internal loop, it calls
vrl_ApplicationInit().  Just after it clears the screen (or draws the horizon,
as the case may be) but before it does the actual rendering of the scene, it
calls vrl_ApplicationDrawUnder().  You can use that routine to "underlay"
information on the screen that appears behind any objects that are drawn. 
If you want to use your own background, just turn off screen clearing
using vrl_WorldSetScreenClear(0) and do your background drawing in
vrl_ApplicationDrawUnder().

     After the system has rendered the entire scene, it calls
vrl_ApplicationDrawOver(); this allows you to "overlay" information on
the screen.  The vrl_ApplicationDrawOver() routine is where you would
put any "heads-up display" type information, such as frame rate or
orientation information.

     Whenever a keystroke is detected, it's passed to the
vrl_ApplicationKey() routine.  Similarly, mouse-up events are passed to the
application using vrl_ApplicationMouseUp().

     All of these routines have default versions in the AVRIL library, so
you don't have to write all of them.  The default versions of the functions
vrl_ApplicationDrawUnder(), vrl_ApplicationDrawOver() and
vrl_ApplicationMouseUp() are empty (i.e., they don't do anything).  The
default version of vrl_ApplicationKey() just checks to see if the user has
pressed the ESC key; if they have, vrl_SystemStopRunning() is called.

     In addition to all this, there's a simple menu system built into this
version of AVRIL; it will be described later.

A Moving Experience

Objects can have functions and data associated with them.  When the
system walks through the hierarchy of objects, it calls each object's
function; those functions can make use of the data associated with the
object.

     The default vrl_ApplicationInit() routine sets up an object function
to let an input device (the keypad by default) move the user around.  You
can look at the code in input.c for all the details, but essentially here's
what it does

     vrl_Device *headdev = vrl_ConfigFindDevice("head");
     if (headdev == NULL)
           headdev = vrl_DeviceOpen(vrl_KeypadDevice, 0);
     head = vrl_CameraGetObject(vrl_WorldGetCamera());
     vrl_ObjectSetApplicationData(head, headdev);
     vrl_ObjectSetFunction(head, object_move_locally);

If no head device is found in the configuration file, the keypad is used. 
The head is found (the head being the object to which the camera is
attached), and the head object's application-specific data field is set to
point to the headdev.  The head's function is set to object_move_locally(),
which looks like this:

static int object_move_locally(vrl_Object *obj)
     {
     return object_mover(obj, vrl_ObjectGetApplicationData(obj),
VRL_COORD_LOCAL);
     }

The functions that are set on objects get called whenever the world is
updated by the vrl_ObjectUpdate() or vrl_WorldUpdate() routines.  When
object_move_locally() gets called, it just calls object_mover() on the
object, passing the device pointer which is stored in the object's
application data.

     The object_mover() routine is basically the same as the movement
tasks that were described earlier (the ones in example 6) but slightly more
general.

Lots of Input

     The file input.c contains simple versions of
vrl_ApplicationDrawOver(), vrl_ApplicationMouseUp(),
vrl_ApplicationKey() and vrl_ApplicationInit() that are shared by all our
example programs.  The vrl_ApplicationMouseUp() routine looks like this:


vrl_Object *active_object = NULL;  /* points to the currently-selected object,
if any */

void vrl_ApplicationMouseUp(int x, int y, unsigned int buttons)
     {
     vrl_Object *old_active = active_object;
     if ((buttons & 1) == 0)
           return;
     vrl_RenderMonitorInit(x, y);
     vrl_SystemRender(NULL);  /* redraw screen */
     if (vrl_RenderMonitorRead(&active_object, NULL, NULL))
           {
           if (active_object == old_active)
                active_object = NULL;
           else
                vrl_ObjectSetHighlight(active_object, 1);
           }
     if (old_active)
           vrl_ObjectSetHighlight(old_active, 0);
     vrl_SystemRequestRefresh();
     }

This routine uses the "Monitor" facility of AVRIL to allow the user to
select objects.  The mouse location is passed to vrl_RenderMonitor(); this
tells the system to keep an eye on that point on the screen.  The screen is
then re-drawn using vrl_SystemRender(), and the monitor is read using
vrl_RenderMonitorRead().  If that function returns a non-zero value, then
the mouse cursor was on top of an object; since we passed &active_object
to the vrl_RenderMonitorRead() function, active_object now points to the
object that the mouse cursor was on top of.  This is the object that got
moved around by the manipulation device in example 6.  If the user clicks
again on the previously-selected object, then the active_object is set to
NULL; otherwise, the newly-activated object gets its highlighting turned
on.  In any case, we un-highlight the previously active object, and tell the
system the screen needs to be refreshed (since the highlighting of an object
has changed).

     The vrl_ApplicationKey() routine is very simple; the only
complicated part is that it handles auto-repeat of keystrokes:

void vrl_ApplicationKey(unsigned int c)
     {
     static int lastkey = 0;
     if (c == INS)
           {
           int i;
           for (i = 0; i < 100; ++i)
                {
                process_key(lastkey);
                
vrl_SystemRender(vrl_ObjectUpdate(vrl_WorldGetObjectTree()));
                }
           }
     else
           process_key(lastkey = c);
     }

If the key is INS (defined in avrilkey.h), the last key is re-processed 100
times; all other keys are processed once, and the lastkey variable is
updated.  Notice the call to vrl_SystemRender(); it looks pretty
complicated, but after you read some of the later sections it will make
more sense.  We need to update the world and re-render the scene after
every keystroke, so the user will see the ongoing changes.

     The process_key() function is fairly long, and will probably change
from version to version of AVRIL.  Most of it should be pretty easy to
understand, so you may want to take a few minutes to look through the
source code in input.c (where you'll also find the source for the
vrl_ApplicationMouseUp() and vrl_ApplicationDrawOver() routines).

     The vrl_ApplicationDrawOver() routine provides the position, frame
rate, compass and "heads-up display" support for the AVRIL demos.  It
looks like this:

void vrl_ApplicationDrawOver(vrl_RenderStatus *stat)
     {
     vrl_Camera *cam = vrl_WorldGetCamera();
     char buff[100];
     if (vrl_ConfigGetPositionDisplay())
           {
           sprintf(buff, "Position: %ld,%ld", vrl_CameraGetX(cam),
vrl_CameraGetZ(cam));
           vrl_UserInterfaceDropText(10, 10, 15, buff);
           }
     if (vrl_ConfigGetFramerateDisplay())
           {
           sprintf(buff, "Frames/sec: %ld", vrl_SystemGetFrameRate());
           vrl_UserInterfaceDropText(5, 170, 15, buff);
           }
     if (vrl_ConfigGetCompassDisplay())
           vrl_UserInterfaceDrawCompass(cam, 250, 40, 35);
     if (showhud)
           {
           sprintf(buff, "%c%c%c",
                stat->memory ?  'M' : ' ',
                stat->objects ? 'O' : ' ',
                stat->facets ?  'F' : ' ');
           vrl_UserInterfaceDropText(10, 20, 15, buff);
           }
     if (vrl_MouseGetUsage())
           {
           vrl_Device *dev = vrl_MouseGetPointer();
           if (dev)
                {
                int x = vrl_DeviceGetCenter(dev, X);
                int y = vrl_DeviceGetCenter(dev, Y);
                int deadx = vrl_DeviceGetDeadzone(dev, X);
                int deady = vrl_DeviceGetDeadzone(dev, Y);
                /* white inner box */
                vrl_DisplayLine(x - deadx, y - deady, x + deadx, y -
deady, 15);
                vrl_DisplayLine(x - deadx, y + deady, x + deadx, y +
deady, 15);
                vrl_DisplayLine(x - deadx, y - deady, x - deadx, y +
deady, 15);
                vrl_DisplayLine(x + deadx, y - deady, x + deadx, y +
deady, 15);
                /* black outer box */
                vrl_DisplayLine(x-deadx-1, y-deady-1, x+deadx+1,
y-deady-1, 0);
                vrl_DisplayLine(x-deadx-1, y+deady+1, x+deadx+1,
y+deady+1, 0);
                vrl_DisplayLine(x-deadx-1, y-deady-1, x-deadx-1,
y+deady+1, 0);
                vrl_DisplayLine(x+deadx+1, y-deady-1, x+deadx+1,
y+deady+1, 0);
                }
           }
     }

There are several "configuration" variables that get accessed to determine
what information should be overlaid on the display; the state of those
configuration variables is toggled by code in process_key().  This
configuration information will be explained in more detail later, in the
section about configuration files.

     The call to vrl_WorldGetCamera() returns a pointer to the
currently-active virtual camera.  The buffer buff[] will be used to construct
strings that we want to display on the screen.

     If the user wants their location displayed, a text string containing
the camera's current X and Z values is constructed and displayed at
location (10, 10) on the screen.  The first value is the horizontal distance
in pixels from the left of the screen, and the second value is the vertical
distance in pixels from the top of the screen.  The color used is 15, which
is white.  The vrl_UserInterfaceDropText() function automatically produces
a "drop shadow" behind the text, ensuring it's visible even if it's overlaid
on top of a white background.

     If the user wants a compass to be shown, the
vrl_UserInterfaceDrawCompass() routine is called.  The compass is
displayed at location (250, 40) on the screen, and each "arm" of the
compass is 35 pixels long.

     If the showhud variable is set, a variety of debugging information is
displayed.  When the renderer draws a scene, it may run out of internal
memory, or it may find there are too many objects or facets for it to
process.  If this happens, it sets bits in a special structure; a pointer to this
structure is passed to vrl_ApplicationDrawOver(), so that it can alert the
user to the problem.  In this case, an 'M' is displayed if the renderer ran
out of memory, an 'O' is displayed if there were too many objects, and an
'F' is displayed if there were too many facets.

     If the mouse is in 6D input device mode, a small square is drawn
on the screen; if the mouse cursor is inside this box, there'll be no
movement.  It's sort of a visual "dead zone", if you will.  The idea for this
box came from a demo of the Superscape VR system; it was a clever
enough idea that I adopted it for this example.

Into the System

     We've talked a lot so far about the vrl_System routines; now let's
take a closer look at how they work.

vrl_Boolean vrl_SystemStartup(void)
     {
     vrl_MathInit();
     if (vrl_DisplayInit(0))
           {
           printf("Could not enter graphics mode!\n");
           return -1;
           }
     atexit(vrl_DisplayQuit);
     vrl_MouseInit();
     atexit(vrl_MouseQuit);
     if (vrl_TimerInit())
           return -2;
     atexit(vrl_TimerQuit);
     if (vrl_RenderInit(800, 800, 500, 5, 65000))
           return -3;
     atexit(vrl_RenderQuit);
     atexit(vrl_DeviceCloseAll);
     atexit(vrl_SerialCloseAll);
     /* make sure that exit() [and therefore the atexit() functions] get
        called if there are any fatal errors */
     signal(SIGABRT, exit);
     signal(SIGFPE, exit);
     signal(SIGILL, exit);
     signal(SIGINT, exit);
     signal(SIGSEGV, exit);
     signal(SIGTERM, exit);
     vrl_SystemStartRunning();
     vrl_SystemRequestRefresh();
     return 0;
     }

The vrl_SystemStartup() routine does the initialization of all the various
AVRIL subsystems.  It starts by calling vrl_MathInit(), which sets up the
trig tables used internally by AVRIL (for example, a table of sines that's
used by the vrl_Sine() function described earlier).  Then the display is
initialized into graphics mode, and the mouse and timer are set up.

     After that, the rendering engine itself is initialized; the parameters
to the vrl_RenderInit() function may change with a future release of the
software, but for now just use the values that are shown above.  The value
52000 is the amount of memory the renderer should allocate for its internal
use; if the renderer needs more than this amount of memory when
rendering a scene, it will set the "memory" value in the status struct
described earlier (which is passed to vrl_ApplicationDrawOver()).  If the
renderer is unable to initialize itself (for example, if it couldn't allocate the
specified amount of memory) then vrl_RenderInit() returns a non-zero
value.

     Notice the use of atexit() to ensure that everything is shut down
properly when the program exits.  The signal() calls ensure that the exit()
routine will be called in case of any errors; exit() will in turn call the
various atexit() functions, cleanly closing down the system.

     Finally, vrl_SystemStartRunning() is called and an initial display
refresh is requested.  The vrl_SystemStartRunning(),
vrl_SystemStopRunning(), and vrl_SystemIsRunning() routines are used to
control whether the system is currently "running" or not.  They just set and
check the value of the variable system_is_running; however, using the
routines makes your code a bit more readable.  It's also possible to
redefine those routines to do something in addition to just setting or
clearing a flag.

     The vrl_SystemRun() routine is the main loop of every AVRIL
application.  It looks like this:

void vrl_SystemRun(void)
     {
     vrl_ApplicationInit();
     while (vrl_SystemIsRunning())
           {
           vrl_Object *list;
           if (vrl_KeyboardCheck())
                vrl_ApplicationKey(vrl_KeyboardRead());
           check_mouse();
           vrl_TaskRun();
           vrl_DevicePollAll();
           list = vrl_WorldUpdate();
           if (vrl_SystemQueryRefresh())
                vrl_SystemRender(list);
           }
     }

It shouldn't come as any surprise that this looks like an event loop in a
GUI application; on some systems, that's exactly how vrl_SystemRun()
will be implemented.  However, on a DOS platform it's necessary to
explicitly check the mouse and keyboard for activity.

     If a key has been pressed, the keyboard is read and the value of the
key is passed to vrl_ApplicationKey().  The function check_mouse() is
used to interrogate the mouse for updates:

static void check_mouse(void)
     {
     unsigned int mouse_buttons;
     if (vrl_MouseGetUsage())  /* being used as 6D pointing device */
           return;
     if (!vrl_MouseRead(NULL, NULL, NULL))  /* mouse hasn't changed */
           return;
     vrl_MouseRead(NULL, NULL, &mouse_buttons);
     if (mouse_buttons)  /* button down */
           {
           int mouse_x, mouse_y;
           int win_x, win_y;
           unsigned int down_buttons = mouse_buttons;
           vrl_DisplayGetWindow(&win_x, &win_y, NULL, NULL);
           while (mouse_buttons)  /* wait for button release */
                vrl_MouseRead(&mouse_x, &mouse_y, &mouse_buttons);
           if (down_buttons & 0x07)
                vrl_ApplicationMouseUp(mouse_x - win_x, mouse_y -
win_y, down_buttons);
           }
     }

The vrl_MouseGetUsage() call is necessary because the mouse can be used
in either of two completely different ways: as a pointing device for
selecting objects on the screen, or as a 6 Degree-Of-Freedom (6D) input
device; the 6D mode is described later, in the section on input devices.  If
vrl_MouseGetUsage() returns a non-zero value, then the mouse is being
used as a 6D input device, and input from it shouldn't be processed any
further at this point.

     If the mouse hasn't changed location or button status, the call to
vrl_MouseRead() will return zero, in which case no further processing is
done.  If the mouse buttons are down, the routine tracks the mouse input
until the buttons are released.  The button status is saved in the variable
down_buttons, and then passed to the routine vrl_ApplicationMouseUp()
along with the mouse cursor location in the current window.

     Back in vrl_SystemRun(), the vrl_TaskRun() function is called to
run all the tasks that have been created (like the spin(), bounce() and
pulsate() tasks we used in example 4).  Next, vrl_WorldUpdate() is called;
it walks the hierarchical tree of objects in the world, updating their
location and orientation information and threading them onto a linked list
which is returned as the value of the vrl_WorldUpdate() function. 
Walking the tree also causes the function associated with each object to be
called.

     If the display needs to be redrawn (i.e. the
vrl_SystemRequestRefresh() routine that we mentioned earlier has been
called at least once since we last re-drew the screen) then the
vrl_SystemRender() routine is called, and is given the linked list of objects
to render.

     The vrl_SystemRender() routine does the actual updating of the
screen.  Even though source is provided, you should use this routine as-is;
it's likely to change in future releases of AVRIL, and several additional
features will be added.  The code currently looks like this:

vrl_RenderStatus *vrl_SystemRender(vrl_Object *list)
     {
     static vrl_Object *lastlist = NULL;
     int pagenum;
     vrl_RenderStatus *stat;
     vrl_Time render_start;
     if (list == NULL)
           {
           list = lastlist;
           if (list == NULL)
                return NULL;
           }
     else
           lastlist = list;
     pagenum = vrl_DisplayGetDrawPage();
     if (++pagenum >= vrl_DisplayGetNpages())
           pagenum = 0;
     vrl_DisplaySetDrawPage(pagenum);
     render_start = vrl_TimerRead();
     vrl_RenderBegin(vrl_WorldGetCamera(), vrl_WorldGetLights());
     vrl_RenderSetAmbient(vrl_WorldGetAmbient());
     if (vrl_WorldGetScreenClear())
           {
           if (vrl_WorldGetHorizon() && !vrl_DisplayGetDrawMode())
                vrl_RenderHorizon();
           else
                vrl_DisplayClear(vrl_WorldGetSkyColor());
           }
     vrl_ApplicationDrawUnder();
     stat = vrl_RenderObjlist(list);
     last_render_ticks = vrl_TimerRead() - render_start;
     vrl_ApplicationDrawOver(stat);
     vrl_MouseCursorHide();
     vrl_DisplayUpdate();
     vrl_DisplaySetViewPage(pagenum);
     vrl_MouseCursorShow();
     need_to_redraw = 0;
     return stat;
     }

If the list that the vrl_SystemRender() routine is given is NULL (as was
the case in our example vrl_ApplicationMouseUp() routine in input.c) then
the last list of objects rendered is used.

     The system uses the concept of a "draw" page (on which drawing
takes place) and a "view" page (which is the one the user is currently
viewing).  The vrl_SystemRender() routine gets the current drawing page
number, and increments it (so we start drawing on the next page).  It
wraps back to page zero after it's drawn the last available display page.  It
then sets the render_start variable to the current time (to keep track of
how long it takes to draw this screen), then calls vrl_RenderBegin() to set
the current camera and list of light sources.  It sets the ambient light level
to be that for the current world, checks to see if it should clear the screen,
then either clears it or draws a horizon.  The vrl_ApplicationDrawUnder()
routine we looked at earlier is then called, and the vrl_RenderObjectlist()
routine is called to do the actual rendering.  The last_render_ticks variable
is set; this is the value returned by vrl_SystemGetRenderTime(), and is
also used to compute the frame rate returned by
vrl_SystemGetFrameRate().

     The vrl_ApplicationDrawOver() routine is then called to put any
additional information on the display.  The cursor is hidden, and
vrl_DisplayUpdate() is called; this is necessary, since some display systems
don't have multiple pages and instead use an off-screen buffer which the
vrl_DisplayUpdate() routine copies to the screen.  For systems that have
multiple-page displays, the current view page is set to the (now finished)
drawing page.  The mouse cursor is then revealed again, and the
need_to_redraw variable (which was set by vrl_SystemRequestRefresh()) is
cleared.

     The final vrl_System routine is vrl_SystemCommandLine().  It just
goes through the command-line parameters and calls the appropriate
routines to read the various types of files:

void vrl_SystemCommandLine(int argc, char *argv[])
     {
     int i;
     vrl_Camera *cam;
     for (i = 1; i < argc; ++i)  /* i = 1 to skip argv[0] */
           {
           FILE *in = fopen(argv[i], "r");
           if (in == NULL) continue;
           if (strstr(argv[i], ".wld"))
                vrl_ReadWLD(in);
           else if (strstr(argv[i], ".fig"))
                vrl_ReadFIG(in, NULL, NULL);
           else if (strstr(argv[i], ".plg"))
                vrl_ReadObjectPLG(in);
           /* ignore anything else */
           fclose(in);
           }
     if (!vrl_WorldGetCamera())   /* need to have a camera */
           vrl_CameraCreate();
     vrl_WorldUpdate();
     }

After all the files on the command line have been processed, the
vrl_SystemCommandLine() routine checks to see if a current camera has
been set.  If not, a new camera is created.

Configuration Files

AVRIL supports the loading of configuration files; the format of these files
is given in Appendix B.  The functions that support configuration
information are

     int vrl_ReadCFG(FILE *in);
     void vrl_ConfigureAllDevices(void);
     vrl_Device *vrl_ConfigFindDevice(char *name);
     vrl_DisplayDriverFunction *vrl_ConfigGetDisplayDriver(void);
     char *vrl_ConfigGetDisplayDriverName(void);
     unsigned int vrl_ConfigGetDisplayMode(void);

The vrl_ReadCFG() function reads a configuration file and stores the
information from it in an internal set of data structures.  Descriptions of
devices are stored, and vrl_ConfigureAllDevices() can be used to open all
the devices specified in the file; this includes opening any serial ports that
may be required.  The vrl_ConfigFindDevice() routine returns a pointer to
the device that was assigned the given name in the configuration file.  For
example,

     vrl_Device *headdev = vrl_ConfigFindDevice("headtracker");

The vrl_ConfigGetDisplayDriver() function returns a pointer to the display
driver function, and vrl_ConfigGetDisplayDriverName() returns a pointer
to the textual name of the display driver.  The
vrl_ConfigGetDisplayMode() function returns the mode to which the
display should be initialized.

     There are also routines for reading, setting and toggling the various
flags that the user can specify in the configuration file:

     void vrl_ConfigSetCompassDisplay(vrl_Boolean flag);
     vrl_Boolean vrl_ConfigGetCompassDisplay(void);
     void vrl_ConfigToggleCompassDisplay(void);
     void vrl_ConfigSetPositionDisplay(vrl_Boolean flag);
     vrl_Boolean vrl_ConfigGetPositionDisplay(void);
     void vrl_ConfigTogglePositionDisplay(void);
     void vrl_ConfigSetFramerateDisplay(vrl_Boolean flag);
     vrl_Boolean vrl_ConfigGetFramerateDisplay(void);
     void vrl_ConfigToggleFramerateDisplay(void);

The simplest way to make use of all of this is to use the following
function:

     void vrl_ConfigStartup(char *filename);

This function opens and reads the specified configuration file, sets the
appropriate display driver and mode, calls vrl_SystemStartup(), and
configures all the devices.  If the filename pointer is NULL, then the file
"avril.cfg" is used.

     The typical use of vrl_ConfigStartup() is as a replacement for
vrl_SystemStartup().  For example, if you wanted to let the user specify a
configuration file to load by setting the AVRIL environment variable, you
would make the following call in place of vrl_SystemStartup():

     vrl_ConfigStartup(getenv("AVRIL"));

If no AVRIL environment variable is found, getenv() will return NULL
and avril.cfg will be used.

Part II - Technical Reference

     This section describes AVRIL from a technical perspective, and
describes the data types and functions that are available.  Anything that is
not documented here should not be used, since it's subject to change in
future releases of AVRIL.  Also keep in mind that some of the routines
described below may be implemented as macros, and that this may also
change in future releases; none of your code should assume that any
particular routine is either a macro or a real function.

     There are a number of important concepts that are essential to an
understanding of how AVRIL works.  Let's start by examining them.

Basic Data Types

     AVRIL uses a left-handed coordinate system; if the X axis points to
the right, and the Y axis points up, then Z points straight ahead.  If you're
looking at the origin from the positive end of an axis, a clockwise rotation
is a positive rotation angle.

     Distances in AVRIL are represented by vrl_Scalars, and rotation
angles by vrl_Angles.  AVRIL can be compiled to use either floating point
or fixed point, so it's important to use the vrl_Scalar and vrl_Angle types
for portability.  vrl_Scalars should always be treated as (long) integer
values, regardless of whether floating or fixed point is used.  vrl_Angles
are always measured in degrees (converted to the internal vrl_Angle
format, of course).  A third fundamental data type is vrl_Factor, which is
used for things like the return value of trig functions; a special constant
called VRL_UNITY is #defined to be a vrl_Factor of 1.

     In a floating point implementation, all three types (vrl_Scalars,
vrl_Angles and vrl_Factors) are stored as floats; in a fixed-point
implementation, they're all 32-bit integers (which will be "long" on many
systems).  The following macros are provided for converting between
floating point (or regular integers) and the three special types:

     float2scalar(float);
     scalar2float(vrl_Scalar);

     float2angle(float);
     angle2float(vrl_Angle);

     float2factor(float);
     factor2float(vrl_Factor);

Several routines are provided to support portable multiplication and
division of the types:

     vrl_Factor vrl_ScalarDivide(vrl_Scalar a, vrl_Scalar b);
     vrl_Scalar vrl_ScalarMultDiv(vrl_Scalar a, vrl_Scalar b, vrl_Scalar
c);
     vrl_Scalar vrl_FactorMultiply(vrl_Factor a, vrl_Scalar b);

The first of these routines simply divides two vrl_Scalars and returns a
vrl_Factor result; the absolute value of a should be less than the absolute
value of b.  The second routine multiplies two vrl_Scalars and divides by a
third, using a 64-bit intermediate result; in other words, it computes
(a*b)/c.  The third routine multiplies a vrl_Factor by a vrl_Scalar and
returns a vrl_Scalar result; it can also be used for multiplying two
vrl_Factors, or an integer or long value by a vrl_Factor.  The order of the
operands is significant, because C automatically promotes ints to longs.

     On floating-point implementations, there may be occasions where
the computed value of a vrl_Scalar has a fractional part; in such cases you
should use the following function:

     vrl_Scalar vrl_ScalarRound(vrl_Scalar value);

to round to the nearest valid vrl_Scalar value.

     There are currently two trig routines, vrl_Sine() and vrl_Cosine();
they both take vrl_Angles as parameters and return vrl_Factors:

     vrl_Factor vrl_Sine(vrl_Angle angle);
     vrl_Factor vrl_Cosine(vrl_Angle angle);

The routine vrl_MathInit() should be called before calling any of the trig
functions; it pre-computes the trig tables.  This is done in the
vrl_SystemStartup() routine (found in system.c).

     There are two other basic types used in AVRIL: vrl_Time is a
measure of elapsed time in ticks, and vrl_Boolean is a true/false type value
(non-zero being true).

Vectors

     A vrl_Vector is a three-element array, which can be indexed by the
#defined constants X, Y and Z; for example, if v is a vector then v[X] is
the X-component of the vector.  In general, vrl_Vectors are made up of
three vrl_Scalars; however, a normalized vector (such as a facet normal, a
basis vector, or a vector that's been normalized using the
vrl_VectorNormalize() function) will actually have vrl_Factors as elements. 
The following functions perform fundamental operations on vrl_Vectors:

     void vrl_VectorCreate(vrl_Vector result, vrl_Scalar x, vrl_Scalar y,
vrl_Scalar z);
     void vrl_VectorCopy(vrl_Vector destination, vrl_Vector source);
     void vrl_VectorAdd(vrl_Vector result, vrl_Vector v1, vrl_Vector v2);
     void vrl_VectorSub(vrl_Vector result, vrl_Vector v1, vrl_Vector v2);
     void vrl_VectorNegate(vrl_Vector v);
     vrl_Factor vrl_VectorDotproduct(vrl_Vector v1, vrl_Vector v2);
     vrl_Scalar vrl_VectorCrossproduct(vrl_Vector result, vrl_Vector v1,
vrl_Vector v2);
     vrl_Scalar vrl_VectorMagnitude(vrl_Vector v);
     void vrl_VectorNormalize(vrl_Vector v);
     vrl_Scalar vrl_VectorDistance(vrl_Vector v1, vrl_Vector v2);
     void vrl_VectorScale(vrl_Vector v, vrl_Scalar newmag);
     void vrl_VectorRescale(vrl_Vector v, vrl_Scalar newmag);
     void vrl_VectorPrint(FILE *out, char *str, vrl_Vector v);
     vrl_Boolean vrl_VectorEqual(vrl_Vector v1, vrl_Vector v2);
     void vrl_VectorZero(vrl_Vector v);

The vrl_VectorCreate() function takes three vrl_Scalars and assembles
them into a vrl_Vector.  The vrl_VectorCopy(), vrl_VectorAdd() and
vrl_VectorSub() routines do element-by-element copies, additions and
subtractions of vrl_Vectors.  The vrl_VectorNegate() function reverses the
direction of a vrl_Vector by flipping the sign of each of its components. 
The vrl_VectorDotproduct() routine computes the dot product (inner
product) of two vectors; at least one of the vectors should be normalized
for this to work properly.

     The vrl_VectorCrossproduct() routine computes the vector cross
product (outer product) of two vectors.  This is likely to be slow, since it
normalizes the result (which involves doing a square root operation).  It
returns the magnitude of the vector prior to normalization.  The
vrl_Magnitude() routine returns the magnitude of a vector, and the
vrl_VectorNormalize() routine scales a vector so that it has a magnitude of
1.

     The vrl_VectorDistance() takes two vrl_Vectors (each representing
a point in space) and computes the distance between those two points.  The
vrl_Scale() function takes a normalized vrl_Vector and scales all its
components by the given amount; the vrl_Rescale() function takes a non-
normalized vector and re-scales it to have the specified magnitude.

     The vrl_VectorPrint() routine prints out a message followed by the
values of each of the components of the vrl_Vector, enclosed in square
brackets.  Do not attempt to write to the screen with this routine, since it
will not work well in graphics mode.

     The vrl_VectorEqual() routine returns a non-zero value if the two
vrl_Vectors are identical; vrl_VectorZero() sets the components of a
vrl_Vector to zero.  The [0,0,0] vector is sometimes needed, so a global
vrl_Vector variable called vrl_VectorNULL is defined.

Matrices

     A vrl_Matrix is a 4 by 3 array that stores location and orientation
information.  All AVRIL matrices are homogenous; the upper 3 by 3
submatrix stores rotation information and the last 3-element row stores a
translation vector.  You should never have to deal with the vrl_Matrix type
directly.  However, in case you do have a need to deal with actual
matrices, the following routines are provided:

     void vrl_MatrixIdentity(vrl_Matrix m);
     void vrl_MatrixCopy(vrl_Matrix result, vrl_Matrix m);
     void vrl_MatrixMultiply(vrl_Matrix result, vrl_Matrix m1, vrl_Matrix
m2);
     void vrl_MatrixInverse(vrl_Matrix result, vrl_Matrix m);
     void vrl_MatrixRotX(vrl_Matrix m, vrl_Angle angle, vrl_Boolean
leftside);
     void vrl_MatrixRotY(vrl_Matrix m, vrl_Angle angle, vrl_Boolean
leftside);
     void vrl_MatrixRotZ(vrl_Matrix m, vrl_Angle angle, vrl_Boolean
leftside);
     void vrl_MatrixRotVector(vrl_Matrix m, vrl_Angle angle, vrl_Vector
vector,
           vrl_Boolean leftside);
     void vrl_MatrixResetRotations(vrl_Matrix m);
     void vrl_MatrixGetBasis(vrl_Vector v, vrl_Matrix m, int axis);
     void vrl_MatrixTranslate(vrl_Matrix result, vrl_Scalar x, vrl_Scalar
y, vrl_Scalar z);
     void vrl_MatrixSetTranslation(vrl_Matrix result,
           vrl_Scalar x, vrl_Scalar y, vrl_Scalar z);
     void vrl_MatrixGetTranslation(vrl_Vector v, vrl_Matrix m);
     
The vrl_MatrixIdentity() function sets the matrix to zeroes, except for the
diagonal elements which are set to VRL_UNITY.  The vrl_MatrixCopy()
and vrl_MatrixMultiply() routines are used to copy and multiply matrices,
and the vrl_MatrixInverse() routine computes the matrix inverse.  The
various rotation functions apply a rotation around X, Y, Z or a specified
vector by a given angle; the vrl_MatrixResetRotations() routine sets all the
rotations to zero.  Several of the vrl_Matrix routines use a leftside
parameter; if non-zero, this parameter specifies that the transformation
should be applied as a pre-multiplication instead of a post-multiplication.

     The function vrl_MatrixGetBasis() gets one of the basis vectors of
the rotation part of the matrix; this is equivalent to (but faster than)
transforming an axis-aligned unit vector by the matrix.  In other words,
vrl_MatrixGetBasis(v, m, X) is equivalent to transforming the vector
[1,0,0] by the rotation part of the matrix and storing the result in the vector
v.

     The vrl_MatrixTranslate() routine applies a translation to the
matrix, and vrl_MatrixSetTranslation() sets the actual translation part of the
matrix.  The vrl_MatrixGetTranslation() routine fills the given vector with
the current translation part of the matrix.

Transforms

     You should never have to use any of the transform functions
directly; this is all handled for you by AVRIL.  A vector can be
transformed by a matrix, or each component of the transform (X, Y or Z)
can be computed separately:

     void vrl_Transform(vrl_Vector result, vrl_Matrix m, vrl_Vector v);
     vrl_Scalar vrl_TransformX(vrl_Matrix m, vrl_Vector v);
     vrl_Scalar vrl_TransformY(vrl_Matrix m, vrl_Vector v);
     vrl_Scalar vrl_TransformZ(vrl_Matrix m, vrl_Vector v);

Coordinate Systems

     AVRIL allows objects to be translated or rotated in five different
coordinate systems.  This may seem like a lot, but they're easy to get used
to.  An object can be moved in its own local coordinate system, the
coordinate system of the object it's attached to, the "world" coordinate
system, the viewer's coordinate system, or the coordinate system of
another object.

     For example, consider a bicycle on an open train car.  The bicycle
is facing sideways, so that if you were sitting on it you'd be watching the
scenery go by on the right side of the train.  The train itself is moving
northeast.  If we translate the bicycle along the positive Z axis in its local
coordinate system, it will travel sideways off the train car, in a south-
easterly direction.  If we move it in the positive Z direction of its parent, it
will move to the rider's left, towards the front of the train (northeast).  If
we move it in the positive Z direction in the world, it will move due north. 
If we're looking at it from directly above, moving the bicycle in the
viewer's positive Z direction would send it through the train car and down
into the ground.  If a bird is flying due south, then moving the bicycle in
the positive Z direction relative to the bird would make the bike move due
south.

     We represent these various coordinate systems by the constants
VRL_COORD_LOCAL, VRL_COORD_PARENT,
VRL_COORD_WORLD, and VRL_COORD_OBJREL.  The view-relative
coordinate system is just a special case of the VRL_COORD_OBJREL
coordinate frame, with the viewer as the object that the movement should
be relative to.

Worlds

     In AVRIL, a virtual world is a collection of objects, light sources,
virtual cameras and miscellaneous attributes.  You can have any number of
worlds within a single AVRIL application; they're distinct from each other,
and you can switch between them whenever you like.

     When you run an AVRIL program, a default world is created and
initialized for you; if you only plan on having one world in your
application, you don't have to do anything special.  If you want to create
additional worlds, you can simply declare variables of type vrl_World and
initialize them by calling vrl_WorldInit(&yourworld); however, it's
probably better to dynamically allocate them using malloc().  In fact, the
simplest way to create a world is with the vrl_WorldCreate() function,
which allocates the space and initializes the world for you.  To make a
given world current, use the vrl_WorldSetCurrent() function; the
vrl_WorldGetCurrent() function can be used to get a pointer to the current
world.

     vrl_World *vrl_WorldInit(vrl_World *world);
     vrl_World *vrl_WorldCreate(void);
     void vrl_WorldSetCurrent(vrl_World *world);
     vrl_World *vrl_WorldGetCurrent(void);

You can easily add objects, light sources and cameras to the current world,
and remove them; you can also count how many of each the current world
contains, and get pointers to the linked list of lights, linked list of cameras
and the hierarchical tree of objects  You can also find lights, cameras and
objects by name.

     void vrl_WorldAddLight(vrl_Light *light);
     void vrl_WorldRemoveLight(vrl_Light *light);
     vrl_Light *vrl_WorldFindLight(char *name);

     void vrl_WorldAddCamera(vrl_Camera *camera);
     void vrl_WorldRemoveCamera(vrl_Camera *camera);
     vrl_Camera *vrl_WorldFindCamera(char *name);

     void vrl_WorldAddObject(vrl_Object *obj);
     void vrl_WorldRemoveObject(vrl_Object *obj);
     vrl_Object *vrl_WorldFindObject(char *name);

     int vrl_WorldCountObjects(void);
     int vrl_WorldCountLights(void);
     int vrl_WorldCountCameras(void);

     vrl_Light *vrl_WorldGetLights(void);
     vrl_Light *vrl_WorldGetCameras(void);
     vrl_Object *vrl_WorldGetObjectTree(void);

If you need to iterate through the linked list of lights or cameras, you can
use the functions

     vrl_Light *vrl_LightGetNext(vrl_Light *light);
     vrl_Camera *vrl_CameraGetNext(vrl_Camera *camera);

You can also obtain information about the total number of facets in the
world, the minimum and maximum bounds of the world, the center of the
world and the radius of the world's bounding sphere using these functions:

     int vrl_WorldCountFacets(void);
     void vrl_WorldGetBounds(vrl_Vector v1, vrl_Vector v2);
     void vrl_WorldGetCenter(vrl_Vector v);
     vrl_Scalar vrl_WorldGetSize(void);

Each world has a "current camera" through which the world is seen; you
can set the current camera, or get a pointer to it using these routines:

     void vrl_WorldSetCamera(vrl_Camera *cam);
     vrl_Camera *vrl_WorldGetCamera(void);

The clearing of the screen prior to each frame, and the use (and colors) of
the horizon, are controlled by the following functions:

     void vrl_WorldSetScreenClear(int n);
     int vrl_WorldGetScreenClear(void);
     void vrl_WorldToggleScreenClear(void);

     void vrl_WorldSetHorizon(int n);
     int vrl_WorldGetHorizon(void);
     void vrl_WorldToggleHorizon(void);

     void vrl_WorldSetGroundColor(int color);
     int vrl_WorldGetGroundColor(void);

     void vrl_WorldSetSkyColor(int color);
     int vrl_WorldGetSkyColor(void);

The rate at which the user moves and turns is controlled by the "turn" step
and the "move" step.  In addition, the movement "mode" can be set to 0 or
1; if it's 1 (the default) then simple movement can move the user
vertically, otherwise they stay on the ground.  Note that these are really
only suggestions, and it's up to the application to make use of them.

     void vrl_WorldSetMovementMode(int n);
     int vrl_WorldGetMovementMode(void);
     void vrl_WorldToggleMovementMode(void);

     void vrl_WorldSetMovestep(vrl_Scalar distance);
     vrl_Scalar vrl_WorldGetMovestep(void);

     void vrl_WorldSetTurnstep(vrl_Angle angle);
     vrl_Angle vrl_WorldGetTurnstep(void);

Finally, additional aspects of the virtual world such as the ambient light
level and the "scale factor" (the number of real-world millimeters per unit
of distance in the virtual world) can be set and queried using the following
functions:

     void vrl_WorldSetAmbient(vrl_Factor ambient);
     vrl_Factor vrl_WorldGetAmbient(void);

     void vrl_WorldSetScale(vrl_Scalar scale);
     vrl_Scalar vrl_WorldGetScale(void);

Objects

     Objects are the most important entities in a virtual world.  All
objects have a location and orientation, and they can be attached to each
other in a tree-structured hierarchy.  Each object can have a shape (i.e.
geometric description) and a surface map.  You can create an object
statically (by declaring a variable of type vrl_Object) or dynamically
(either by using malloc() to allocate the space and vrl_ObjectInit() to
initialize it, or by simply calling vrl_ObjectCreate()).  If you use
vrl_ObjectCreate(), you can optionally specify a shape for the object to
use; if you don't want to assign a shape, use NULL.  You can also destroy
objects using vrl_ObjectDestroy.

     vrl_Object *vrl_ObjectInit(vrl_Object *obj);
     vrl_Object *vrl_ObjectCreate(vrl_Shape *shape);
     void vrl_ObjectDestroy(vrl_Object *object);


You can create an exact copy of an object using the function

     vrl_Object *vrl_ObjectCopy(vrl_Object *obj);

Note that the newly-created object will share all the same properties
(including the shape and surface map) as the original and will be in the
exact same location as the original; you should probably move it.  The
copy will have nothing attached to it (it doesn't inherit children from the
original), and will be a sibling of the original (sharing the same parent, if
any).

Objects can be rotated around any of the axes, in any coordinate frame,
using the following function:

     void vrl_ObjectRotate(vrl_Object *obj, vrl_Angle angle, int axis,
           vrl_CoordFrame frame, vrl_Object *relative_to);

The axis is one of the defined constants X, Y or Z.  The frame is one of
the coordinate frames discussed earlier.  If the frame is
VRL_COORD_OBJREL, then the relative_to parameter points to the
object that motion should be relative to.  For example, to rotate an object
45 degrees around the viewer's Z axis, you would make the following call:

     vrl_ObjectRotate(obj, float2angle(45), Z,
           VRL_COORD_OBJREL, vrl_CameraGetObject(vrl_WorldGetCamera()));

Translations of an object are done with the following function:

     void vrl_ObjectTranslate(vrl_Object *obj, vrl_Vector v,
           vrl_CoordFrame frame, vrl_Object *relative_to);

The object is moved along the vrl_Vector v in the specified frame.  The
meaning of the relative_to parameter is the same as it was for
vrl_ObjectRotate().

The vrl_ObjectRotate() and vrl_ObjectTranslate() routines both apply a
rotation to the current state of the object.  If you wish to make the
rotations absolute, call vrl_ObjectRotReset(obj).  If you wish to make
translations absolute, call vrl_ObjectVectorMove(obj, vrl_VectorNULL). 
These should both be done before applying the vrl_ObjectRotate() and
vrl_ObjectTranslate() functions.

to set the translations to zero.

The vrl_ObjectRotate() and vrl_ObjectTranslate() functions should be used
for all object rotation and translation.  Some older functions are also
provided for rotating and moving objects relative to their parent (one of the
more common cases); they are as follows:

     void vrl_ObjectMove(vrl_Object *obj, vrl_Scalar x, vrl_Scalar y,
vrl_Scalar z);
     void vrl_ObjectRelMove(vrl_Object *obj, vrl_Scalar x, vrl_Scalar y,
vrl_Scalar z);
     void vrl_ObjectRotX(vrl_Object *obj, vrl_Angle angle);
     void vrl_ObjectRotY(vrl_Object *obj, vrl_Angle angle);
     void vrl_ObjectRotZ(vrl_Object *obj, vrl_Angle angle);
     void vrl_ObjectRotVector(vrl_Object *obj, vrl_Angle angle, vrl_Vector
vector);
     void vrl_ObjectVectorMove(vrl_Object *obj, vrl_Vector v);
     void vrl_ObjectVectorRelMove(vrl_Object *obj, vrl_Vector v);

An object's current location in the world coordinate frame can be obtained
in two ways, either component-by-component for each of X, Y and Z, or
copied into a vector:

     vrl_Scalar vrl_ObjectGetX(vrl_Object *object);
     vrl_Scalar vrl_ObjectGetY(vrl_Object *object);
     vrl_Scalar vrl_ObjectGetZ(vrl_Object *object);
     void vrl_ObjectGetLocation(vrl_Object *object, vrl_Vector v);

The current world-space orientation of an object's "forward", "up" and
"right" vectors can be obtained using the following routines:

     void vrl_ObjectGetForwardVector(vrl_Object *object, vrl_Vector v);
     void vrl_ObjectGetRightVector(vrl_Object *object, vrl_Vector v);
     void vrl_ObjectGetUpVector(vrl_Object *object, vrl_Vector v);

The vectors filled in by these routines will all be normalized.

An object can be attached to another object, or detached from whatever
object it is currently attached to:

     vrl_Object *vrl_ObjectAttach(vrl_Object *obj, vrl_Object *newparent);
     vrl_Object *vrl_ObjectDetach(vrl_Object *obj);

Note that movement and rotation in the VRL_COORD_PARENT system
(including that performed using vrl_ObjectRotX() and other similar
functions) is carried out relative to the object's parent.  In other words, if
the object is attached to another object, its location and orientation will
depend on that of its parent; if the parent moves, the child will move with
it.  However, if the child moves the parent will stay where it is.

You can find the "root" of an object tree using the following function:

     vrl_Object *vrl_ObjectFindRoot(vrl_Object *obj);

You can walk an entire object tree, executing a function on each node of
the tree, using the following routine:

     void vrl_ObjectTraverse(vrl_Object *object, int (*function)(vrl_Object
*obj));

The function is called once for each object in the hierarchy, and is given a
pointer to the object it's being called on; if the function returns a non-zero
value at any point, the tree is not processed any further.  All parent objects
are processed before their descendants.

The distance between two objects can be found using

     vrl_Scalar vrl_ObjectComputeDistance(vrl_Object *obj1, vrl_Object
*obj2);

The shape and surface map of an object can be altered at any time, and as
often as needed, using the following routines:

     void vrl_ObjectSetShape(vrl_Object *object, vrl_Shape *shape);
     vrl_Shape *vrl_ObjectGetShape(vrl_Object *object);

     void vrl_ObjectSetSurfacemap(vrl_Object *object, vrl_Surfacemap *map);
     vrl_Surfacemap *vrl_ObjectGetSurfacemap(vrl_Object *object);

Objects can be flagged as invisible (in which case they're not drawn) or
highlighted (in which case they're drawn with a bright outline).  They can
also have a "layer" property, and individual layers can be made visible or
invisible, as described later in the section on Layers.  Note that layer zero
is always visible; in effect, an object whose layer is zero will appear on all
layers.  The following routines are used to set, query and toggle those
values:

     void vrl_ObjectSetVisibility(vrl_Object *object, int vis);
     int vrl_ObjectGetVisibility(vrl_Object *object);
     void vrl_ObjectToggleVisibility(vrl_Object *object);

     void vrl_ObjectSetHighlight(vrl_Object *object, highlight);
     int vrl_ObjectGetHighlight(vrl_Object *object);
     void vrl_ObjectToggleHighlight(vrl_Object *object);

     void vrl_ObjectSetLayer(vrl_Object *object, int layer);
     int vrl_ObjectGetLayer(vrl_Object *object);

AVRIL will normally select a level of detail for an object automatically;
however, you can override this mechanism on an object-by-object basis
using two routines to set and get the current "forced" rep for an object:

     void vrl_ObjectSetRep(vrl_Object *object, vrl_Rep *rep);
     vrl_Rep *vrl_ObjectGetRep(vrl_Object *object);

If you want automatic representation selection to be re-enabled for the
object, just use vrl_ObjectSetRep(obj, NULL).

Whenever an object moves, all the objects "descended" from that object
must be updated.  The following function will update the object and all its
descendants; you should generally only call this once per frame, on the
object tree for the current world:

     vrl_Object *vrl_ObjectUpdate(vrl_Object *object);

The macro vrl_WorldUpdate() can be used to do this more concisely.

     A vrl_Object can have several other properties associated with it. 
These include a name, a function, and some application-specific data.  The
function associated with an object gets called whenever the object is
processed during the tree-walking that vrl_ObjectUpdate() performs.  The
following routines allow you to get and set these additional properties:

     void vrl_ObjectSetName(vrl_Object *obj, char *str);
     char *vrl_ObjectGetName(vrl_Object *obj);
     void vrl_ObjectSetFunction(vrl_Object *obj, int (*fn)(vrl_Object *));
     void *vrl_ObjectGetFunction(vrl_Object *obj);
     void vrl_ObjectSetApplicationData(vrl_Object *obj, void *data);
     void *vrl_ObjectGetApplicationData(vrl_Object *obj);

Shapes

     As described earlier, AVRIL keeps shape information separate from
object descriptions, so that shapes can be re-used by multiple objects. 
Shapes (entities of type vrl_Shape) are generally read from PLG files using
the vrl_ReadPLG() function, described later.  You can also create them
using the vrl_Primitive family of functions, also described later in this
document.

     You can modify a shape after it's been loaded; bear in mind that
any changes you make to a shape will affect all objects using that shape! 
To re-scale a shape, or shift all the vertices in the shape relative to the
shape's origin point, use the following functions:

     void vrl_ShapeRescale(vrl_Shape *shape, float sx, float sy, float sz);
     void vrl_ShapeOffset(vrl_Shape *shape, vrl_Scalar tx, vrl_Scalar ty,
vrl_Scalar tz);

After making changes to a shape (or any representation within a shape),
you should call vrl_ShapeUpdate() to recompute the shape's bounds.

     void vrl_ShapeUpdate(vrl_Shape *shape);

Shapes can have a default surface map, which is used for objects that don't
set one of their own.  A pointer to a shape's default surface map can be
obtained, or a pointer to a new surfacemap for a shape set, by calling the
functions

     vrl_Surfacemap *vrl_ShapeGetSurfacemap(vrl_Shape *shape);
     void vrl_ShapeSetSurfacemap(vrl_Shape *shape, vrl_Surfacemap *map);

To get a pointer to the representation of a shape that will be used at a
given on-screen size, use following function:

     vrl_Rep *vrl_ShapeGetRep(vrl_Shape *shape, vrl_Scalar size);

To add an additional representation to an existing shape, use the function

     void vrl_ShapeAddRep(vrl_Shape *shape, vrl_Rep *rep, vrl_Scalar size);

The size parameter gives the apparent on-screen size in pixels at which the
shape should be used.  You can find out how many representations a shape
has using the function

     int vrl_ShapeCountReps(vrl_Shape *shape);

A shape's name can be set or obtained using the functions

     void vrl_ShapeSetName(vrl_Shape *shape, char *str);
     char *vrl_ShapeGetNext(vrl_Shape *shape);

Shapes are kept internally in a singly-linked list; if you need to iterate
through the list, the following two functions can be used

     vrl_Shape *vrl_ShapeGetList(void);
     vrl_Shape *vrl_ShapeGetNext(vrl_Shape *shape)

You can also locate a shape by name using

     vrl_Shape *vrl_ShapeFind(char *name);

Representations

     A shape can have any number of representations, at various levels
of detail.  Each representation (vrl_Rep) has a set of vertices (each of type
vrl_Vector) and a set of facets (each of type vrl_Facet).  A representation
can also have a "sorting type" field; this will be explained in more detail in
future releases of this documentation.

     You can traverse the list of representations for a shape, calling a
function on each representation, by using the following routine:

     void vrl_ShapeTraverseReps(vrl_Shape *shape, int (*function(vrl_Rep
*rep)));

The function is once called for every representation, and is given the
representation as a parameter.  If the function returns a non-zero value, the
processing of the representation list stops at that rep.

     The other approach is to iterate through the linked list of
representations using the functions

     vrl_Rep *vrl_ShapeGetFirstRep(vrl_Shape *shape);
     vrl_Rep *vrl_RepGetNext(vrl_Rep *rep);

You can set and get a vrl_Rep's sorting type, find out the approximate size
(in pixels) at which a rep becomes effective, as well as count the number
of vertices and facets in a rep using the following functions:

     void vrl_RepSetSorting(vrl_Rep *rep, int type);
     int vrl_RepGetSorting(vrl_Rep *rep);

     int vrl_RepGetSize(vrl_Rep *rep);

     int vrl_RepCountVertices(vrl_Rep *rep);
     int vrl_RepCountFacets(vrl_Rep *rep);

There are also "traversal" functions for vertices and facets:

     void vrl_RepTraverseVertices(vrl_Rep *rep, int (*function)(vrl_Vector
*vertex));
     void vrl_RepTraverseFacets(vrl_Rep *rep, int (*function)(vrl_Facet
*facet));

If you need to get or set the values of a vertex's coordinates, you can use
the functions

     void vrl_RepGetVertex(vrl_Rep *rep, int n, vrl_Vector v);
     void vrl_RepSetVertex(vrl_Rep *rep, int n, vrl_Vector v);

Be careful when using vrl_RepSetVertex(); it's easy to move a vertex and
create non-planar or non-convex facets, which confuse the renderer.  You
can only move vertices safely if you know the vrl_Rep is composed
entirely of triangles, since by their nature triangles are always planar and
convex.  In any case, be sure to call vrl_ShapeUpdate() after moving any
vertices.

Facets

     AVRIL's terminology is slightly different from some other VR
systems; a "facet" is a flat three-dimensional entity, whereas a "polygon" is
a two dimensional area on the screen.  The job of the graphics pipeline is
to turn facets into polygons.

     Facets in AVRIL have an array of integers that specify which
vertices in the representation should be connected (and in what sequence)
to form the outline of the facet.  Facets also have an index into the surface
map for an object, to determine what the surface properties of the facet
should be.  They also have a bit that indicates whether or not the facet
should be highlighted.

     The surface index of any facet can be set or queried at any time
using the following two routines:

     void vrl_FacetSetSurfnum(vrl_Facet *facet, int n);
     int void vrl_FacetGetSurfnum(vrl_Facet *facet);

The highlighting of the facet can be set, queried or toggled using the
following routines:

     void vrl_FacetSetHighlight(vrl_Facet *facet, int high);
     int vrl_FacetGetHighlight(vrl_Facet *facet);
     void vrl_FacetToggleHighlight(vrl_Facet *facet);

The number of points in the facet, the index of any particular point, or a
pointer to the vertex for a particular point, can all be obtained using these
routines:

     int vrl_FacetCountPoints(vrl_Facet *facet);
     int vrl_FacetGetPoint(vrl_Facet *facet, int n);
     vrl_Vector *vrl_FacetGetVertex(vrl_Rep *rep, vrl_Facet *facet, int n);


vrl_Facets can be identified by an ID number.  The ID number for a
vrl_Facet can be set and read, and a vrl_Facet with a particular ID can be
found, using the following functions:

     void vrl_FacetSetId(vrl_Facet *facet, unsigned int n);
     unsigned int vrl_FacetGetId(vrl_Facet *facet);
     vrl_Facet *vrl_RepFindFacet(vrl_Rep *rep, unsigned int id);

Surfaces

     AVRIL surfaces are designed for expandability.  At the moment,
each vrl_Surface consists of a type, a hue and a brightness.  The types are
SURF_SIMPLE (no lighting, just a fixed color), SURF_FLAT (for flat
shading), SURF_METAL (for a pseudo-metallic effect) and SURF_GLASS
(for a partially transparent effect).  Surfaces are initialized and modified
using the following routines:

     vrl_Surface *vrl_SurfaceInit(vrl_Surface *surf, unsigned char hue);
     vrl_Surface *vrl_SurfaceCreate(unsigned char hue);

     void vrl_SurfaceSetType(vrl_Surface *surf, vrl_LightingType type);
     vrl_LightingType vrl_SurfaceGetType(vrl_Surface *surf);

     void vrl_SurfaceSetHue(vrl_Surface *surf, unsigned char h);
     unsigned char vrl_SurfaceGetHue(vrl_Surface *surf);

     void vrl_SurfaceSetBrightness(vrl_Surface *surf, unsigned char b);
     unsigned char vrl_SurfaceGetBrightness(vrl_Surface *surf);

The hue and brightness values are 8-bit unsigned quantities; the maximum
brightness value is therefore 255.

     For backwards compatibility with REND386, AVRIL includes
functions to convert a 16-bit REND386 surface descriptor into a
vrl_Surface, and vice-versa:

     vrl_Surface *vrl_SurfaceFromDesc(unsigned int desc, vrl_Surface
*surf);
     unsigned int vrl_SurfaceToDesc(vrl_Surface *surf);

Surface Maps

     Surface maps contain an array of pointers to surfaces; you can
create a surface map with room for a particular number of entries, and
access entries within a map, using the following routines:

     vrl_Surfacemap *vrl_SurfacemapCreate(int n);
     vrl_Surface *vrl_SurfacemapGetSurface(vrl_Surfacemap *map, int
surfnum);
     vrl_Surface *vrl_SurfacemapSetSurface(vrl_Surfacemap *map, int
surfnum,
           vrl_Surface *surface);

Lights

     Lights in AVRIL have a number of properties; they can be on or
off, they can have an intensity, and they can be associated with an object. 
The on/off and intensity properties are similar to a household dimmer;
rotating the knob on a dimmer alters the intensity, and pushing it in
toggles the light on and off.

     The current version of AVRIL only supports ambient lights and
directional lights; point sources will be supported soon.  Any light that is
not associated with an object is considered ambient; this is in addition to
the overall ambient light level for the world.  A directional light uses the
orientation of the object it's associated with to determine which direction
the light should come from.  A point source light (once implemented) will
use the location of the object it's associated with to determine where the
light comes from.

     As with worlds and objects, lights can be statically or dynamically
created and destroyed using the following functions:

     vrl_Light *vrl_LightInit(vrl_Light *light);
     vrl_Light *vrl_LightCreate(void);
     void vrl_LightDestroy(vrl_Light *light);

The light's type value can be one of LIGHT_AMBIENT,
LIGHT_DIRECTIONAL or LIGHT_POINTSOURCE, and is set and
queried using the following two functions:

     void vrl_LightSetType(vrl_Light *light, int type);
     int vrl_LightGetType(vrl_Light *light);

The light's on/off status can be checked and altered, and the intensity set
and queried, using these functions:

     void vrl_LightOn(vrl_Light *light);
     void vrl_LightOff(vrl_Light *light);
     void vrl_LightToggle(vrl_Light *light);
     vrl_Boolean vrl_LightIsOn(vrl_Light *light);

     void vrl_LightSetIntensity(vrl_Light *light, vrl_Factor inten);
     vrl_Factor vrl_LightGetIntensity(vrl_Light *light);

Notice that the intensity values are vrl_Factors; they should never be less
than zero or greater than VRL_UNITY.

     You can make and break associations between a light source and an
object, and determine what object a light source is currently associated
with, using the following routines:

     void vrl_LightAssociate(vrl_Light *light, vrl_Object *object);
     void vrl_LightDisAssociate(vrl_Light *light);
     vrl_Object *vrl_LightGetObject(vrl_Light *light);

Many of the routines that were used for objects earlier have counterparts
that are used for light sources; they're implemented as macros that just
perform the operations on the object with which the light source is
associated.

     vrl_LightMove(light, x, y, z);
     vrl_LightRelMove(light, x, y, z);
     vrl_LightVectorMove(light, v);
     vrl_LightVectorRelMove(light, v);
     vrl_LightRotX(light, angle);
     vrl_LightRotY(light, angle);
     vrl_LightRotZ(light, angle);
     vrl_LightRotVector(light, angle, vector);
     vrl_LightRotReset(light);
     vrl_LightRotate(light, angle, axis, frame, relative_to);
     vrl_LightTranslate(light, v, axis, frame, relative_to);
     vrl_LightAttach(obj, newparent);
     vrl_LightDetach(obj);
     vrl_LightGetLocation(light, v);
     vrl_LightGetX(light);
     vrl_LightGetY(light);
     vrl_LightGetZ(light);

It's important to note the difference between attaching and associating light
sources.  A light source can be associated with an object, which means it
will use that object's location and orientation as its own.  The object with
which the light is associated can be attached to another object, and
"inherit" location and orientation information from it.  The
vrl_LightAttach() and vrl_LightDetach() routines are provided only as a
convenience; what you're really attaching and detaching with those
routines is the object that the light is associated with.  You generally
associate a light source with an object once, and leave it that way; you can
attach or detach the light however you want after that.

     Lights can have other attributes as well, just as objects can;
specifically, they can have a name and some application-specific data,
which are accessed using the following functions:

     void vrl_LightSetName(vrl_Light *light, char *str);
     char *vrl_LightGetName(vrl_Light *light);
     void vrl_LightSetApplicationData(vrl_Light *light, void *data);
     void *vrl_LightGetApplicationData(vrl_Light *light);

Cameras

     AVRIL allows you to have any number of virtual cameras.  Each
camera is associated with an object, much as lights are.  However, unlike
lights, cameras must be associated with an object; there's no such thing as
an "ambient" camera.  Cameras are initialized and destroyed just like
objects or light sources:

     vrl_Camera *vrl_CameraInit(vrl_Camera *camera);
     vrl_Camera *vrl_CameraCreate(void);
     void vrl_CameraDestroy(vrl_Camera *camera);

Cameras have only a few properties that are important; in particular, a
zoom factor, an aspect ratio, and hither and yon clipping plane distances. 
These are all set and queried using the following routines:

     void vrl_CameraSetZoom(vrl_Camera *camera, float zoom);
     float vrl_CameraGetZoom(vrl_Camera *camera);

     void vrl_CameraSetAspect(vrl_Camera *camera, float asp);
     float vrl_CameraGetAspect(vrl_Camera *camera);

     void vrl_CameraSetHither(vrl_Camera *camera, vrl_Scalar hither);
     vrl_Scalar vrl_CameraGetHither(vrl_Camera *camera);

     void vrl_CameraSetYon(vrl_Camera *camera, vrl_Scalar yon);
     vrl_Scalar vrl_CameraGetYon(vrl_Camera *camera);

Notice that the zoom factor and aspect ratio are floats; this may change in
a future release of AVRIL.  The zoom factor works like the zoom on a
camera; the higher the zoom, the more the image is magnified.  The zoom
is the tangent of half the field of view (viewing angle).  The aspect ratio is
the ratio between the horizontal and vertical zoom factors.  The hither
clipping distance is the distance in virtual space from the camera to the
invisible plane at which objects will be "clipped".  The "yon" distance is
like an invisible wall; any object entirely on the far side of the wall will
not be seen.

     The routines for associating a camera with an object and for
determining what object a camera is currently associated with are as
follows:

     void vrl_CameraAssociate(vrl_Camera *camera, vrl_Object *object);
     vrl_Object *vrl_CameraGetObject(vrl_Camera *camera);

There's no vrl_CameraDisAssociate() function, as there was for lights;
cameras must be associated with an object in order to have any meaning.

     Again, routines are provided for manipulating and querying the
location and orientation of a virtual camera; these are macros, just as they
were for lights:

     vrl_CameraMove(camera, x, y, z);
     vrl_CameraRelMove(camera, x, y, z);
     vrl_CameraVectorMove(camera, v);
     vrl_CameraVectorRelMove(camera, v);
     vrl_CameraRotX(camera, angle);
     vrl_CameraRotY(camera, angle);
     vrl_CameraRotZ(camera, angle);
     vrl_CameraRotVector(camera, angle, vector);
     vrl_CameraRotReset(camera);
     vrl_CameraRotate(camera, angle, axis, frame, relative_to);
     vrl_CameraTranslate(camera, v, axis, frame, relative_to);
     vrl_CameraAttach(obj, newparent);
     vrl_CameraDetach(obj);
     vrl_CameraGetX(camera);
     vrl_CameraGetY(camera);
     vrl_CameraGetZ(camera);
     vrl_CameraGetLocation(camera, v);

Camera can have other attributes as well, just as objects and lights can;
specifically, they can have a name and some application-specific data,
which are accessed using the following functions:

     void vrl_CameraSetName(vrl_Camera *camera, char *str);
     char *vrl_CameraGetName(vrl_Camera *camera);
     void vrl_CameraSetApplicationData(vrl_Camera *camera, void *data);
     void *vrl_CameraGetApplicationData(vrl_Camera *camera);

In addition, there are three routines that obtain the current "forward",
"right" and "up" vectors for a camera:

     vrl_CameraGetForwardVector(camera, v)
     vrl_CameraGetRightVector(camera, v)
     vrl_CameraGetUpVector(camera, v)

Layers

     Layers were described earlier, in the section on objects.  The
routines for dealing with layers are as follows:

     void vrl_LayerOn(int n);
     void vrl_LayerOff(int n);
     void vrl_LayerToggle(int n);
     int vrl_LayerIsOn(int n);
     void vrl_LayerAllOn(void);

The last routine, vrl_LayerAllOn(), turns all the layers on at once; this is
the default condition.

File I/O Routines

     AVRIL supports the PLG file format, the FIG file format, and most
of the WLD file format; these formats are described in the Appendices. 
The library contains routines for reading each of those formats:

     vrl_Shape *vrl_ReadPLG(FILE *in);
     vrl_Object *vrl_ReadObjectPLG(FILE *in);
     int vrl_ReadWLD(FILE *in);
     vrl_Object *vrl_ReadFIG(FILE *in, vrl_Object *parent, char *rootname);

The vrl_ReadPLG() routine reads a shape from the specified file and
returns a pointer to it.  The vrl_ReadObjectPLG() routine is similar, but it
also allocates an object and assigns the shape to it.

     The vrl_ReadWLD() function reads a world description from the
file into the current world; you can make as many calls to this routine as
you like, combining a number of WLD files.  The vrl_ReadFIG() routine
lets you specify a "parent" object to which the newly-read object tree
should be attached, as well as the name of the root object.  Any segnames
that are assigned in the FIG file will be added to the current world's list of
objects as rootname.segname.

     While loading a PLG file, a scale factor and offset can be applied. 
The vertices read from the file are multiplied by the scaling factors, and
then the offsets are added to them.  The scale factors and offsets are set
using:

     void vrl_SetReadPLGscale(float x, float y, float z);
     void vrl_SetReadPLGoffset(float x, float y, float z);

FIG files can also have a scale factor applied to them.  In addition, parts of
a figure that have a "segnum" value set can have pointers to their objects
placed into a parts array specified by the user:

     void vrl_SetReadFIGscale(float x, float y, float z);
     void vrl_SetReadFIGpartArray(vrl_Object **ptr, int maxparts);

If the ptr is not NULL, then any parts in the FIG file that have a segnum
will create an entry in the array, indexed by the segnum value.  The
maxparts value is the number of elements the caller has provided space for
in the array.

     There are several other routines that support file operations.  Two
routines maintain a kind of "current directory" for file loading; they are

     void vrl_FileSetLoadpath(char *path);
     char *vrl_FileFixupFilename(char *fname);

The first sets the given path (if not NULL) to be the directory that
subsequent filename fixups should use.  The second routine is used to
generate a full filename, with the current loadpath prepended.  Note that
filenames beginning with '/' or '\' are not modified.  Also note that
vrl_FileFixupFilename() returns a pointer to an internal buffer, which will
be rewritten on the next call to vrl_FileFixupFilename().  If you really
need to keep the fixed-up filename around, you should strcpy() it to
another buffer or strdup() it.

System and Application routines

     These routines were described in detail in the Introduction, but
here's a quick summary:

     vrl_Boolean vrl_SystemStartup(void);
     void vrl_SystemRun(void);
     vrl_RenderStatus *vrl_SystemRender(vrl_Object *list);
     vrl_Time vrl_SystemGetRenderTime(void);
     vrl_Time vrl_SystemGetFrameRate(void);
     void vrl_SystemCommandLine(int argc, char *argv[]);

     void vrl_SystemRequestRefresh(void);
     vrl_Boolean vrl_SystemQueryRefresh(void);

     void vrl_SystemStartRunning(void);
     void vrl_SystemStopRunning(void);
     vrl_Boolean vrl_SystemIsRunning(void);

     void vrl_ApplicationDrawUnder(void);
     void vrl_ApplicationDrawOver(vrl_RenderStatus *stat);
     void vrl_ApplicationInit(void);
     void vrl_ApplicationKey(unsigned int c);
     void vrl_ApplicationMouseUp(int x, int y, unsigned int buttons);

These are not really part of AVRIL's "guts", since you don't need to use
anything in system.c (which is the only module that knows about the
vrl_Application functions).

User Interface

     The current version of AVRIL has a few primitive user interface
routines for you to use.  A better user interface needs to be designed; in
the meantime, here are the routines:

     void vrl_UserInterfaceBox(int width, int height, int *x, int *y);
     void vrl_UserInterfacePopMsg(char *msg);
     void vrl_UserInterfacePopText(char *text[]);
     int vrl_UserInterfaceDismiss(void);
     int vrl_UserInterfacePopMenu(char *text[]);
     unsigned vrl_UserInterfacePopPrompt(char *prompt, char *buff, int n);

The vrl_UserInterfaceBox() routine puts up a nice bordered box, centered
on the screen.  The width and height determine the size of the box.  When
the routine returns, x and y will contain the screen coordinates of the top-
left corner of the box.

     The vrl_UserInterfacePopMsg() routine displays a one-line text
message.  The vrl_UserInterfacePopText() routine puts up a multi-line
message; the array of string pointers has to have a NULL pointer entry at
the end.

     The vrl_UserInterfaceDismiss() routine is useful after you've called
either vrl_UserInteracePopMsg() or vrl_UserInterfacePopText(); it waits for
the user to press a key or click the mouse.

     The vrl_UserInterfacePopMenu() routine displays a menu and waits
for the user to select an item.  If the user clicks on an item with the
mouse, the index of that item will be returned as the value of the function. 
If the user presses a key, the menu is searched item by item until one is
found that has an uppercase letter matching the key the user entered; the
index of that entry is returned.  If the user clicks outside the menu, or
presses ESC, the value -1 is returned.

     The vrl_UserInterfacePopPrompt() box displays a prompt to the
user and lets them enter a text response.  The backspace key is supported. 
The user can end their input using either ENTER or ESC; the key they
press to end their input is returned as the value of the function.

     There are two other routines which are not really part of the user
interface; they're used to overlay text on the screen or display the
compass.  They're typically called from vrl_ApplicationDrawOver().

     void vrl_UserInterfaceDrawCompass(vrl_Camera *camera, int x, int y,
int armlen);
     void vrl_UserInterfaceDropText(int x, int y, int color, char *text);

In vrl_UserInterfaceDrawCompass(), the x and y values are the location of
the "origin" of the compass and armlen is the length of each arm.  (The
arms will of course seem shorter because of perspective).  The x, y and
armlen values are in screen coordinates (i.e., pixels).  The camera is used
to obtain orientation information about the user's viewpoint.

     The vrl_UserInterfaceDropText() routine displays the text message
at the given screen coordinates in the given color, with a black (i.e., color
0) drop shadow.

Tasks

     The pseudo-tasking mechanism was described back in the
Introduction section.  Tasks are added using vrl_TaskCreate(), which takes
a pointer to the function, a pointer to the data, and the period.  The tasks
should be run periodically by calling vrl_TaskRun(), which is normally
done in vrl_SystemRun().  The tasks can obtain a pointer to their data by
calling vrl_TaskGetData(), the elapsed time since they last ran by calling
vrl_TaskGetElapsed(), and the current time by calling
vrl_TaskGetTimeNow().  Note that for any given call to vrl_TaskRun(), all
the tasks will receive the same value from vrl_TaskGetTimeNow(); this is
different from vrl_TimerRead(), since the timer runs independently of the
tasks.  You may want to use one or the other of those two functions
depending on the circumstances.

     vrl_Boolean vrl_TaskCreate(void (*function)(void), void *data,
vrl_Time period);
     void vrl_TaskRun(void);
     void *vrl_TaskGetData(void);
     vrl_Time vrl_TaskGetElapsed(void);
     vrl_Time vrl_TaskGetTimeNow(void);

Primitives

     AVRIL currently provides five utility routines for creating simple
geometric primitives.  Each takes a surface map pointer; if the value is
NULL, the default color for geometric primitives is used.

     vrl_Shape *vrl_PrimitiveBox(vrl_Scalar width, vrl_Scalar height,
vrl_Scalar depth,
           vrl_Surfacemap *map);

     vrl_Shape *vrl_PrimitiveCone(vrl_Scalar radius, vrl_Scalar height, int
nsides,
           vrl_Surfacemap *map);

     vrl_Shape *vrl_PrimitiveCylinder(vrl_Scalar bottom_radius, vrl_Scalar
top_radius,
           vrl_Scalar height, int nsides, vrl_Surfacemap *map);

     vrl_Shape *vrl_PrimitivePrism(vrl_Scalar width, vrl_Scalar height,
vrl_Scalar depth,
           vrl_Surfacemap *map);

     vrl_Shape *vrl_PrimitiveSphere(vrl_Scalar radius, int vsides, int
hsides,
           vrl_Surfacemap *map);

The box and sphere have their origin at their geometric centers, the cone
and the cylinder have their origin at the center of their bases, and the
prism has its origin at one corner.  You can use vrl_ShapeOffset() to
change these choices if you wish.

Rendering

     The rendering "engine" needs to be initialized before any actual
rendering is done.  The renderer needs to know how much memory to
allocate for itself, as well as the maximum number of objects, facets,
vertices and lights it will have to contend with.  When the program is
ready to exit, vrl_RenderQuit() should be called to cleanly shut down the
engine.

     vrl_Boolean vrl_RenderInit(int maxvert, int maxf, int maxobjs, int
maxlights,
           unsigned int mempoolsize);
     void vrl_RenderQuit(void);

The routines in system.c normally handle the calling of vrl_RenderInit(),
and the setting up of an atexit() function for vrl_RenderQuit().

Two functions are used to give the renderer a pointer to the current camera
and list of lights, and to set the current ambient lighting level (usually that
for the current world):

     void vrl_RenderBegin(vrl_Camera *camera, vrl_Light *lights);
     void vrl_RenderSetAmbient(vrl_Factor amb);

Finally, two functions do the actual drawing; one draws a horizon, the
other renders a list of objects (such as that returned by vrl_ObjectUpdate()
or vrl_WorldUpdate()).

     void vrl_RenderHorizon(void);
     vrl_Status *vrl_RenderObjlist(vrl_Object *objects);

The vrl_RenderObjlist() function returns a pointer to a status struct, which
was described in the Introduction section.  If the list of objects is NULL,
then the last non-NULL list of objects that was passed is used

     There are two functions that allow you to monitor a particular point
on the screen, and then see what objects and facets were under the cursor
(and nearest the viewer).

     void vrl_RenderMonitorInit(int x, int y);
     vrl_Boolean vrl_RenderMonitorRead(vrl_Object **obj, vrl_Facet **facet,
int *vertnum);

The x and y values are coordinates in the current screen window (such as
those passed to vrl_ApplicationMouseUp()).  The obj pointer (if not
NULL) gets set to point to the object the cursor was over; similarly, the
facet pointer (if not NULL) gets set to point to the facet the cursor was
over.  The vertnum pointer is not currently used.  If nothing was under the
cursor, vrl_RenderMonitorRead() returns zero.  Remember that you must
call vrl_RenderObjlist() between the call to vrl_RenderMonitorInit() and
vrl_RenderMonitorRead(); typically, you would call it as
vrl_RenderObjlist(NULL) to just re-render the last object list that was
used.

The Keyboard, the Mouse and the Timer

     AVRIL has a set of routines which deal with the keyboard, mouse
and timer; these will vary from one platform to another, but they should all
provide the same high-level interface to application software.

     vrl_Boolean vrl_TimerInit(void);
     void vrl_TimerQuit(void);
     vrl_Time vrl_TimerRead(void);
     vrl_Time vrl_TimerGetTickRate(void);
     void vrl_TimerDelay(vrl_Time milliseconds);

These routines let you initialize, de-initialize and read the timer.  The
vrl_TimerGetTickRate() routine returns the number of ticks per second that
the timer runs at.  The higher this number is, the more accurate the
frames/second calculations will be (among other things).  It is expected
that all future versions of AVRIL will use 1000 ticks per second, so that
each tick is one millisecond.

     vrl_Boolean vrl_MouseInit(void);
     void vrl_MouseQuit(void);
     vrl_Boolean vrl_MouseReset(void);
     vrl_Boolean vrl_MouseRead(int *x, int *y, unsigned int *buttons);
     void vrl_MouseCursorHide(void);
     void vrl_MouseCursorShow(void);
     void vrl_MouseGetLimits(int *min_x, int *max_x, int *min_y, int
*max_y);
     void vrl_MouseSetUsage(int u);
     int vrl_MouseGetUsage(void);
     void vrl_MouseSetPointer(void *u);
     void *vrl_MouseGetPointer(void);

These routines let you initialize and read the mouse.  Whenever you write
to the currently visible page (the viewpage) you should first call
vrl_MouseCursorHide() to prevent the mouse from being "squashed" under
a falling polygon.  When you're finished updating the display, call
vrl_MouseCursorShow() to let the rodent run free again.  The
vrl_SystemRender() routine, found in system.c, shows how these routines
are used.  The user interface routines do the calls to
vrl_MouseCursorHide() and vrl_MouseCursorShow(), so you don't need to
worry about them when you use those functions.  The
vrl_MouseGetLimits() function lets you find out the current range of
possible mouse X and Y values.

     Since the mouse can be used either as a screen-oriented pointer or
as a 6D input device, there has to be some way of toggling between those
two functions.  That's what the vrl_MouseSetUsage() and
vrl_MouseGetUsage() functions are for; a non-zero value means the mouse
is a 6D device, a zero value means it's a screen pointer.

     When the mouse is a 6D device, it's useful to be able to obtain a
pointer to the vrl_Device which is using it; similarly, the vrl_Device
function (described later) must be able to set that pointer.  That's what the
vrl_MouseSetPointer() and vrl_MouseGetPointer() calls do; they set and
get a pointer to the vrl_Device that's using the mouse for 6D input.

     vrl_Boolean vrl_KeyboardCheck(void);
     unsigned int vrl_KeyboardRead(void);

The vrl_KeyboardCheck() routine returns non-zero if a key has been
pressed, and vrl_KeyboardRead() returns the actual key.  Most keys just
return their ASCII values; see the file avrilkey.h for definitions of special
keys (like arrows, function keys, etc).

Display Routines

     The interface to the display routines is in a state of flux; the 2.00
release of this document will provide the details, and a future appendix
will explain how to write your own display routines.

PCX File Routines

There are two functions that deal with files in PCX format:

     vrl_Boolean vrl_ReadPCX(FILE *in);
     vrl_Boolean vrl_WritePCX(FILE *out);

The first one reads a PCX file into the current drawing page, the other
writes the current drawing page out to disk as a PCX file.

Devices

AVRIL has support for input devices providing multiple "degrees of
freedom" (DOF).  In fact, AVRIL's devices are even more general; each
device can have an arbitrary number of input and output channels.

     To use a device in AVRIL, you must first "open" it; this is
analogous to opening a file.  When you're finished with the device, you
should "close" it; you can close all open devices using
vrl_DeviceCloseAll(), which is set as an atexit() function by
vrl_SystemStartup().

     vrl_Device *vrl_DeviceOpen(vrl_DeviceDriverFunction fn, vrl_SerialPort
*port);
     void vrl_DeviceClose(vrl_Device *device);
     void vrl_DeviceCloseAll(void);

Most input devices communicate over a serial port; the port parameter is a
pointer to such a port that's been opened with vrl_SerialOpen().  See the
section on Serial Ports for more details.  Devices (such as the keyboard)
that don't use a serial port should pass NULL as the port parameter.  The
fn parameter is a function that operates the device; there are a number of
these already defined for popular devices, and it's easy to write your own
if you have unusual devices you wish to support.  They are listed in
avrildrv.h, and in the cfg.c file.

     Once a device has been opened, you can easily find out how many
input channels it has, and how many two-state buttons are on the device,
using the following two functions:

     int vrl_DeviceGetNchannels(vrl_Device *device);
     int vrl_DeviceGetNButtons(vrl_Device *device);

Sometimes devices can get into a strange state, or drift from their initial
settings; the function vrl_DeviceReset() resets a device to the state it was
in just after it was opened.

     int vrl_DeviceReset(vrl_Device *device);

Devices should be periodically "polled" to see if they have anything to
report; this is normally done in the vrl_SystemRun() function.  An
individual device can be polled using vrl_DevicePoll(), which returns a
non-zero value if new data was acquired.  All the devices can be polled by
calling vrl_DevicePollAll().

     int vrl_DevicePoll(vrl_Device *device);
     void vrl_DevicePollAll(void);

You can get the current value of a channel's input by calling
vrl_DeviceGetValue(), and you can read the button status on the device
using vrl_DeviceGetButtons():

     vrl_Scalar vrl_DeviceGetValue(vrl_Device *device, int channel);
     unsigned long vrl_DeviceGetButtons(vrl_Device *device);

The buttons are returned as an unsigned long, which is assumed to have at
least 32 bits.  Each bit corresponds to a single button on the device.

     The first six channel numbers are special, since they correspond to
the six basic degrees of freedom a device can have.  The first three, whose
channel numbers are the #defined values X, Y, and Z, provide the three-
dimensional location of the input device in its private coordinate system. 
The next three, whose channel numbers are the #defined values XROT,
YROT and ZROT, provide the rotation of the device around each of the
three axes.  Every device is expected to provide at least those six values;
others, such as glove-like input devices, may have a separate channel for
the flexion of each finger.

     You can determine whether a channel's value has changed since the
previous poll by using vrl_DeviceChannelGetChanged(), and you can use
vrl_DeviceGetChangedButtons() to obtain an unsigned long word whose
bits indicate whether the corresponding buttons have changed state since
the previous poll.

     vrl_Boolean vrl_DeviceGetChanged(vrl_Device *device, int channel);
     unsigned long vrl_DeviceGetChangedButtons(vrl_Device *device);

Note that "previous poll" means the one prior to the most recent one; in
other words, you would check the changed flags immediately after a call to
vrl_DevicePoll() or vrl_DevicePollAll() in order to see if they've changed
since the last time through.

     Some devices (such as the Logitech Cyberman and the Global
Devices Controller) are capable of output as well as input.  You can find
out the number of output channels a device has, and set a particular
channel to a particular value, using the following two functions:

     void vrl_DeviceOutput(vrl_Device *device, int channel, vrl_Scalar
value);
     int vrl_DeviceGetNOutputChannels(vrl_Device *device);

There are some devices that shouldn't be polled too frequently (possibly
because the polling takes a long time).  Devices drivers typically set their
own polling frequency when they're initialized, but you can read and set
the polling period using these two functions:


     void vrl_DeviceSetPeriod(vrl_Device *device, vrl_Time period);
     vrl_Time vrl_DeviceGetPeriod(vrl_Device *device);

Some devices provide fewer than six degrees of freedom; in particular,
some devices (such as sourceless head trackers) provide only rotational
information, while others might provide only positional information.  In
addition, some axes are absolute, while others are relative; for example, a
magnetic tracker provides absolute rotation, whereas a joystick provides a
rate of rotation.  Two functions are used to determine what a device's
suggested modes of operation are for translation and rotation:

     vrl_DeviceMotionMode vrl_DeviceGetRotationMode(vrl_Device *device);
     vrl_DeviceMotionMode vrl_DeviceGetTranslationMode(vrl_Device *device);

Each of these two functions returns one of the values
VRL_MOTION_NONE (i.e., this type of motion is not reported by this
device), VRL_MOTION_RELATIVE (i.e. this device provides relative
information) or VRL_MOTION_ABSOLUTE (this device provides
absolute information).

     It's important to note that these are all suggestions from the device
driver as to how it should be used; you can still choose, in your
application, to make any device either absolute or relative.

     To understand what these next few functions do, it's important to
understand what kind of processing the system does on the values once it
receives them from the device.  Each channel can be in either
"accumulate" or "non-accumulate" mode.  For accumulating devices, the
value is first checked to see how close it is to zero; if it's less than a
channel-specific deadzone value, then the value is considered to be zero. 
For non-accumulating devices, the deadzone value is treated as a minimum
change from the most recently read value for this channel; a device that
moves by less than the deadzone amount between consecutive polls will
not change in value.

     The value is then scaled so that its maximum value is less than a
channel-specific scale value.  For channels with the accumulate flag set,
the value is also scaled by the elapsed time; the scale for such channels is
treated as the maximum rate of change per second.

The scale and deadzone values can be set and read using the following
calls:

     vrl_Scalar vrl_DeviceGetDeadzone(vrl_Device *device, int channel);
     void vrl_DeviceSetDeadzone(vrl_Device *device, int channel, vrl_Scalar
value);
     vrl_Scalar vrl_DeviceGetScale(vrl_Device *device, int channel);
     vrl_DeviceSetScale(vrl_Device *device, int channel, vrl_Scalar value);
     vrl_Boolean vrl_DeviceGetAccumulate(vrl_Device *device, int channel);
     void vrl_DeviceSetAccumulate(vrl_Device *device, int channel,
vrl_Boolean value);

If you like, you can bypass all that processing and obtain the actual, "raw"
value being reported by the device by calling

     vrl_Scalar vrl_DeviceGetRawValue(vrl_Device *device, int channel);

Devices are kept internally in a linked list; if you want to iterate over the
list, you can do it with the following two functions:

     vrl_Device *vrl_DeviceGetFirst(void);
     vrl_Device * vrl_DeviceGetNext(vrl_Device *device);

You can retrieve a human-readable description of a device by calling the
function

     char *vrl_DeviceGetDesc(vrl_Device *device);

You can produce output on any of the device's channels by calling

     void vrl_DeviceOutput(vrl_Device *device, int channel, vrl_Scalar
value);

The channel and the value (which ought to be in the range 0 to 255) are
passed along to the device; if it's capable of outputting that value on that
channel, it does.  Not all devices are capable of producing output, and
those that can have a variety of different ways of doing it (including sound
and vibration).  All you can be sure of is that a value of zero will turn the
output off, and a non-zero value will turn it on.

     Some 2D devices can map their two axes into 6 degrees of freedom
using combinations of buttons.  There are two functions to get and set the
mapping tables they use:

     void vrl_DeviceSetButtonmap(vrl_Device *device, vrl_DeviceButtonmap
*b);
     vrl_DeviceButtonmap *vrl_DeviceGetButtonmap(vrl_Device *device);

Buttonmaps are a fairly complex topic, and are discussed in more detail in
the appendix on writing device drivers.

Serial Ports

Since many input devices use serial communications, AVRIL contains a
library of serial port routines.  These will mostly be of interest to people
writing device drivers, but they might also be used for modem
communications.

     To some extent, serial port support will be platform-dependent; the
meaning of the various parameters in the vrl_SerialOpen() call will be
different on different systems.  However, all the other routines should be
the same regardless of platform.

     A serial port can be opened and closed using vrl_SerialOpen() and
vrl_SerialClose() respectively; all the serial ports can be closed at once
using vrl_SerialCloseAll(), which gets set as an atexit() function by
vrl_SystemStartup().  The communications parameters can be set using
vrl_SerialSetParameters().

     vrl_SerialPort *vrl_SerialOpen(unsigned int address, int irq, unsigned
int buffsize);
     void vrl_SerialClose(vrl_SerialPort *port);
     void vrl_SerialCloseAll(void);
     void vrl_SerialSetParameters(vrl_SerialPort *port, unsigned int baud,
           vrl_ParityType parity, int databits, int stopbits);

The address parameter to vrl_SerialOpen() is interpreted differently on
different platforms; on PC-compatible machines, it's the base address of
the UART chip (usually 0x3F8 for COM1, 0x2F8 for COM2).  The irq
parameter is also system-dependent; on PC-compatible machines, it's the
hardware interrupt level the serial port uses (usually 4 for COM1, 3 for
COM2).

     The buffsize parameter is the size of buffer to use for incoming
data.  If it's set to zero, the port will be in a non-buffered mode; this may
mean that characters get lost.  Such a mode would only be used if you're
doing your own handshake with the device; in other words, you send it a
byte to poll it, and then sit in a tight loop receiving the resulting data.  In
this case, the irq value is ignored.

     The baud parameter to the vrl_SerialSetParameters() function is the
baud rate; this is usually 9600 for most input devices.  The parity
parameter is one of VRL_PARITY_NONE, VRL_PARITY_EVEN or
VRL_PARITY_ODD; most devices use VRL_PARITY_NONE.  The
databits field is the number of data bits per transmitted byte; this is either
7 or 8, and most devices use 8.  The number of stop bits can be 1 or 2,
and is usually 1.  Newly-opened serial ports are set up to be 9600 baud,
VRL_PARITY_NONE, 8 data bits and 1 stop bit.

     The vrl_SerialCheck() routine will return a non-zero value if there
are unread characters in the input buffer (or if there's a character waiting
at the UART if the port is in unbuffered mode).  The vrl_SerialGetc()
routine reads and returns a byte.  It should not be called unless you know
there's a character waiting; in buffered mode, such a call will return zero,
while in unbuffered mode the call will block until a character arrives!  The
vrl_SerialFlush() routine will get rid of any characters waiting in the input
buffer.

     vrl_Boolean vrl_SerialCheck(vrl_SerialPort *port);
     unsigned int vrl_SerialGetc(vrl_SerialPort *port);
     void vrl_SerialFlush(vrl_SerialPort *p);

The vrl_SerialPutc() and vrl_SerialPutString() routines put out single
characters and null-terminated strings of characters respectively.  The
terminating null byte is not send by vrl_SerialPutString().

     void vrl_SerialPutc(unsigned int c, vrl_SerialPort *port);
     void vrl_SerialPutString(unsigned char *s, vrl_SerialPort *p);

There are also two routines for controlling the state of the DTR and RTS
lines:

     void vrl_SerialSetDTR(vrl_SerialPort *port, vrl_Boolean value);
     void vrl_SerialSetRTS(vrl_SerialPort *port, vrl_Boolean value);


Packet Routines

Many serial devices communicate by sending "packets" of data.  There are
four routines in AVRIL to support the reception of packets:

     vrl_DevicePacketBuffer *vrl_DeviceCreatePacketBuffer(int buffsize);
     void vrl_DeviceDestroyPacketBuffer(vrl_DevicePacketBuffer *buff);
     vrl_Boolean vrl_DeviceGetPacket(vrl_SerialPort *port,
vrl_DevicePacketBuffer *buff);
     unsigned char *vrl_DevicePacketGetBuffer(vrl_DevicePacketBuffer
*buff);

The vrl_DeviceCreatePacketBuffer() function creates a packet buffer with
room for the specified number of bytes; vrl_DeviceDestroyPacketBuffer()
destroys such a buffer.  The vrl_DevicePacketGetBuffer() routine returns a
pointer to the actual data packet.

     The vrl_DeviceGetPacket() routine is designed to handle a
particular type of packet, a fixed-size one in which the leading byte has the
top bit set and none of the other bytes do.  This format is used by the
Logitech Cyberman, among other devices.  You can use this routine
directly, or you can write your own (with a different name, of course); the
source code is found in packet.c, and the vrl_DevicePacketBuffer data
structure is in avril.h (and it's not expected to change, unlike many other
internal data structures).  The vrl_DeviceGetPacket() routine returns a non-
zero value if a complete packet has been received (i.e. exactly buffsize
bytes have been received, starting with a byte with the top bit set).

Some Final Notes

     This is the first official release of AVRIL, so the paint may not be
quite dry yet.  If you run into any problems, my email address is
broehl@uwaterloo.ca; be sure to put AVRIL in the subject line so I know
what it's about, otherwise it might take me days to get back to you.  (It
might anyway...)

Support for AVRIL

     There are two electronic mailing lists for discussing AVRIL.  The
first list is called avril-announce, and is used for announcements of new
releases, utilities, applications and so on.  The second list is called avril-
developers, and is used as a way for people who are developing
applications using AVRIL to communicate and exchange ideas.

     To subscribe to either or both lists, send mail to
majordomo@sunee.uwaterloo.ca with the following line(s) in the body of
the message:

     subscribe avril-announce YourName
     subscribe avril-developers YourName

To unsubscribe from either or both lists, do the exact same thing but with
the word "unsubscribe" instead of the word "subscribe".

Future Features

     Features that will be added in future releases include routines for
handling stereoscopic viewing, sound, networking, and an application
language of some sort.

     The latest release of AVRIL can always be found on
sunee.uwaterloo.ca, in the pub/avril directory.  I will also try to put the
latest version on major sites such as wuarchive.wustl.edu, oak.oakland.edu,
x2ftp.oulu.fi and possibly others.  Please feel free to make it available to
everyone; the only restrictions are that you can't sell it (since it's free!)
and you can't develop commercial applications without properly licensing
it.

     There should be a new release of AVRIL every few months;
starting with version 2.00, AVRIL should be ported to several other
platforms.

     In the meantime, I hope you enjoy using AVRIL.

Appendix A - REVISION HISTORY

This is the first major release of AVRIL.  There have been several changes
since the pre-release version 0.9c; they are listed below.  It's not
anticipated that the kind of sweeping changes described here will happen
in future releases of AVRIL.

Changes since version 0.9c:

Added support for multi-channel input devices.

Added support for serial communications.

Renamed all the types to begin with the "vrl_" prefix; the types affected
are Scalar, Factor, Angle, Vector and Matrix.  Also changed UNITY to
VRL_UNITY.  There are #defines at the end of avril.h to ease the
transition; they'll be removed for version 2.00.

Added vrl_Boolean and vrl_Time types.

Modified avril.h to #include <stdio.h> and #include <mem.h> (for
memcpy()).

Added #defines for XROT, YROT and ZROT for indexing device
channels.

Added various additional vector and matrix functions such as
vrl_VectorNegate() and vrl_VectorEqual().  Also added a new global
variable, the null vector vrl_VectorNULL.

Added a "leftside" parameter to the vrl_MatrixRotX(), vrl_MatrixRotY(),
vrl_MatrixRotZ(), and vrl_MatrixRotVector() functions.

Removed the vrl_List structure and put names into the structs for
vrl_Lights, vrl_Cameras, and vrl_Objects.  Added functions for accessing
those names, and for finding entities based on their name.  Also added
routines for traversing and iterating over the linked lists of vrl_Lights and
vrl_Cameras.

Added vrl_ObjectRotate() and vrl_ObjectTranslate() routines, and modified
several older movement and rotation functions to make calls to those two.

Made surface maps into a struct, rather than just an array of vrl_Surface
pointers.  This allows for additional information about a surface map to be
kept, and for the surface maps to be kept in a linked list.

Added the notions of a world having "bounds" and a "radius", and routines
for supporting those notions.

The RVD drivers are now searched for along the PATH, as well as in the
current directory.

Renamed all the vrl_New* functions to make their naming consistent with
everything else.  For example, vrl_NewObject() is now vrl_ObjectCreate(). 
Again, #defines were added to the end of avril.h to ease the transition;
these will be removed as of the 2.00 release.

Added a vrl_WorldUpdate() macro.

Added application-specific data to vrl_Objects, vrl_Lights and
vrl_Cameras.

Added functions to vrl_Objects, which get called during world updating.

Added ID numbers to facets.

Renamed vrl_SetFigurePartArray() to vrl_SetReadFIGpartArray() to be
more consistent.

Switched to using standard the DOS timer (at an accelerated rate, and
chaining to the old one periodically) because the RTC timer interfered with
Turbo Debugger.

Standardized on a tick rate of 1000 per second.

Eliminated the vrl_TimerAddHandler() routine, since there was too much
risk that handler routines would do things that ought not be done inside an
interrupt handler; a bug in Borland C 3.1 bug also forced all routines
called from within an interrupt to be assembled without 386 instructions.

Added a call to directly query the frames per second rate.

Renamed the vrl_DrawCompass() and vrl_DropText() routines to
vrl_UserInterfaceDrawCompass() and vrl_UserInterfaceDropText() to make
it clear that they're a part of the user interface family of functions.

Modified the vrl_PrimitiveSphere() routine to accept separate counts of the
number of longitudinal and latitiudinal sides.

Add support for configuration files.

Added the additional mouse routines vrl_MouseGetUsage(),
vrl_MouseSetUsage(), vrl_MouseSetPointer(), vrl_MouseGetPointer() and
vrl_MouseGetLimits().

Appendix B - CFG FILE FORMAT

A CFG file is a platform-specific ascii file used to describe the user's
preferred configuration.  It contains a series of statements, one per line;
anything after a '#' character on any given line is taken as a comment, and
blank lines are ignored.  At the moment, there are only a few statement
types defined; this is expected to change.

COMPASS state
     If state is "on", then the user wants the compass displayed on the
     screen.

FRAMERATE state
     If state is "on", then the user wants the frame rate displayed on the
     screen.

POSITION state
     If state is "on", then the user wants their current X,Z position
     displayed on the screen.

DISPLAYDRIVER name [mode]
     Specifies a display driver and mode to use; the mode is optional,
     and both are name and mode are platform-specific.

DEVICE name type mode address irq buffsize
     Sets up a device driver, and assigns it a symbolic name that can be
     referenced by the application.  The type field is the name of a
     device type, like "Cyberman" or "Spaceball".  The mode is passed
     to the device using vrl_DeviceSetMode(), and the address, irq and
     buffsize fields are just passed to the call to vrl_SerialOpen(); see the
     section on Serial Ports for details.  All parameters except the name
     and type are optional; devices that are not interfaced over a serial
     port do not need the address, irq or buffsize parameters, and
     devices all have a default mode.  The name can be anything you
     like; however, it should be something that's referenced by the
     application.  For example,

           device headtracker Redbaron 0 0x2F8 3 2000 

     would set up the Logitech Red Baron ultrasonic tracking device; the
     device would be hooked up to COM2 (address = 0x2F8, irq = 3)
     with a 2000 byte buffer.  The application would simply look up the
     device called "headtracker" and use the values it returns without
     having to worry about what kind of device it actually is.

DEVCONFIG name channel scale deadzone
     Sets the scale and deadzone parameters for a particular channel of a
     particular device.  The name must match the name of a device
     already specified with a DEVICE statement.  The channel can be
     either a number or one of the special values X, Y, Z, XROT,
     YROT or ZROT.  The scale can be either a number (in which case
     it's taken to be a scalar distance) or a number with an 'a' or 'A' in
     front of it (in which case it's taken to be an angle).  The deadzone
     value is a number, specified in device coordinates.  The deadzone is
     optional, and defaults to zero.
     For example,

           devconfig headtracker 2 15

     would cause channel 2 (the Z channel) to have a scale factor of 15
     and a deadzone of zero, while

           devconfig headtracker YROT a45 10

     would case channel 4 (the Y rotation channel) to have an angular
     scale factor of 45 degrees and a deadzone of 10 device units.

Appendix C - PLG FILE FORMAT

I originally designed PLG files for use with REND386; for better or worse,
they seem to have become something of a standard.  REND386, AVRIL,
VR386 and Jon Blossom's Gossamer all use them for object geometry
description; there are also translators that turn other formats into PLG, and
the NorthCAD-3D program can generate PLG files as output.  The PLG in
the name stands for "polygon".

     There will soon be a file format for the interchange of virtual
objects and virtual worlds between VR systems; at that point, support for
the PLG file format will diminish.  Conversion programs will be made
available to convert PLG files to the new format.

     A PLG file basically has three parts: a header line, a list of vertices
and a list of facets.

     The header line has the object name, the number of vertices, and
the number of facets; for example:

     kitchen_table 100 35

which would mean that the object "kitchen_table" has 100 vertices and 35
facets.

     Anything after the facet count should be ignored, since it may be
used for future expansion.

     Following this line are the vertices, one x,y,z triplet per line (each
value is a floating-point number, and they're separated by spaces).  For
example:

     18027 23025 98703

Only the first three values on the line should be used; anything following
these values should be ignored.  This allows future support for such things
as vertex normals.

     This is followed by the facet information, one line per facet; each
of these lines is of the form

     surfacedesc n v1 v2 v3 ...

The surfacedesc is described below.  The n is the number of vertices in the
facet.  The v1, v2, v3 and so on are indices into the array of vertices; the
vertices are listed in a counter-clockwise order as seen from the "front"
(i.e. visible side) of the facet.  Note that the vertices are counted "origin
zero", i.e. the first vertex is vertex number 0, not vertex number 1.

For example:

     0x8002 4 8 27 5 12

would mean a four-sided facet bounded by vertices 8, 27, 5 and 12.  This
facet has a surface descriptor of 0x8002.

     Anything after the list of vertex indices should be ignored.

     The PLG format supports comments.  Anything after a # should be
ignored by any program that parses PLG files.  In addition, lines beginning
with a '*' should be ignored.

     PLG files can have multiple copies of an object at different
resolutions.  PLG files containing such multiple-resolution versions of
objects must have "#MULTI" as their first line.

     For each object defined in such a file, the object name includes a
number specifying the pixel size of the object on the screen.  The object
names for each representation must be

     <name>_####

where #### is the smallest pixel width to use this representation for. For
example, TABLE_15 would be a valid name.

     If the smallest rep size is zero, then that represenation will be used
no matter how small the object gets.  If the smallest rep size is 1 or
greater, then the object will vanish if it gets too small.

     The surface descriptor can either be a decimal integer or a 0x or 0X
followed by a hexadecimal integer value.  The surface descriptor is a 16-
bit value which is interpreted as follows:

     H R SS CCCC BBBBBBBB

The R bit is reserved, and should be set to zero.  If the H bit is set, it
indicates that this is a "mapped" surface descriptor; the bottom 14 bits are
taken to be an index into a surface map.

If the H bit is clear, the SS bits are interpreted as follows:

     00 -- This facet is "solid shaded"; i.e. it should be drawn in a
           fixed color, with no special effects. If the CCCC bits are
           zero, then the BBBBBBBB bits directly specify one of the
           256 available colors; if the CCCC bits are non-zero, then
           they specify one of sixteen hues and the top four bits of
           BBBBBBBB specify which shade of that hue to use.

     01 -- This facet is "flat shaded"; i.e. it should be drawn with a
           constant shading that is determined by the angle at which
           light is striking it; thus, as the facet moves around, its
           apparent brightness will change.  The CCCC bits specify
           one of sixteen hues, and the bottom 8 bits BBBBBBBB
           represent the "brightness" of the color.  This brightness
           value is multiplied by the cosine of the angle between the
           facet's normal vector and the vector from the facet to the
           light source; the result is used to specify an offset into the
           given color's array of shades.  Note that if the CCCC value
           is 0, the color will always be black.

     10 -- This facet should be treated as being "metallic"; the CCCC
           bits (which should be non-zero) specify one of the 16 hues,
           and the top 5 bits of the BBBBBBBB value are used as an
           offset into a range of shades to cycle through to give the
           metallic effect, i.e. a starting offset into the color cycle.

     11 -- This facet should be treated as being "transparent"; it is just
           like surface type 10, except that alternating rows of dots are
           used instead of solid colors, allowing you to "see through"
           the facet.

Appendix D - FIG FILE FORMAT

FIG files are a way of representing multi-segmented, hierarchical entities.

      This format will soon be considered obsolete.

     There will soon be a file format for the interchange of virtual
objects and virtual worlds between VR systems; at that point, support for
the FIG file format will diminish.  Conversion programs will be made
available to convert FIG files to the new format.

     The syntax of a figure file is simple, and very C-like.  It consists of
a series of segments, each of which can possess a set of attributes, as well
as child segments.  Each segment is bounded by braces.  Attributes are
arbitrary text strings ending in a semicolon.

     The list of possible attributes is open-ended and extensible;
programs that read figure files should ignore any attributes they don't
recognize.

An example will make all this clearer.

     {
     comment = a human body;
     name = pelvis;  comment = this is the name of the root segment;
           {
           name = chest;
                { name = left upper arm; { name = left lower arm; } }
                { name = right upper arm; { name = right lower arm; } }
                { name = head; }
           }
           { name = left upper leg; { name = right lower leg; } }
           { name = right upper leg; { name = right lower leg; } }
           }
     }

In general, attributes are of the form "keyword = value;", though this is not
a requirement.  The attributes used above are name and comment.  Note
that no program ever has to recognize a comment attribute, since by
defintion comments should be ignored.

The attributes currently defined are as follows:

     name = somestring;
     pos = x,y,z;
     rot = x,y,z;
     plgfile = filename scale x,y,z shift X,Y,Z sort type map filename;
     segnum = someinteger;

The pos is the x,y,z displacement of the origin of this segment relative to
the parent's coordinate system.  The rot is the rotation of this segment
relative to the parent.  For root objects (which have no parent) these values
are the absolute location and rotation of the entire figure in world
coordinates.

     The plgfile gives the name of a .plg file containing a geometric
representation of the segment.  Note that the figure file format does not
strictly depend on .plg files; the reason the syntax is "plgfile = " rather
than just "file =" is because a segment may have a large number of
different representations and an application can choose whichever one they
like.

     The scale, shift, sort and map values are all optional, but in order
to specify any of them you must specify all the preceeding ones (i.e. you
cannot simply omit the scale parameter).  The scale values represent the
amount by which the object should be scaled along each of its axes when
it's loaded.  The shift value is the amount by which to shift the object's
origin at load time.  The sort value is the type of depth-sorting to use for
this segment's representation (the default is zero).  The map value is the
name of a file containing a list of unsigned values that are to be used in
surface remapping for this segment.  If the top bit of a color value is set in
a plg file, the bottom fourteen bits are used as an index into this map.

     The difference between shift and pos is important.  The shift value
is used to shift an object relative to its "native" origin, while the pos value
is the amount by which the new origin should be displaced from the parent
node's origin.

     For example, suppose you want to represent the head of a human
figure with a cube.  The cube may, in the .plg file, be defined with its
(0,0,0) point at one corner.  Clearly, this origin is inconvenient for the
head, since if the origin is centered over the neck of the figure then the
head will be displaced to one side.

     Alternatively, the cube might be defined with its (0,0,0) point at its
geometric center.  However, this is also impractical; your head  should not
rotate freely about its center.  If it does, stop reading this document
immediately and seek medical attention.

     What you to do is shift the cube so that its origin lies below the
center of the cube, where your "neck joint" is.  That's what the shift value
in the plgfile attribute specifies.

Important note: objects rotate about their [0,0,0] point as loaded.

     The pos attribute specifies where this neck joint is in relation to the
origin of the chest segment.  If your chest were longer vertically, then the
pos attribute of the head segment should be increased in the Y direction
(for example).

     The segnum attribute associates a simple integer value with a
segment, which can subsequently be used to refer to the segment when
manipulating it.

     Note that a figure file can in fact contain a series of segments; each
of these is a root segment, so a figure file can in effect store a complete
scene description (excluding lights and cameras).

Appendix E - WLD FILE FORMAT

WLD files were designed to store information about the layout of objects
in a virtual world.

      This format will soon be considered obsolete.

There will soon be a file format for the interchange of virtual objects and
virtual worlds between VR systems; at that point, support for the WLD file
format will diminish.  Conversion programs will be made available to
convert WLD files to the new format.

A WLD file is entirely ascii.  Each statement is one line; anything after the
first '#' is treated as a comment and ignored.  Blank lines are also ignored. 
The format is intended to be highly extensible; any line which cannot be
recognized should simply be ignored.  Each statement contains some
information about the scene; the possible types of statements are listed
below.  Everything is case-insensitive; keywords are shown below in
uppercase, but are generally entered in lowercase.

LOADPATH path
     Specifies a path prefix for loading files.  Any files (whether
     specified in the world file itself, subsequent world files, or in
     referenced FIG files) will be loaded from the specified directory. 
     However, if a filename begins with the '\' or '/' characters, it is
     used verbatim (i.e. the LOADPATH setting is ignored).

PALETTE filename
     Loads a 256-entry binary palette file (3 bytes (R,G,B) for each
     entry).  Note that alternate palettes may not handle shading as well
     as the default one does.

SKYCOLOR index
     Specifies which of the 256 available colors should be used for the
     "sky".

GROUNDCOLOR index
     Specifies which of the 256 available colors should be used for the
     "ground". If the sky and ground color are identical, a solid screen
     clear is used; this is a bit faster.

SCREENCLEAR value
     If the specified value is non-zero, then the screen will be cleared
     before each frame; if it's zero, the screen clearing is not done (this
     is useful if you know that the entire window will be covered by the
     image, and that no background will show through; in such a
     situation, specifying this option will improve performance).

AMBIENT value
     Specifies the level of the ambient light; 76 is the default, and a
     good value to use.

LIGHT x,y,z
     Specifies the location of a light source in world coordinates.

CAMERA x,y,z tilt,pan,roll zoom
     Specifies your starting location, viewing direction and zoom factor. 
     The x,y,z values are floating-point numbers giving coordinates, the
     tilt,pan,roll values are floating-point angles, and the zoom is a
     floating-point number giving the zoom factor.

HITHER value
     Specifies the near clipping distance in world coordinates.  The
     value should typically be 10 or more.

YON value
     Specifies the far clipping distance in world coordinates.  The value
     should typically be 1000000 or more.

OBJECT [objname=]filename sx,sy,sz rx,ry,rz tx,ty,tz depthtype mappings
parent
     Loads an object from a .plg file with the given filename.  If the
     objname= is present, it assigns the newly-loaded object that name
     for future reference.  The sx,sy,sz values are floating-point scale
     factors to increase or decrease the size of the object as it's loaded. 
     The rx,ry,rz values are the angles to rotate the object around in
     each of the three axes; ry is done first, then rx and finally rz.  The
     tx,ty,tz values translate (move) the object to a new location; this is
     done after the scaling and rotation.  The depthtype field is not
     currently used.  The mappings feature is explained below.  The
     parent field is the name of the object that this object is a child of;
     if omitted, the child moves independently.  If parent is the word
     "fixed", then the object is fixed in space and cannot be moved.  All
     fields are optional, but you must include all the fields prior to the
     last one you wish to use (i.e. you can only leave things off the end,
     not off the beginning or out of the middle).

FIGURE [figname=]filename sx,sy,sz rx,ry,rz tx,ty,tz parent
     Loads a segmented figure from a FIG file with the given filename. 
     All the parameters have the same meaning as for the OBJECT
     statement described above.

POLYOBJ npts surface x1,y1,z1 x2,y2,z2 [...] x8,y8,z8
     Directly specifies a facet to be placed in the scene.  The value npts
     is the number of points (maximum 8), the surface is a surface name
     (see below on surfaces) and the vertices are given in world
     coordinates.

POLYOBJ2 npts surface1,surface2 x1,y1,z1 x2,y2,z2 [...] x8,y8,z8
     Directly specifies a double-sided facet to be placed in the scene. 
     The value npts is the number of points (maximum 8), surface1 and
     surface2 are surface names (see below on surfaces) and the vertices
     are given in world coordinates.

INCLUDE filename
     Includes the specified file as if its contents appeared at the current
     point in the current file.

POSITION objname x,y,z
     Moves (i.e. translates) the specified object to the given x,y,z
     location.

ROTATE objname rx,ry,rz
     Rotates the specified object to the given angles about each of the
     axes.  The angles are specified in floating point, and are measured
     in degrees.  The rotation order is Y then X then Z.

VERSION number
     Allows you to define a version number.  Not currently used for
     anything; can be omitted.

TITLE text
     Allows you to define a title for your world.

About Mapping

A PLG file can contain indexed color values (such as 0x8002) which are
used to index a surface map.  Entries in surface maps refer to surfaces. 
Surfaces are created using the SURFACEDEF statement, surface maps are
created with the SURFACEMAP statement, and entries are placed in them
with the SURFACE statement.  The statement formats are as follows:

SURFACEDEF name value
     Defines a new surface; maps a surface name (such as "wood") to a
     numeric surface descriptor (value) of the type described in
     Appendix C.

SURFACEMAP name maxentries
     Marks the start of a new surface map.  All subsequent SURFACE
     entries will be placed in this map.  The maxentries field gives the
     maximum number of entries this surface map will have; if omitted,
     it defaults to 10.

SURFACE index name
     Defines an entry in the current surface map, which takes an index
     value (the bottom 15 bits of the value in the .plg file) and maps it
     into a surface name (which is in turn mapped to a 16-bit color
     value).

USEMAP mapname
     Causes all subsequently loaded objects that don't have a mapname
     on their OBJECT statements to use the specified mapname.
Appendix F - WRITING DEVICE DRIVERS

Writing device drivers for AVRIL is easy.  You basically create a single
function with a unique name; for example, if you want to support a
(mythical) RealTronics Atomic Tracking System, your function might be

     int vrl_ATSDevice(vrl_DeviceCommand cmd, vrl_Device *device)
           {
           [...]
           }

You should add an entry for your new function to the list in avrildrv.h, and
possibly to the cfg.c file.

     Your driver routine will get called periodically by the application. 
The vrl_Device struct is pre-allocated by AVRIL, so you just have to fill
in the various fields.  The cmd is one of VRL_DEVICE_INIT,
VRL_DEVICE_RESET, VRL_DEVICE_POLL, or VRL_DEVICE_QUIT.

     When a device is first opened, AVRIL will set all the fields in the
vrl_Device struct to reasonable values.  The VRL_DEVICE_INIT call
should fill in the following fields with driver-specific information:

     char *desc;                 /* user-readable device description */
     int nchannels;              /* number of input channels the device has
*/
     vrl_DeviceChannel *channels;   /* pointer to array of channels */

The desc is a string describing the device, the nchannels value is the
number of input channels the device has (should be at least 6) and the
channels field is set to point to an array of vrl_DeviceChannel structs, one
per channel.  These channels should be dynamically allocated, rather than
using a static struct; this is to allow multiple instances of the same type of
device (for example, a Cyberman on each of COM1 and COM2, each with
its own channel-specific data).  For this same reason, your driver shouldn't
use any global variables; you should instead dynamically allocate memory
for any additional per-device-instance data and store the pointer to that
data in the localdata field of the vrl_Device struct.  The
VRL_DEVICE_INIT call should also fill in the appropriate values for all
the channels.

The VRL_DEVICE_INIT call may also choose to fill in some or all of the
following:

     int nbuttons;               /* number of buttons the device has */
     int noutput_channels;       /* number of output channels */
     vrl_DeviceOutputFunction *outfunc;  /* function to call to generate
output */
     vrl_DeviceMotionMode rotation_mode;     /* rotation mode for this
device */
     vrl_DeviceMotionMode translation_mode;  /* translation mode for this
device */
     vrl_Buttonmap *buttonmap;   /* button mapping table for 2D devices */
     int version;                /* device struct version number */
     int mode;                   /* mode of operation */
     vrl_Time period;            /* milliseconds between reads */

The number of buttons the device has is assumed to be zero unless you set
it otherwise, as is the number of output channels.  The outfunc field is a
pointer to a function (probably declared static in the same source file as
your driver function) that handles output to the device; this is described in
more detail below.  If your device doesn't do output, leave this field at its
default value of NULL.

     The meaning of the two vrl_DeviceMotionMode type fields was
described earlier in this document, in the section on Devices.  They both
default to VRL_MOTION_RELATIVE.  The mode is driver-specific, and
can be initialized to whatever value you like (since the value is only
interpreted by your driver).  The version field should be left at its default
value of zero by drivers following this version of the driver specification;
as the driver specification evolves, this value will increase.

     The period defaults to zero, meaning that a call to vrl_DevicePoll()
will always result in your driver function being called with a cmd of
VRL_DEVICE_POLL.  If you don't want to be polled every cycle, set the
period to the minimum number of ticks (milliseconds) that should elapse
between polls.  Note that this is a minimum value; the delay between polls
may be even longer if the system is busy doing other things.

     The VRL_DEVICE_RESET command is very similar to
VRL_DEVICE_INIT, and may in fact be the same for some devices.  The
difference is that VRL_DEVICE_INIT does one-time initializations (such
as taking over interrupt vectors).

     The VRL_DEVICE_QUIT command should "shut down" the
device, putting it in a quiescent state and undoing anything that was done
in VRL_DEVICE_INIT and VRL_DEVICE_RESET (for example,
restoring interrupt vectors).  It's also responsible for releasing any memory
that was dynamically allocated by VRL_DEVICE_INIT, including that
pointed to by the channels field and the localdata field if it was used.

     Serial devices can assume that when VRL_DEVICE_INIT is called,
the port they'll be using is already open, and that the port field is set; the
driver also does not need to (and should not) close the port.  However,
devices that actually use the port should check that it's not NULL, and
return -4 if it is.

     The VRL_DEVICE_POLL command should read the raw data from
the hardware (for example, by calling vrl_DeviceGetPacket()) and decoding
the values into the rawdata fields of the appropriate channels.  You should
be sure to set the changed field for any channels that you update.

There are a number of values associated with each channel; they are as
follows:

struct _vrl_device_channel
     {
     long centerpoint;        /* value of center point in raw device coords
*/
     long deadzone;           /* minimum acceptable value in device coords
*/
     long range;              /* maximum absolute value relative to zero */
     vrl_Scalar scale;        /* maximum returned value */
     vrl_Boolean accumulate : 1;      /* if set, accumulate values */
     vrl_Boolean changed : 1;         /* set if rawvalue has changed */
     long rawvalue;           /* current value in raw device coordinates */
     long oldvalue;           /* previous value in raw device coordinates
*/
     vrl_Scalar value;        /* current value (processed) */
     };

The only fields you must set are the centerpoint, deadzone, range, scale
and accumulate.  The centerpoint is the current "zero" value of the device;
for example, the value an analog joystick on a PC-compatible reports when
the stick is at rest can be considered its centerpoint.

     The deadzone has two different interpretations.  If the accumulate
flag is set, then the deadzone is the minimum displacement from the
centerpoint that will be recognized.  If the accumulate flag is clear, then
the value is the minimum change from the previous value (as stored in
oldvalue by the higher-level routines) that will be recognized.

     The scale, and range values are used to convert the rawvalue into
units more suitable for the application.  The scale is the number of world-
space units corresponding to the range in device units.  The scale, and
deadzone should both be positive values, and can be changed by the
application.  The range value can be negative; this is useful for reversing
the direction of a device axis.  The range value is only ever set by your
driver.

     The accumulate flag, in addition to controlling how the deadzone is
interpreted, causes the value to be scaled by the elapsed time in seconds
since the last poll.

     The best way to understand all this is to consider what happens
when you read new values from the device.  First, you store the data for
each channel in the corresponding channel's rawvalue field; you can re-
map axes at this point (device coordinate Y goes into the Z channel, for
example).  You should set the changed flag, to indicate there's a new
value there.

     Next, the higher-level code takes your rawvalue and subtracts the
centerpoint.  If the channel is in accumlate mode, it checks if the absolute
value of the data is less than deadzone; if it is, it truncates it to zero.  If
the channel is not in accumulate mode, the data is compared to the
oldvalue field; if it's within plus or minus deadzone of it, the data is
ignored.

     Once the value has been centered and deadzoned, it is multiplied by
the scale and divided by the range.  If accumulate is set, the resulting
value is multiplied by the elapsed time in milliseconds and then divided by
1000 to convert to seconds.

Buttonmaps

     Some 2D devices (such as mice and joysticks) can be used as 6D
devices, by using their buttons to map their input axes to the 6 possible
degrees of freedom.  Such devices should set their nbuttons field to zero
(or at least to the number of buttons that will not be used for mapping). 
They should also set their buttonmap field to point to a default set of axis
mappings.

     The buttonmap field is a pointer to a two-dimensional array.  Each
row of the array corresponds to a button combination; on a two-button
device, row 0 is for no buttons down, row 1 is for the first button down,
row 2 is for the second button down and row three is for both buttons
down.  There are two columns in the array, the first of which contains the
index of the channel that the input device's X value should be stored in,
and the second of which contains the index of the Y channel.

For example,

     static vrl_Buttonmap default_map =
           { { YROT, Z }, { X, Y }, { ZROT, XROT }, { X, Y }};

Would mean that when no buttons are down, the device's X axis
corresponds to a Y rotation, and its Y channel to a Z translation.  When
the first button is down, the device's X axis corresponds to an X
translation, and its Y axis to a Y translation, and so on.

     The application can change the buttonmap field (using
vrl_DeviceSetButtonmap()) to point to a different set of mappings.

     One thing to watch out for: since only two channels at a time are
active, the others should have their rawvalue set equal to their centerpoint,
and their changed flags set; otherwise, they'll retain whatever values they
had last time a particular button combination was active.

Output

     Some devices are capable of tactile or auditory feedback; those that
are should set the outfunc field in the vrl_Device struct to point to a
function that does the actual work, and set the noutput_channels field to
the number of output channels the device has.  Such a function for our
mythical ATS device might look like this:

     int vrl_SpaceballOutput(vrl_Device *dev, int parm1, vrl_Scalar parm2)
           {
           [...]
           }

The parm1 parameter is the output channel number, and parm2 is the value
to output (in the range 0 to 255).  The routine should return 0 on success
and non-zero on failure, although those values are not currently used or
reported.
