The code for all these programs (and the Makefile) can be found
in code.tar.gz.
Basics
outline.cxx
outline.cxx shows the basic outline of a Performer/CAVE program. The initialization consists of the sequence of calls to pfInit, pfCAVEConfig, pfConfig, and pfCAVEInitChannels. The main loop of the program consists of calls to pfSync, pfCAVEPreFrame, pfFrame, and pfCAVEPostFrame. Most application-specific tasks should happen in the loop before pfSync; latency-critical operations should be done between pfSync and pfFrame.
Functions in the pfCAVE library have names beginning with pfCAVE; all other functions and classes whose names begin with pf are part of the standard Performer libraries.
Performer's rendering pipeline is based on channels; a channel corresponds to a single view of the scene. pfCAVE creates channels for each wall and eye-view being displayed; it groups the channels to share most major attributes, such as the scene database and swapbuffering. pfCAVEMasterChan returns a pointer to one of the CAVE channels, which is sufficient for changing any of the shared attributes.
Performer can run in single-process or multi-processed mode. When running on a multiple CPU system, or using multiple graphics pipes, it automatically runs multiprocessed; on a single CPU machine, it defaults to single-process mode. A Performer application uses a pipeline model, with three basic stages - the APP, CULL, and DRAW. In multiprocessed mode, each stage runs in a separate process. The APP stage is for application computations; the CULL stage culls the scene database so that only visible objects will be rendered; the DRAW stage renders the objects passed to it by the CULL stage.
The world database is stored in a scene graph, the root of which is a pfScene object. The scene must be assigned to the channels (as in createScene) in order for them to process it.
The one unusual bit in outline.cxx is the changing of the statistics mode. Performer can collect a great deal of statistics on the processing and rendering that it is doing. In its default mode, it just collects the timing data for each stage in the pipeline. When running in simulator mode, outline.cxx enables all statistics gathering, except the graphics fill data, which would change the actual display of the scene. The statistics can be displayed in the simulator using the 't' command (if you want your program to force the statistics to be displayed, call pfCAVEMasterChan()->drawStats() on each frame). When running in non-simulator mode, outline.cxx disables all statistics, as the statistics gathering can have a slight performance cost.
Compiling a pfCAVE program requires the pfCAVE libraries and header, which are in the same locations as the standard CAVE library (/usr/local/CAVE/include & /usr/local/CAVE/lib). There is only one header file - pfcave.h. By default, it assumes you are using OpenGL; if you use IrisGL, you should #define IRISGL before including pfcave.h. The OpenGL pfCAVE library is -lpfcave_ogl; the IrisGL version is -lpfcave_igl.
The Performer headers and libraries are all found in the standard system directories (/usr/include/Performer & /usr/lib). The libraries required by an OpenGL Performer program are:
-lpfdu_ogl -lpfutil_ogl -lpf_ogl -lGL -lXi -lX11 -lm -lfpe -lC -limageThe libraries for an IrisGL Performer program are:
-lpfdu_igl -lpfutil_igl -lpf_igl -lgl -lm -lfpe -lC -limage
navobj.cxx demonstrates two significant Performer features - pfdLoadFile and scene traversal callbacks.
The pfdLoadFile function will load models in several supported object formats, including Inventor, Wavefront Obj, DXF, and 3D Studio. It uses DSOs (Dynamic Shared Objects) for the loaders for each format; hence support for new formats can be easily added (see the example below). The format of a file is determined from the extension of the file name; Inventor files must have names ending in ".iv" (or ".wrl"), Wavefront file names must end in ".obj", etc. The loader creates a scene graph containing the model data, and returns a (pfNode *) pointer to its root. This subgraph can then be added to the main scene graph. (Performer allows multiple instancing, so a model can in fact be added to the scene several times, as a child of different SCSs or DCSs, for example.)
Once a scene graph has been assigned to a channel, it will be traversed (that is, each node is visited and any required processing is done) on each frame, in the APP, CULL, and DRAW stages. An application may assign its own callback functions to any node, to be called during any of these traversals. navobj.cxx uses this approach to have the navigate function called for the DCS being used for navigation, rather than calling the function directly from the main loop. Traversal callbacks are assigned to nodes using pfNode::setTravFuncs; data can be passed to a callback via pfNode::setTravData. A traversal callback's prototype should be of the form:
int callbackfunction(pfTraverser *traverser,void *appData)The function should normally return the value PFTRAV_CONT (other values will halt the traversal below the current node).
A DCS is a transformation which may be changed while the program runs (an SCS is one which does not change after it is created). It is a Group node; all children of a DCS are transformed by it. navobj.cxx creates a DCS to contain the navigation transformation, and makes all the objects children of that node. Most of the navigation code is identical to that in a non-Performer CAVE program; the function pfCAVEDCSNavTransform is used at the end to copy the CAVE navigation transformation into the DCS for Performer to use.
Other new features used in navobj.cxx:
Performer classes may be subclassed, just as with any other class. However, Performer has a data typing system which any derived classes must work with (this typing system allows one to perform queries such as determining whether a given pfNode is a DCS). This consists of creating a class variable for the class's pfType, and giving every class instance a pointer to the type variable via pfMemory::setType (all Performer classes are derived from pfMemory). The class type variable must be created by an initialization function which is called after pfInit and before pfConfig. See the code for simpleNavigator's init function and constructor for details.
In simpleNavigator, the app function is used for the navigation update, instead of setTravFuncs, as was done in navobj.cxx. app is a pfNode virtual function which this overloads; each node's app function is called during the APP stage traversal of the scene graph, if it is needed. Most Performer nodes do not require any APP traversal, so app will not actually be called unless the function needsApp is also overloaded to return TRUE.
Other new features used in navclass.cxx:
perfClass is a template for creating a Performer class.
Copy the header & source code; replace all instances of "perfClass" with the name
of the new class you are creating, and replace all instances of "pfGroup" with the
name of the Performer class that you are deriving it from.
If you do not need the app() function (or are deriving from a non-pfNode
class), you can remove both app() and needsApp().
Loading a more complicated scene
world.cxx
loadWorld.cxx
cache.h
cache.cxx
world.cxx allows one to create more complex scenes using a very simple database format which is parsed by loadWorld. The database is described in a text file, with one option per line; the allowed options are:
sample.world - example world database
New features used in world.cxx:
As mentioned previously, pfdLoadFile can be extended to support new object formats using DSOs. pfdbWorld.cxx is an example this; it is a slight modification of loadWorld.cxx to make it useable by pfdLoadFile.
A new loader must define an external function
pfNode * pfdLoadFile_format(char *filename)where format is the file name extension for objects in the new format. It should read the file whose name is given in the argument, and return a pointer to the root of the model's scene graph. The loader should be compiled into a DSO named
libpfformat_ogl.so (for OpenGL) libpfformat_igl.so (for IrisGL)The Makefile entry worlddso shows how to create a DSO from its .o files.
pfdLoadFile(filename) searches for the DSO corresponding to filename when it is called. The default location is /usr/lib/libpfdb. You can tell Performer to also search for loader DSOs in other directories using the environment variable PFLD_LIBRARY_PATH; the value is a search path formatted like $PATH. Because the file loaders are dynamic objects which are found at run-time, existing programs can use loaders for new formats without being recompiled. For example, if you build libpfworld_ogl.so and place it in your directory ~/lib, you can view a .world file with perfly by doing:
setenv PFLD_LIBRARY_PATH ~/lib /usr/sbin/perfly foo.world
Objects can be animated (that is, moved around in the scene) using DCSs. Each object that is to be animated should be made a child of a separate DCS. The DCSs can be updated from the main loop, or by using APP traversal callbacks. anim.cxx is a very basic example which uses a DCS to move an object around on a simple path. bounce.cxx is a slightly more complex example, in that it uses pfNode::setTravData to pass application data to the traversal callback, in order to associate additional information with the animated object. The data is passed as the second argument to the callback function.
bounce2.cxx
bounceDCS.h
bounceDCS.cxx
bounce2.cxx converts the traversal callback approach of bounce.cxx to a subclassing approach, similar to the change between navobj.cxx and navclass.cxx.
Other new features used in these examples:
grab.cxx demonstrates how to allow a CAVE user to pick up and release objects (without having them jump to or from the wand). This is basically a matter of changing coordinate systems, which is done using transformation matrices. Any object which the user can grab must have a DCS, similar to the animated objects in the previous examples. grabberDCS is a subclass of DCS which handles the transformations involved in grabbing and releasing an object.
grabberDCS has two basic functions - grab and release. These functions switch the object between grabbed and un-grabbed mode. When a grabberDCS is grabbed, it computes its current transformation in "wand-space coordinates" by multiplying its world-space transformation matrix by the inverse of the wand's transformation matrix. It then continuously concatenates this wand-space transformation with the latest wand transformation to produce a new world-space transformation. When it is released, the last such transformation is kept.
Other new features used in grab.cxx:
grav.cxx
gravDCS.h
gravDCS.cxx
The gravDCS class extends the grabberDCS class further
with additional behaviour. Whenever the object is not grabbed, it
is affected by gravity, similar to the bounceDCS class previously.
The basic method for intersection testing is:
The bullet class in shoot.cxx uses an intersection test to determine
if the bullet has hit anything. It does this by forming a segment which
connects the bullet's previous and current positions (using
segset.segs[0].makePts()), and passing this to isect.
If it hits an object, it gets the point of intersection, sets its
position to that point, and stops moving. Finding the point of intersection
in world coordinates requires three steps. Calling pfHit::query
with the argument PFQHIT_POINT will find the point, but in the intersected
object's local coordinate system. The PFQHIT_XFORM query gets the
accumulated scene transformation at that point (i.e. the product of all
SCSs and DCSs between the root node of the isect test and the
intersected object). pfVec3::xformPt applies this transformation
to get the desired point in world coordinates. If you request the
normal at the intersection point, the normal should also be transformed
(using pfVec3::xformVec however, since it is a direction vector
rather than a point in space).
Another thing to note in shoot.cxx is the use of pfNode::setTravMask
for the intersection traversal. The traversal mask can be used to control
which nodes are actually tested by isect; if the bitwise AND of the
segset's isectMask and a node's traversal mask is 0, that node
and all of its children are not tested. In bullet.cxx, the bullet's
traversal mask is set to 0 (in the constructor); this guarantees that
the bullet will not collide with itself. When the bullet stops moving,
its mask is set to 0xffffffff, so that other bullets may collide with it.
A further important feature of setTravMask is the PFTRAV_IS_CACHE
flag (used in the third argument). When this flag is set, it enables
the caching of intersection test data. In many cases this will significantly
speed up the intersection tests.
Other new features used in shoot.cxx:
walk.cxx
walk.cxx demonstrates the use of intersection testing for terrain following.
The walkNavigator class functions similarly to the simpleNavigator
class, except that it controls the user's elevation (the navigated Z
coordinate), so that he is always walking on the "floor". The intersection
test in followGround() shoots a ray straight down from the current
head position. If it intersects a polygon, the user is translated to
guarantee that the foot position coincides with the point of intersection.
Performer stores all geometry data in pfGeoSet objects. In all of the
previous examples, the geometry was created by pfdLoadFile()
from model files. waves.cxx demonstrates how a program can create
its own dynamic geometry.
A GeoSet contains one or more primitives of a given type
(lines, triangle strips, polygons, etc.) A single GeoSet
may hold several lines, or several triangle strips, but it cannot contain
objects of different types.
A GeoSet includes vertex position, normal, color, and texture coordinate data.
Except for the position data, each of these is optional, and will not be
used if it is not set. They may also be defined on either a per-vertex or
per-primitive basis (e.g. you can provide individual normals for each vertex in
the object, or just one normal for the entire object).
When creating a GeoSet, you must:
A primitive lengths array (which should be allocated from shared memory)
is necessary for linestrip, tristrip, and polygon GeoSets, as the
primitives can use arbitrary numbers of vertices. For example, if
a tristrip GeoSet has 3 primitives, and lengths[0]=5,
lengths[1]=3, and lengths[2]=7, then the first
triangle strip will be formed from vertices 0 through 4 (or using
indices 0 through 4 if the GeoSet is indexed), the second strip will
be formed from vertices 5 through 7, and the third strip will be
formed from vertices 8 through 14.
A GeoSet's GeoState is used for defining other rendering information, such
as the material or texture map.
The pfLOD class implements level-of-detail switching in Performer.
Level-of-detail is an important tool for real-time performance in applications.
Objects which are far away should be drawn using simplified models; only
when an object is close enough to the viewer for all the details to be
visible should a full, complex model be drawn. With a pfLOD node, you can
provide multiple versions of a model with varying amounts of detail, and
specify the distance range at which each version should be drawn.
The program lod.cxx takes all of the objects given on the command line and
builds a single pfLOD with them (the objects should be listed in decreasing
order of detail); it sets the switching ranges at 40 foot intervals.
pfLOD::setRange(i,distance) sets the switch-in range for child #i.
When the range from the viewer to the center of the node is greater than distance,
and less than the switch-in range for child #i+1, child #i will be
drawn. Note that the "range" from the viewer to an object is equal to the distance
between them when using a 1024x1024 window with a 45 degree field of view; when the
window size or field of view is different, the range will equal the distance
multiplied by a corresponding scale factor (this is because, in theory, LOD
switching should be based on the amount of screen space covered by a model,
rather than just its distance). The field of view in the CAVE is typically
larger than 45 degrees, and so the actual switching distances will be greater
than the value given to setRange().
On Reality Engine and Infinite Reality systems, LOD fading can be used to make
the transitions between levels less abrupt. To enable LOD fading, you must
set the channels' LOD attribute PFLOD_FADE to a value greater than 0.
For example:
Intersection testing
Performer provides various tools for intersection testing -
that is, for determining whether a line intersects an object,
and where.
The primary intersection tool is the pfNode::isect function.
This tests a ray against any arbitrary node or scene graph.
It returns information on intersections via the pfHit class,
which can include the intersection position, the geode hit, the
specific triangle hit, and the normal at the point of intersection.
walkNavigator.h
walkNavigator.cxx
Creating geometry
waves.cxx
GeoSets may be indexed or non-indexed. If a GeoSet is non-indexed,
the vertex data is taken from the arrays in order. For example, if the
primitive type is triangles (PFGS_TRIS), the first three vertices
define the first triangle, the next three vertices define the second
triangle, etc. For a non-indexed GeoSet, the last argument to setAttr
is NULL. If the data is indexed, the index lists (setAttr's last
argument) indicate which vertices to use when forming the primitives.
For example, for triangles, vertices numbers ilist[0],
ilist[1], and ilist[2] define the first triangle
(i.e. the vertex positions would be verts[ilist[0]], verts[ilist[1]],
verts[ilist[2]]) (where ilist and verts are the
variable names as used in waves.cxx).
Although waves.cxx uses the exact same index list for the vertices, normals,
and colors, these can in fact have separate index lists. The only requirement
is that if one attribute is indexed, all attributes must be indexed.
Levels-of-Detail
lod.cxx
pfCAVEMasterChan()->setLODAttr(PFLOD_FADE, 10.0f);
Miscellaneous tools & code fragments
This sections contains various code fragments and shells for some basic tasks
which I have found useful in multiple applications.
Intersection testing
isect.cxx
isect.cxx contains the basic shell of an intersection testing function, handy for plugging in to an application. There are two versions of the function, with slight variations. Both return 1 if there is an intersection, 0 if there isn't one.
isectTest(node,pos,dir,&retPoint,&retNorm) takes a starting position (pos) and direction (dir) to define the intersection ray; it is given a length of 1000.0 units. The function returns both the position and the normal at the point of intersection; they are transformed into node's coordinate system.
isectTest2(node,rayPoint0,rayPoint1,&retPoint) takes two endpoints (rayPoint0 & rayPoint1) to define the ray. It only returns the position of the intersection.
createSphere(radius,color) uses the pfdu library to create a sphere of the given radius and color, with 32 polygons. It also creates a pfGeode for the sphere, and returns a pointer to the geode. This function can be easily modified to create any of the other pfdu primitives (pfdNewCone(), pfdNewCylinder, pfdNewArrow(), etc.).
It is often necessary to traverse a scene graph (or subgraph), to modify the scene, or get some global information. traverseGraph(node) is the basic shell of a graph-traversing function. When it encounters a group node, it recursively traverses each of the group's children. When it encounters a geode, it loops through all the geosets that are included in the geode (application-specific processing of the geosets would be added here). Additional isOfType() tests can be added for other classes which might require special processing, such as SCSs (just be sure you're aware of the hierarchy of Performer classes when doing this).
The pfNode::getBound() method returns the bounding sphere of a node, which
is useful for such things as determining if the wand near an object.
However, in some cases a bounding box may be preferable to a sphere.
The function getBoundingBox(node) computes the pfBox which bounds node
(and all its children). It does this by traversing the entire subtree, getting
the bounding box for each geoset, and using pfBox::extendBy() to merge all
these boxes; pfBox::xform() is used to transform the boxes by any SCSs or
DCSs which are encountered.
Note: Since writing this code, I have discovered the pfutil function
pfuTravCalcBBox(), which is supposed to do the same thing.
reflect.cxx shows how to use the pfMatrix and pfVector classes to compute the
reflection of a ray about a normal vector. This is useful for things like
bouncing a moving object off of walls (note that in this case, the ray must be
negated as well as reflected).
Non-global lighting
lightGroup.h
lightGroup.cxx
pfLightSources are the normal way of adding lights to a scene. However, these lights are global - they affect all the geometry in the scene. If you want to have lights which only affect some of your objects, you must use pfLights. pfLight is a libpr class; it is not a node class and cannot be included directly in the scene graph. To use them, you must either attach the pfLights to geostates, which are then attached to geosets, or you must turn them on and off in drawing callbacks.
lightGroup is a subclass of pfGroup which has lights attached to it. The lights are turned on in the group's pre-draw traversal, and turned off in the post-draw traversal. As a result, only nodes which are under the group in the scene graph will be affected by these lights. To use a lightGroup, create the desired pfLights, set their position, color, etc., and attach them to the group via the lightGroup::addLight() function. If you change the pre- or post-draw callbacks for the lightGroup node, make sure that your callbacks call lightGroup::lightsOn() or lightGroup::lightsOff(), respectively.
Last modified 8 March 1997.
Dave Pape, pape@evl.uic.edu