Performer by Example

A Step-by-Step Introduction to Using Performer in the CAVE

(Notes for EV Seminar given on 13 November 1996, with subsequent additions)


Recent additions: Levels-of-Detail Non-global lighting


  1. Basics
  2. Loading objects & navigating
  3. Defining a Performer class
  4. Loading a more complicated scene
  5. Animated objects
  6. Grabbing objects
  7. Intersection testing
  8. Creating geometry
  9. Levels-of-Detail
  10. Miscellaneous tools & code fragments

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.

Statistics display

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.

Makefile

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 -limage
The libraries for an IrisGL Performer program are:
-lpfdu_igl -lpfutil_igl -lpf_igl -lgl -lm -lfpe -lC -limage

Loading objects & navigating

navobj.cxx

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:

Defining a Performer class

navclass.cxx
simpleNavigator.h
simpleNavigator.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.h
perfClass.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:

object filename
Load the model filename, and make it a child of the current group.
objectsequence filepattern startframe endframe step duration
Loads a flipbook of models. The file names for the models are generated from filepattern by using it as a printf format string, with the frame number as the argument; the frame number runs from startframe to endframe, stepping by step. duration is the length of one loop through the entire sequence, in seconds. e.g. "objectsequence M%d 1 5 2 4" would load the files "M1", "M3", and "M5", and flip through them in a 4 second loop.
light x y z red green blue
Adds a LightSource to the scene, at the position (x,y,z), with the color (red,green,blue).
beginSCS xtran ytran ztran xrot yrot zrot scale
Creates a new SCS as a child of the current group; the SCS then becomes the current group, until endSCS is reached. The SCS will have a transformation consisting of a translation by (xtran,ytran,ztran), an Euler angle orientation of (xrot,yrot,zrot), and a scaling by scale.
endSCS
Pops the current SCS from the stack of groups.
beginGroup
Creates a new pfGroup node under the current group, and makes it the current group, until endGroup is reached.
endGroup
Pops the current group.
Note: endSCS and endGroup are actually interchangeable; but should be used appropriately for clarity in the database file.

sample.world - example world database

New features used in world.cxx:

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

Animated objects

anim.cxx

bounce.cxx

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:

Grabbing objects

grab.cxx
grabberDCS.h
grabberDCS.cxx

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.

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.

The basic method for intersection testing is:

  1. Create a pfSegSet describing the ray(s) to test
  2. Call node->isect
  3. If it returns true, query the pfHit(s) for the desired data

shoot.cxx
bullet.h
bullet.cxx

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
walkNavigator.h
walkNavigator.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.

Creating geometry

waves.cxx

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:

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.

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.

Levels-of-Detail

lod.cxx

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:

	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.

Creating a sphere geode

sphere.cxx

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

Traversing a scene graph

traverse.cxx

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

Computing a bounding box

boundBox.cxx

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.

Reflecting a vector

reflect.cxx

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