Behind the graphics lies a layered system of rendering abstraction. In order to make polygon definition and rendering as simple as possible, the user-level code can take advantage of object ``primitives'' that hide the complicated details of adding objects to a virtual scene.
One of the first buildings constructed in the Metaverse has become one of its most popular sites. It is called the Black Sun, and it is a colossal black frustum whose base is 100 meters by 100 meters. Software hackers, movie stars, rock starts and Nipponese business people gather here to interact with each other. The inside is much like a night club and is divided into four quadrants (one for each of the previously mentioned groups) with a circular bar in the center. It was constructed by Hiro Protagonist (the main character of the story), his former business partner and employer, Da5id, and several other hackers. The idea behind it was to use simple polygonal shapes in order to save computing power for collision detection and other more important aspects.
From this comes the inspiration for this project. The details for building a scale model of the Black Sun in a virtual reality environment are all available in the novel. Snow Crash was the primary reference for the design of the virtual world. Throughout the novel, pieces of information regarding the size of the Black Sun building, the width of the Street, the number of ``ports'' granting entrance to the Metaverse and even the circumference of the sphere upon which the Metaverse is built is provided. All that is necessary for the person(s) doing the world development is to collect and to use this information.
For the purposes of this paper, screenshots have been taken while the C2 simulator software was running. These are intended to give a general view of the highlights of this environment. The following figures present several views of the Black Sun and the undeveloped Street and Metaverse outside the walls of the building.
|
|
|
|
|
|
|
|
As can be seen, the Black Sun is very large. Some liberties were taken in designing the building because no specific details were given in the novel. This includes the size, spacing and elevation of the tables that are in each of the four quadrants, the height of the building and the layout of the tunnel system.
The only other area where the novel's description was not followed exactly is in the length of the Street. The Metaverse is actually a sphere with a circumference of 65,536 kilometers, and the Street wraps all the way around this sphere. At this stage, attempting to render such a gigantic object is unrealistic, so the Street has been made to be only 2 kilometers long. This provides ample space for later development while not hurting the performance.
If both the first and second buttons are pressed simultaneously, ``fly mode'' is toggled. The effect is to allow the user to increase his or her upward (y) motion instead of restricting motion to be in only the x- and z-directions. When in fly mode, collision detection is still active, so the user cannot fly through walls or floors any easier than he or she could walk through them.
In order to manipulate certain objects in the scene, the user must form a fist (or somehow make a gesture where all five fingers are closed) with the hand wearing the data glove. When this gesture is made, the user can lift objects or open and close doors. Currently, the only such object is the trap door in the floor of the Black Sun leading to the underground tunnel system.
void
draw () {
GLfloat vertex[3];
/* Set values for the vertices. */
glBegin(GL_QUADS);
addVertexN3fv(vertex[0]);
addVertexN3fv(vertex[1]);
addVertexN3fv(vertex[2]);
addVertexN3fv(vertex[3]);
glEnd();
}
Clearly, this level currently does not have a complete layer of abstraction
for the modeling API, but there are hidden details here. For instance, the
above calls normalize the vertices passed. Other calls are available for
adding only a vertex, adding a user-specified normal with a vertex and
adding only the normal. However, the addVertexN2*() and
addVertexN3*() calls are intended to be the most commonly used
variants. Helper functions for normalizing vectors or the three components
of a vector are also available to facilitate the addition of polygon normals
to a scene.
As mentioned, facilities for adding textures and lighting are also present in this layer. If a user wants to add a light to his or her scene, he or she would do the following:
#define ONE_LIGHT_ID GL_LIGHT0
light_t one_light;
void
init () {
/* Define ambient light properties. */
one_light.ambient[RED] = 0.1;
one_light.ambient[GREEN] = 0.1;
one_light.ambient[BLUE] = 0.1;
one_light.ambient[ALPHA] = 1.0;
/* Define diffuse light properties. */
one_light.diffuse[RED] = 0.8;
one_light.diffuse[GREEN] = 0.8;
one_light.diffuse[BLUE] = 0.8;
one_light.diffuse[ALPHA] = 1.0;
/* Define specular light properties. */
one_light.specular[RED] = 0.8;
one_light.specular[GREEN] = 0.8;
one_light.specular[BLUE] = 0.8;
one_light.specular[ALPHA] = 1.0;
/* Define light position. */
one_light.position[X_COORD] = 0.0;
one_light.position[Y_COORD] = 32.0;
one_light.position[Z_COORD] = -1.0;
one_light.position[SCALE] = 1.0;
/* Add this light to the scene. */
addLight3fv(ONE_LIGHT_ID, &one_light);
}
Again, it is easy to see that this layer does not fully abstract the
modeling API at this time. Spotlights can similarly be added by defining
the spotlight direction (spot_direction[3]), the spotlight cutoff
angle (spot_cutoff) and the spotlight exponent
(spot_exponent).
As mentioned above, texture mapping is provided at this layer. The image file format used is IRIS RGB, and the function newTexture() loads the specified image file and returns a pointer to a texture_t structure. The code for loading IRIS RGB files is based on a model written by Kevin Perry (perry@princenton.edu). The user then specifies the texture coordinates when defining the polygon coordinates just as is done in OpenGL. An example of how to use this code is given next in the next section that covers the ``object'' layer.
quadrilateral_t* quad;
void
init () {
double vertex[4][3], normal[3];
/* Set vertices. */
quad = newQuadrilateral();
addObjNormal3dv(normal);
addObjVertex3dv(vertex[0]);
addObjVertex3dv(vertex[1]);
addObjVertex3dv(vertex[2]);
addObjVertex3dv(vertex[3]);
endObject();
}
void
draw () {
drawObject(quad, QUADRILATERAL);
}
To free the memory used for quad, a routine called
killQuadrilateral() is provided so that all memory deallocation is
hidden from the user.
Defining a triangle or other polygon is done similarly. Currently, calls for creating other primitives for triangles (newTriangle()) and polygons with an arbitrary number of vertices (newPolygon()) are also available. They both have corresponding kill*() routines for deallocation. However, all object creation routines that require distinct vertices use addObjVertex2*() and/or addObjVertex3*(). Every object creation must end with a call to endObject(). The layer knows which object is currently being defined at all times, and thus no other calls (e.g., creating another polygon) can come between the new*() and endObject() block.
For more complex objects such as spheres or cylinders, a slightly different approach is taken. The syntax closely follows the OpenGL calls to routines such as gluSphere() or gluCylinder(). A sphere would be created and rendered as follows:
sphere_t* sphere;
void
init () {
/* Create a sphere with a radius of 3.0 made up of 32 slices and 32
stacks. */
sphere = newSphere(3.0, 32, 32);
endObject();
}
void
draw () {
drawObject(sphere, SPHERE);
}
Besides all objects using the same calls to addNormal*(),
addVertex*() and endObject(), every object currently supported
is rendered with call to drawObject(). The first argument to this
routine is a pointer to the object being drawn, and the second is a member
of the enumerated type objenum. The current members of
objenum are:
Addition of more primitives and high-level objects is simple. At the most basic level, all that is needed is a routine for allocating a new object and destroying it when necessary. Creating these new calls is simple because they must follow the existing structure for creation and destruction. Beyond this, a block of code must be added to drawObject(), to drawTexObject() and to endObject() to deal with the new object. The addition to the rendering routines is simple and involves one of two rendering methods:
An entry must also be made to the objenum type (found in objects.h). Once these steps are taken, user code can immediately take advantage of having the new object available to define and render.
In conjunction with the ``direct'' layer, texture mapping of objects created at this level is possible. As with other facilities, the ``object'' layer utilizes the ``direct'' layer to provide this functionality. This code block shows how to define and render a texture mapped object.
triangle_t* tri;
texture_t* tex;
void
init () {
tex = newTexture("path/to/texture.rgb");
tri = newTriangle();
addObjNormal3f(0.0, 0.0, 1.0);
addTexObjCoord2s(0, 0);
addObjCoord2f(0.0, 0.0);
addTexObjCoord2s(0, 1);
addObjCoord2f(0.0, 1.0);
addTexObjCoord2s(1, 0);
addObjCoord2f(1.0, 0.0);
endObject();
}
void
draw () {
drawTexObject(tri, TRIANGLE);
}
At this time, the texture mapping code in both layers is not fully
functional. While the image files appear to be read correctly, any
attempts to render a textured polygon cause a segmentation violation. The
cause of this problem is not yet clear.
A major detail hidden by the above examples is the activation of collision detection on the newly created objects. As far as the user can tell, there is nothing special about any polygon. Underneath, a database of polygons in the scene is being built to provide for collision detection should the user decide to enable it. The collision detection implementation is discussed in the following section.
In order to use V-Collide, all polygons must be triangulated. Thus, at the ``object'' layer, all polygonal objects are actually made of separate triangles. This is entirely hidden from the user. When endObject() is called, the coordinates that the user gave for the polygon are used to form the triangles that will make up the new polygon. Then these triangles are added to the V-Collide database of objects and are activated for collision detection. Any object that is not triangulated by the ``object'' layer is thus not activated for collision detection.
Currently, only triangles and quadrilaterals are triangulated. Code exists for defining and rendering arbitrary polygons, but these polygons are not triangulated at this time. The more complex polygons (i.e., spheres, disks, etc.) are also not triangulated, and in fact, these objects are rendered using the glu*() calls for the time being. However, simple, untested code for triangulating and rendering a disk without using gluDisk() exists. Because the ``object'' layer abstracts every aspect of this, the user would not have to modify any code whatsoever to deal with triangulation of these objects once it is implemented.
Collision detection is performed against the position of the tracker wand in world coordinates. To do this, a ``mark triangle'' is positioned at the coordinates of the wand. It is translated appropriately as the wand moves through space and is never actually rendered. All objects defined through the ``object'' layer are paired with this triangle for collision detection.
At the time of this writing, collision detection is not functional. Collisions are not being properly reported, and this is probably due to an error when pairing triangles for detection. However, an addition to the ``direct'' layer that provides an interal transformation matrix stack is providing the necessary services to allow polygonal transformation with collision detection.
When the V-Collide library was integrated into the code, the need for a higher level interface arose. Because all the polygons needed to be triangulated, some way of providing automatic triangulation and addition of those triangles into the V-Collide database was needed. This began the development on the ``object'' layer. The ``object'' layer evolved into an easy method for adding primitives and high-level objects into the scene. As shown above, this result has been achieved. While this layer is still not yet complete, addition to it is designed to be quick and easy.
In the end, progress on the project proved to be much slower than expected. This was primarily due to the work done on the layering system so that polygon and object addition would be fast at the user-level. Once these two layers were stabilized, actual rendering of objects in the scene did prove to be quick and easy. However, time constraints left several aspects of the project unfinished. These are summarized next.
The Street and surrounding Metaverse are totally undeveloped, but the necessary information to finish the Street is available in Snow Crash. This includes the addition of ``ports'' through which users enter the Metaverse and the monorail and its stops that are used by avatars for rapid transportation along the Street. With regard to developing the Metaverse, the possibilities are limitless. Ideally, anyone could design a building that could be incorporated into the Metaverse and placed somewhere on the Street. As in the novel, what people build in the Metaverse are limited only by the developer's imagination.
Beyond further scene definition and rendering, both the ``direct'' and ``object'' layers still have several unfinished areas. In particular, the ``direct'' layer does not completely hide the rendering API at this time. However, while neither layer is complete, both have been designed to be completely independent of the code for this specific project. They are designed to be add-in ``modules'' and could thus be compiled into an independent utility library.