XP - CAVE Application Framework, Version 0.4

The XP system, originally designed for the Megabook project, is based on a what is meant to be a fancy / flexible scene description format, combined with a class system that adds new, common features to scene graph nodes. The scene text file is intended to describe the actual Performer scene graph, hierarchically grouping the different elements. Each element in the scene file corresponds to a node of some XP class. The xpWorld class (the root node) handles most of the basic parsing of the file; individual nodes parse their attribute values in a standardized manner.

Previously, the XP nodes were subclasses of Performer nodes, multiply derived from the Performer classes and the xpNode class. In this, the latest version, there is no multiple inheritance. The xpNode classes form their own separate class hierarchy. Each xpNode creates a corresponding pfNode, and two parallel scene graphs (one XP and one Performer) are constructed.

A simple example of the XP scene file format is the following:

light(color="1 0 0",pos="-1 0 1")

Object (file="button.pfb",name="clicker",
	translate="5 3 3",rotate="30 0 -45",name="transformer")
	{
	WandTrigger(sphere="0 0 0  2",
		eventMessage="button1,switcher,toggle",
		eventMessage="button2,switcher,on",
		name="Trigger")
	}

Transform (translate="0 10 0")
	{
	Switch (name=switcher)
		{
		Object (file=page01.pfb)
		}
	}
Each node is specified by its type name (e.g. Transform) followed by a list of attribute values in parentheses (e.g. (translate="5 3 3", name="transformer") ). Even if no attributes are given, the open/close parentheses are required. For simplicity, all XP node classes are groups; a node can be followed by a pair of braces ( {} ) containing other nodes; these nodes are then children of the first node. In the above example, the first Transform is the parent of the Object and WandTrigger which follow it.

Messages

The XP class system provides for sending messages between nodes, as a way to implement many basic interactions (generally using Trigger nodes). All node classes accept character string messages via the message() member function; different classes have different messages that they recognize. In the simple example above, the WandTrigger sends the messages "toggle" and "on" to the node named "switcher"; these messages are recognized by the xpSwitch class and change the switch's state.

Events

Events are things that happen "in" a node during a frame update. For example, the user entering a trigger's volume or a switch turning on are events. Each node class has a set of pre-defined named events which it can generate. In concept, any type of node can generate events, although trigger nodes are the main event-producers. Events can be used to cause other things to happen by associating messages with them. In the scene file, you can specify an eventMessage option for a node, in the form: eventMessage="e,n,m", which means 'when event e occurs, send message m to node n'. Event messages can have an optional delay associated with them (eventMessage="e,n,m,d"); in this case, the message will be queued up to be sent d seconds after the event actually occurs.

xpNode classes

The basic xpNode class interface is:
class xpNode
	{
	public:
	 xpNode(void);
	 virtual void parseOption(char *tag,char *value);
	 virtual void postInit(void);
	 virtual void message(char *msg);
	 virtual void app(void);
	 virtual void reset(void);
	 virtual void switchOff(void);
	 virtual void addEventMessage(char *str);
	 virtual void sendMessage(struct xpEventMessage *msg);
	 virtual void appTraverse(void);
	}
These functions form the basic xpNode interface, which can be extended by each child class as required. parseOption() and postInit() are used in parsing the scene file and creating the scene graph. message() is used during run-time to send event messages between nodes (e.g. a button might send an "on" message to a light node). app() performs run-time updates on the node; it is called once per frame, in the APP process, during execution. reset() should restore a node to its initial state; this is intended for such things as "cleaning up" an active application before a new user starts through it. switchOff() is called when the node is underneath a Switch node which is being turned off; it should perform any actions needed to disable the node (for example, a Sound node will tell vss to stop playing any samples that it has started).

addEventMessage() and sendMessage() are used in parsing message descriptions during initialization, and sending them during execution, respectively. These only need to be extended by classes which send messages in non-standard ways (such as to a dynamically determined node). appTraverse() is used to recursively traverse the scene graph and call each node's app() function; it should only be overloaded in node classes which do not want to have all of their children traversed on each frame (as is done in the xpSwitch class).

Parsing nodes

The xpWorld's scene file parsing routine follows this rough outline:
  1. Read node type name
  2. Create a node of that type
  3. Add it to the scene graph
  4. Parse the comma-separated attribute options in the parentheses:
  5. Repeat 1-4 until end-of-file
  6. For each xpNode created, node->postInit()
Node type names and attribute tags are case-insensitive.

A class's parseOption(tag,value) function should check if tag is one of the attributes that it knows about. If so, it gets the value of the attribute from value. If not, it should call its parent class's parseOption() function. The base xpNode::parseOption() recognizes the "Name" and "EventMessage" attributes, so that all nodes can be assigned names and can send simple messages in response to events. The name attribute is also assigned to the corresponding Performer node.

The postInit() function is used to do any initialization which must wait until all the attributes have been set, or until the entire scene has been loaded. For example, trigger nodes will get pointers to the nodes to which they send messages at this point, searching for nodes with the names which were given in their options.

Creating a new xpNode class

This is a step-by-step description of how to create a new xpNode class, using a Slider class as an example. The Slider will be a DCS which translates between two positions, when a "start" message is received.

Slider source code: Slider.h Slider.cxx

First, copy the files proto/myNode.h and proto/myNode.cxx (or proto/mySimpleNode.h and proto/mySimpleNode.cxx); these are templates for a basic xpNode subclass. Replace all occurrences (in both files) of the name "myNode" by the name of your new class (in this case, "Slider"). If your class is going to be derived from a class other than xpNode, in this case xpTransform, replace the occurrences of "xpNode" by the parent class name (except for the return value of construct()).

Add any class-specific member data and functions. In the case of Slider, we add the positions startPos_ and endPos_, the flag active_, and the time value startTime_.

For any attributes which will be set in the scene description file, add code to parse them to parseOption(). In Slider, we add the tags "startpos" and "endpos":

	void Slider::parseOption(char *tag,char *val)
	{
	 if (!strcasecmp(tag,"startpos"))
		sscanf(val,"%f%f%f",&startPos[0]_,&startPos[1]_,&startPos[2]_);
	 else if (!strcasecmp(tag,"endpos"))
		sscanf(val,"%f%f%f",&endPos[0]_,&endPos[1]_,&endPos[2]_);
	 else
		xpTransform::parseOption(tag,val);
	}
parseOption() should always call the parent class's parseOption() if it does not recognize a given tag.

If any post-parsing initialization is required, add it to postInit(). In Slider, we set the node's translation to put it at the starting position.

	void Slider::postInit(void)
	{
	 setTrans(startPos_[0],startPos_[1],startPos_[2]);
	 xpTransform::postInit();
	}

If the node expects to receive and respond to any messages, add this code to message(). Slider accepts one message - "start"; any other messages are passed to the parent class for handling:

	void Slider::message(char *msgstring)
	{
	 if (!strcasecmp(msgstring,"start"))
		start();
	 else
		xpTransform::message(msgstring);
	}

The reset() function restores the initial state. In the Slider class, this consists of just turning off the active_ flag; the position does not need to be reset by Slider, as that is handled by the xpTransform class's reset function - it loads the DCS with the matrix it had after postInit().

	void Slider::reset(void)
	{
	 active_ = 0;
	 xpTransform::reset();
	}

The app() function is used to add actions which should happen on each frame; in this case, it will automatically update the Transform, when it is active:

	void Slider::app(void)
	{
	 if (active_)
		{
		float dt = pfGetTime() - startTime_;
		if (dt > 1.0f)
			{
			active_ = 0;
			setTrans(endPos_[0],endPos_[1],endPos_[2]);
			}
		else
			{
			pfVec3 pos = startPos_ + (endPos_ - startPos_) * dt;
			setTrans(pos[0],pos[1],pos[2]);
			}
		}
	 xpTransform::app();
	}

Once the new node class is ready, it can be added to the XP application by modifying the world class (either xpWorld.cxx, or a derived class like proto/myWorld.cxx). The header for the node class should be included at the top, myWorld::init() should be modified to call the node class's init() function, and myWorld::nodeClass() should be modified to return the class's class object when its name is encountered. Finally, modify the Makefile, adding the new class's object file to the CLASSES list, and do a "make depend".


Existing xpNode classes

Grabber

C++ class name: xpGrabber ; parent class: xpTransform
Scene file name: grabber

A transformation node which can be grabbed and moved by the wand. The node is grabbed by holding the wand within 6 inches of the node's bounding box and clicking the middle button. Once it is grabbed, it can be dropped by clicking the middle button again. Only one grabber can be grabbed at any time. When it is possible to grab a node, the node will be highlighted by being surrounded by a box or sphere.

Scene file attributes:

Messages:

Events:

Example use:

  grabber (dynamic=1, highlight=wiresphere,
           eventMessage="grab,soundNode,play")   { ... }

Light

C++ class name: xpLight ; parent class: xpNode
Scene file name: light

A point light source; the corresponding Performer node is a pfLightSource.

Scene file attributes:

Messages:

Example use:

  light (pos="-5 0 5 1", color="1 0.9 0.2", name=lamp)
  wandTrigger (sphere="-5 0 5 3", eventMessage="button1,lamp,toggle")

Node

C++ class name: xpNode
Scene file name: node or group

A generic grouping node. This is primarily useful as a parent class for new node types.

All nodes have the following attributes:

Messages:

Object

C++ class name: xpObject ; parent class: xpGrabber
Scene file name: object

A model; generally, all models used in an application should be loaded via Object nodes (or subclasses of Object).

Scene file attributes:

Example use:

  object (file=duck.pfb, grab=1, wall=0)

Path

C++ class name: xpPath ; parent class: xpNode
Scene file name: path

A linear path. Currently, paths are only used for navigator flightpaths.

Scene file attributes:

Example use:

  path (file=path1.txt, name=flightPath)
  userTrigger (sphere="100 100 4 6",
          eventMessage="enter,%navigator,followPath flightPath 5")

Point

C++ class name: xpPoint ; parent class: xpNode
Scene file name: point

A point in space. Points can be used for testing purposes, to determine where, in world coordinates, something is. They can also be used to define reference points in the scene that other nodes might use (none of the basic XP classes make use of points, though).

Scene file attributes:

Messages:

Example use:

  object (file=marker.pfb, grab=1)
    {
    point (name=Locator)
    wandTrigger (sphere="0 0 0 1", eventMessage="button1,Locator,where")
    }

PointatTrigger

C++ class name: xpPointatTrigger ; parent class: xpTrigger
Scene file name: pointattrigger

A trigger which generates events based on pointing the wand at objects which are children of the trigger. Events can occur when the wand points at or away from the objects, or when a button is pressed while pointing at the objects. A PointatTrigger does not use the sphere or box volume of the Trigger class; it instead tests for intersection of a ray from the wand with the subgraph under the trigger.

Scene file attributes:

Events:

Example use:

  pointAtTrigger (distance=6, eventMessage="button1,quack,play")
    {
    object (file=duck.pfb)
    sound (file=quack.aiff, name=quack)
    }

Scene

C++ class name: xpScene ; parent class: xpSwitch
Scene file name: scene

A node which loads in another scene file, placing that file's scene graph under the node so that it can be switched on or off.

Scene file attributes:

Example use:

  scene (file=Scene2.txt, name=Scene2, initVal=off,
         eventMessage="on,Scene2Script,call BEGIN")

Script

C++ class name: xpScript ; parent class: xpNode
Scene file name: script

A node which holds a collection of messages; all the messages (or a labeled subset of the messages) can be sent together by sending the script node a single message.

The format of a script file is one message or label per line. Comments (anything preceded by a '#' character) are ignored. A label is any string which is followed by a colon (':'). A message consists of the name of the node to send the message to, followed by the message (enclose the message in quotes if it contains whitespace), optionally followed by a delay. All messages that follow a label, up to the next label (or end of file), are associated with that label, and will be sent when that label is "call"ed. For example:

	blueSky:
		%world "sky 0 .2 1"
	# This next one changes the sky color for two seconds
	flashSky:
		%world "sky 1 1 0"
		%world "sky 0 .2 1" 2

Scene file attributes:

Messages:

Example use:

  script (file=InitScript.txt, name=InitScript)
  userTrigger (sphere="0 0 0 1000000",
               eventMessage="enter,InitScript,execute")

Selector

C++ class name: xpSelector ; parent class: xpNode
Scene file name: selector

A node which selects one of its children to be active. The active child is the only one which will be visited by the app-traversal and be rendered.

Scene file attributes:

Messages:

A child named with initchild or select must be an immediate child node of the selector node.

Example use:

  selector (name=Character, initChild=Duck)
     {
     object (file=duck.pfb, name=Duck)
     object (file=bunny.pfb, name=Bunny)
     object (file=pig.pfb, name=Pig)
     }
  wandTrigger (sphere="0 0 3 1",
               eventMessage="button1,Character,select Bunny")
  wandTrigger (sphere="-2 0 3 1",
               eventMessage="button1,Character,select Pig")

Sound

C++ class name: xpSound ; parent class: xpNode
Scene file name: sound

A positional sound source. This node will play AIFF sound samples (using bergen); it automatically updates the sample's amplitude based on the distance from the user to the sound.

Scene file attributes:

Messages:

Events:

Example use:

  object (file=duck.pfb, eventMessage="grab,Quack,play",
         eventMessage="drop,Quack,stop")
    {
    sound (name=Quack, file="quack.aiff,1.25", sphere="0 0 0 1",
           maxdistance=50, amplitude=0.2, loop=1)
    }

SoundSource

C++ class name: xpSoundSource ; parent class: xpNode
Scene file name: soundsource

A positional sound source similar to Sound, except that the sample file to play is specified with the play message, rather than as an attribute of the node. This allows it to play many different samples (although it can only play one sample at a time).

Scene file attributes:

Messages:

Events:

Example use:

  object (file=duck.pfb, eventMessage="grab,Quack,play quack.aiff 1.25",
         eventMessage="drop,Quack,stop")
    {
    sound (name=Quack, sphere="0 0 0 1", maxdistance=50, amplitude=0.2, loop=1)
    }

Switch

C++ class name: xpSwitch ; parent class: xpNode
Scene file name: switch

A basic switch node, for turning parts of the scene graph on and off.

Scene file attributes:

Messages:

Events:

Example use:

 switch (name=Switcher, initVal=on,
         eventMessage="switchon,pingSound,play",
         eventMessage="switchoff,pongSound,play")
    {
    object (file=duck.pfb)
    }
 wandTrigger (sphere="0 3 3 1", eventMessage="button1,Switcher,on",
              eventMessage="button2,Switcher,off")

Transform

C++ class name: xpTransform ; parent class: xpNode
Scene file name: transform

A static or dynamic transformation. The transformation has a translation (position), rotation (orientation), and scale (size) value. Multiple calls to translate do not accumulate translations, they merely set a new position value each time; rotate and scale act similarly.

Scene file attributes:

Messages:

Example use:

 transform (translate="0 10 6", rotate="60 0 -10", scale=1.3)
    {
    object (file=duck.pfb)
    }

Trigger

C++ class name: xpTrigger ; parent class: xpNode

Abstract parent class for various trigger classes. This class maintains a volume which the trigger occupies. The derived classes handle determining when the different events occur.

Trigger event-messages are specified in the scene file in the form: "nodename,message,delay", where the delay is optional. The message string message is sent to the scene node named nodename when the corresponding event occurs; any number of messages may be associated with a given event. If delay is given, the trigger waits delay seconds from when an event occurs until it actually sends the message.

Scene file attributes:

Messages:

Note: if debug mode is enabled, a trigger will draw a wireframe sphere or box showing the outline of its volume. The trigger subclasses generally change the outline's color to red when the head or wand (depending on the class) is inside the volume, and white otherwise. The outline will be blue if the trigger is disabled. In debug mode, triggers will also print messages indicating when any events occur, and when messages are sent.

UserTrigger

C++ class name: xpUserTrigger ; parent class: xpTrigger
Scene file name: usertrigger

A trigger which generates events based on the user's head position. Events can occur when the user enters or exits the trigger volume.

Events:

Example use:

 userTrigger (box="-10 2 0  0 5 15",
              eventMessage="enter,bgSound,play",
              eventMessage="exit,bgSound,stop")

WandTrigger

C++ class name: xpWandTrigger ; parent class: xpTrigger
Scene file name: wandtrigger

A trigger which generates events based on the wand position and buttons. Events can occur when the wand enters or exits the trigger volume, or when a button is pressed while the wand is inside the volume.

Events:

Example use:

 wandTrigger (sphere="3 3 3 2",
              eventMessage="enter,buzzSound,play",
              eventMessage="exit,bgSound,stop",
              eventMessage="button1,lampLight,on",
              eventMessage="button3,lampLight,off")


Special Nodes

There are four special nodes which are created automatically by the program, rather than being listed in the scene file. They are the world, navigator, user, and wand nodes. Messages can be sent to these nodes by using their predefined names - "%world", "%navigator", "%user", and "%wand", respectively (the wand and user nodes do not currently recognize any messages, however).

World

The world node is the root of the scene graph; it is a node of class xpWorld. The world node is created by the main program. It in turn takes care of creating the rest of the scene, by parsing the scene file. It also handles global attributes such as the background (sky) color, fog, clipping distances, and backface culling, and assists in message passing between nodes by maintaining the queue of messages which are to be sent.

Since the world node does the parsing of the scene file, if you wish to add new node classes, their parsing must be added to the world. This can be done by creating a subclass of xpWorld which handles just your own node classes, and passes everything else on to xpWorld; see the prototype myWorld.cxx for an example.

Messages accepted by xpWorld:

The world node's name (for sending messages to) is %world.

Navigator

All navigation features are handled by an xpNavigator node. This node is created by xpWorld, and is a child of the root xpWorld node. The navigation does not use a DCS - the navigation transformation is loaded into the Performer channels' view matrices; this means that the matrix returned by a node's pfTraverser (in the APP and DRAW callbacks) defines a transformation between local and world coordinates, rather than tracker coordinates.

The navigator includes features for path following, collision detection, terrain following, and teleporting. It can be in one of two main modes - following a predefined path (an xpPath), or under user control. When under user control, terrain following and collision detection can be used; by default, both are on. The navigation can also be attached to a node in the scene graph, to cause the user to move with a Transform in the scene. These features, as well as the speed of user-controlled movement, can be controlled by sending messages to the navigator node (its name is %navigator).

Messages accepted by xpNavigator:

Keyboard controls

The navigator has the following keyboard controls:

User

Data for the tracked user's head is accessed through the xpUser node. The world node automatically creates this node at startup. Program code can get the pointer to the user node via xpWorld::user().

The xpUser class has the following functions:

Wand

The wand tracking, button, and joystick data is accessible through the xpWand node. The node also keeps track of the currently grabbed object, if there is one. The world node automatically creates this node at startup. Program code can get the pointer to the wand node via xpWorld::wand().

The xpWand class has the following functions:


Debug mode

The XP classes all have access to a global flag variable xpDebug. The generic main program sets xpDebug to true if the environment variable XPDEBUG is defined (i.e. setenv XPDEBUG before running the program). When debug mode is enabled, some of the classes print additional information during run time, such as when significant events occur.

Specifically, when debug mode is enabled: