Category : Printer + Display Graphics
Archive   : AVRIL11.ZIP
Filename : AVRILDOC.TXT

 
Output of file : AVRILDOC.TXT contained in archive : AVRIL11.ZIP










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.


1







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


2







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
(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

3







#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

4







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

#include "avril.h"

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

5







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

6







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

#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();
}


7







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;

8







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

9







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

10







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

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

11







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

12







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

13








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

14







vrl_ObjectTranslate(active_object, v, VRL_COORD_OBJREL, viewer);
vrl_SystemRequestRefresh();
}

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

Our main() is shorter, and simpler. You'll notice that we've
added a call to vrl_ReadCFGfile(); it reads the configuration
file we specify (in this case "example6.cfg"), and 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_DeviceFind() 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 */


15







#include "avril.h"

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

The NULL parameter to vrl_ReadCFGfile() 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

16







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_DeviceFind("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


17







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)

18







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

19







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

20







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



21







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

22







}
}

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.



23







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

24







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] */

25







{
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
loading configuration information are

int vrl_ReadCFG(FILE *in);
int vrl_ReadCFGfile(char *filename);

The vrl_ReadCFG() function reads a configuration file and stores
the information from it in an internal set of data structures.
Any devices specified in the file are opened, and the serial
ports they use are opened as well. If a display driver is
specified, the display is initialized using that driver. The
vrl_ReadCFGfile() routine does the same thing, but uses the name
of a file rather than a pointer to an already-opened file. The
filename is processed to prepend the current loadpath (unless the
filename begins with a slash). If the filename is NULL, then
"avril.cfg" is used.

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

26







vrl_Boolean vrl_ConfigGetFramerateDisplay(void);
void vrl_ConfigToggleFramerateDisplay(void);

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:

vrl_ReadCFGfile(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


27







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 several 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). The types vrl_32bit

28







and vrl_unsigned32bit are used for signed and unsigned 32-bit
numbers, and vrl_16bit and vrl_unsigned16bit are used for signed
and unsigned 16-bit numbers.

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

29







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

30







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.

31







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

32







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

33







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


34







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

You can also orient an object to "look" in a particular direction
using the function

void vrl_ObjectLookAt(vrl_Object *obj, vrl_Vector forward, vrl_Vector up);

The object will be rotated so that it's Z axis points along the
forward vector, and its Y axis points in the general direction of
the up vector. Note that the actual Y orientation may be
different, unless you make sure that up is perpendicular to
forward. The up and forward vectors are specified in world
coordinates, and should both be unit vectors; you may find the
vrl_VectorNormalize() function handy for this.

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.



35







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

36







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


37







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


38







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:

39







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.



40







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


41







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

42







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_LightLookAt(light, forward, up);
vrl_LightAttach(obj, newparent);
vrl_LightDetach(obj);
vrl_LightGetLocation(light, v);
vrl_LightGetX(light);
vrl_LightGetY(light);
vrl_LightGetZ(light);


43







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



44







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_CameraLookAt(camera, forward, up);
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);

45







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

46







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

47







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

48







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.

49







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



50







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

51







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

52







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.

Devices can also have "nicknames"; for example, your
application might deal with a head-tracker called "headtrack"
which would be defined (in the configuration file, most likely)

to be the device used for head tracking (e.g. an Polhemus Isotrak
or a Logitech Red Baron). You can set and query the nickname of
a device, or find a device with a particular nickname, using the
following functions:

char *vrl_DeviceGetNickname(vrl_Device *device)
;
void vrl_DeviceSetNickname(vrl_Device *device, char
*nickname);
vrl_Device *vrl_DeviceFind(char *nickname);

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


53







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);
vrl_unsigned32bit vrl_DeviceGetButtons(vrl_Device *device);

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 a
vrl_unsigned32bit word whose bits indicate whether the
corresponding buttons have changed state since the previous poll.

vrl_Boolean vrl_DeviceGetChanged(vrl_Device *device, int channel);
vrl_unsigned32bit 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.



54







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

55







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:


56







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;

57







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.



58







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









































59








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 [email protected]; 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
[email protected] 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.





60








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 1.0:

Added support for the Polhemus Isotrak.

Added the functions vrl_ObjectLookAt(), vrl_CameraLookAt() and
vrl_LightLookAt().

Simplified the code in cfg.c; as several people pointed out, it
was a complicated solution to a simple problem. Devices now have
"nicknames" by which they can be referenced by the application,
eliminating the need for the cfg.c code to keep track of that
information. The configuration file is read after (not before)
vrl_SystemStartup() is called, and a "display" statement will
simply re-initialize the display to use the new driver and/or
mode.

Fixed a bug in the horizon routine that had caused sky to be
displayed when looking straight down, and ground to be displayed
when looking straight up.

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 and #include (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.


61







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

62







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


































63








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

64







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.


































65








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.


66







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

_####

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.



67







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.


























68








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;

69







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.



70







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








































71








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

72







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

73







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


74







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.












































75








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 */

76







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

77







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
{
vrl_32bit centerpoint; /* value of center point in raw device coords */
vrl_32bit deadzone; /* minimum acceptable value in device coords */
vrl_32bit 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 */
vrl_32bit rawvalue; /* current value in raw device coordinates */
vrl_32bit 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.


78







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.


79







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.



































80


  3 Responses to “Category : Printer + Display Graphics
Archive   : AVRIL11.ZIP
Filename : AVRILDOC.TXT

  1. Very nice! Thank you for this wonderful archive. I wonder why I found it only now. Long live the BBS file archives!

  2. This is so awesome! 😀 I’d be cool if you could download an entire archive of this at once, though.

  3. But one thing that puzzles me is the “mtswslnkmcjklsdlsbdmMICROSOFT” string. There is an article about it here. It is definitely worth a read: http://www.os2museum.com/wp/mtswslnk/