26.4. Programming with CAVELib

CAVELib is a widely used Application Programming Interface (API) for
developing immersive applications. Some of the items that CAVELib abstracts for a developer are
window and viewport creation, viewer-centered perspective calculations, displaying to multiple
graphics channels, multi-processing and multi-threading, cluster synchronization and data
sharing, and stereoscopic viewing. CAVELib-based applications are externally configurable at run
time, making the application executable independent of the display system. So, without
recompilation, the application can be run on a wide variety of display systems. CAVELib’s
cross-platform API, combined with Open Inventor’s cross-platform API, makes it possible to
maintain a single code base that runs on a variety of machines and operating systems.

In the following examples we will assume some basic knowledge of Open
Inventor programming and try to highlight the techniques that are specific to using Open
Inventor with the CAVELib API. Most Open Inventor programming experience, for example creating
and modifying the scene graph, is equally applicable to desktop and immersive environments. Some
knowledge of the CAVELib and OpenGL programming interfaces will be helpful in understanding the
examples. The source code for these examples can be found in the SDK directory “
$OIVHOME/src/Inventor/contrib/ImmersiveVR/CAVELib”.

Display a Cone with CAVELib

In the first example located in “
$OIVHOME/src/Inventor/contrilb/ImmersiveVR/CAVELib/CaveHelloCone.cxx)”, we create some very
simple geometry so we can focus on the necessary setup for using Open Inventor with CAVELib.
This example is conceptually similar to Example 1 in Chapter 2 of the Inventor
Mentor. It shows how to:

Create a simple Open Inventor scene graph. See function main().

Do Open Inventor global (one time) initialization. See function InventorInit().

Do Open Inventor per-thread initialization. See function InventorThreadInit().

Render the Open Inventor scene graph. See function
InventorDraw().

The application’s main() function follows
the usual pattern for using the multi-threaded CAVELib. First we configure CAVELib and
initialize Open Inventor, then we create the scene graph, then we specify the thread init and
draw functions, then we start the CAVELib render loop. CAVELib will create and begin execution
of a render thread for each graphics pipe.

The Open Inventor global initialization function
InventorInit() is called exactly once, from the application’s main function. We
initialize Open Inventor and any extensions that are used by the application. Because we are
using multi-threaded rendering, it is important to use the multi-thread initialization methods,
for example SoDB::threadInit(). We create a simple data
structure to keep some information that is specific to each graphics pipe. For example, each
graphics pipe should have its own Open Inventor render action (SoGLRenderActionSoGLRenderActionSoGLRenderAction ). Finally we specify the number of Open Inventor render caches so
that each graphics pipe will have its own display lists and texture objects.

The Open Inventor thread initialization function
InventorThreadInit() will be called exactly once from each render thread. In this
function we must repeat the Open Inventor initialization calls (for example SoDB::threadInit()) to ensure that Thread Local Storage is set up for
each thread. We create the Open Inventor render action for the current thread and store its
address in the data structure we created in the global initialization function. We set the pipe
number as the cacheContext id for each render action to ensure
that separate display lists and texture objects are created for each pipe.

The Open Inventor render function InventorDraw() will be called one or more times from each render thread for each
frame. The main goal is to transfer information from the CAVELib state into the Open Inventor
traversal state, then apply the appropriate render action to the scene graph. This traversal
state setup would typically be handled by a viewer class in a desktop Open Inventor
application. Open Inventor tracks the OpenGL state and normally expects the OpenGL state to
remain unchanged between render traversals. This allows us to optimize by avoiding unnecessary
state setting. However, in a CAVELib application other parts of the application (and CAVELib
itself, in simulator mode) are using the same OpenGL context and may alter the OpenGL state. In
this example we “push” the OpenGL state and reset Open Inventor’s record of the OpenGL state,
then restore the OpenGL state with a “pop” after rendering.

Example 26.3. The InventorThreadInit function in the CaveHelloCone
example

// Open Inventor per-thread initialization// (CAVELib will call this exactly once from each render thread)//void
InventorThreadInit()
{
// Setup thread local storage for this thread
SoDB::threadInit();
// Create a render action for this thread
SbViewportRegion vp;
SoGLRenderAction *action = new SoGLRenderAction(vp);
// Assume CAVELib does not setup display list sharing,// So each wall's render action should have a different cache context.int id = CAVEPipeNumber();
action->setCacheContext(id);
// Store the render action to use later
oivPipeInfo[id].renderAction = action;
}

Read and Display an Inventor File with CAVELib

This example located in “ $OIVHOME/src/Inventor/contrib/ImmersiveVR/CAVELib/CaveReadFile.cxx)” is conceptually similar to Example 1 in Chapter 11 of the Inventor Mentor. It shows how to:

Read an Open Inventor or VRML file.

See new function: InventorReadFile().

Implement a “ headlight”.

A headlight is a directional light that always points in the direction you are looking. This is convenient for simple scenes.

See new function: InventorAddHeadlight().

Scale and translate a scene to fit inside the CAVE.

Here we simply add some transforms to the Inventor scene graph. You could use CAVELib’s “nav” transforms or something different.

See new function: InventorFitScene().

Update Open Inventor’s “clock” before each frame.

This allows time sensors and animation engines to work. Also updates the headlight direction.

See new function: InventorFrameUpdate().

Initialize Open Inventor’s traversal state with the view and projection matrices computed by CAVELib.

This allows view-dependent nodes like Level of Detail (LOD) and Billboard to work correctly.

See new code in function: InventorDraw().

The source code for the InventorReadFile function is the same as the Mentor example. There is no CAVELib specific code in this function, so it is not reproduced here.

The InventorAddHeadlight() function is not really CAVELib specific either, but it shows a useful technique, similar to what is done in the Open Inventor viewer classes. We will make the “headlight” (a directional light source) always point in the direction we are looking, by updating the rotation matrix at the beginning of each frame. You may wish to use a different lighting setup, for example, positioning one or more point light sources (SoPointLightSoPointLightSoPointLight ) in the scene for illumination.

The InventorFitScene() function implements one of many possible (simple) strategies for scaling the scene to fit inside a specific 3D “box,” in this case the inside of a standard CAVE. In this example the necessary transforms are added to the Open Inventor scene graph, but you may wish to use CAVELib’s “nav” transform, or some other technique. Computing scale factors is not really specific to CAVELib, so this function is not reproduced here.

The InventorFrameUpdate() function will be called exactly once by each render thread, before rendering each frame. We specify this in main() using CAVELib’s CAVEFrameFunction(). We only need one render thread to update the clock, so we call CAVEMasterDisplay(). This function will return TRUE in exactly one of the render threads. All other threads will immediately enter a CAVEDisplayBarrier and wait for the master thread. The master thread will first update Open Inventor’s global realTime field with the current time. This allows time-based sensors and engines in the Open Inventor scene graph to function properly. Next the master thread updates the headlight direction with the current view direction. Finally the master thread enters the display barrier, releasing all the render threads to begin rendering the frame.

The InventorDraw() function has been updated for this example. Look for the comment string “BEGIN NEW CODE FOR THIS EXAMPLE”. CAVELib computes the necessary viewing and projection matrices, based on head tracking if enabled, and passes those matrices to OpenGL before the draw function is called. To simply traverse and render geometry we only need to apply the SoGLRenderActionSoGLRenderActionSoGLRenderAction to the scene graph, as in the previous example. However some very useful Open Inventor nodes depend on knowing the position and direction of the virtual camera or viewer. These nodes include Level of Detail (SoLODSoLODSoLOD ), Billboard (SoBillboardSoBillboardSoBillboard ), and ProximitySensor (SoVRMLProximitySensorSoVRMLProximitySensorSoVRMLProximitySensor ). To allow these nodes to work correctly, we query the view information and matrices from CAVELib and assign values to the corresponding elements in the Open Inventor traversal state. Note the last parameter of FALSE on some of the set calls. This tells Open Inventor that the information has already been sent to OpenGL and should not be sent again.

// Create a headlight similar to an Open Inventor viewer// Rotation will be updated on each frame so the light shines in the// direction we are looking. This is a useful lighting for looking at// objects, but you might prefer to insert point lights.void
InventorAddHeadlight(SoSeparator *sceneRoot)
{
// Headlight must be in a Group, not a Separator, or the// light will not affect the rest of the scene graph.
SoGroup *pHeadlightGroup = new SoGroup(3);
SoDirectionalLight *pHeadlightNode = new SoDirectionalLight;
pHeadlightNode->direction.setValue(SbVec3f(.2f, -.2f, -.9797958971f));
// NOTE: This is the global variable updated in InventorFrameUpdate
m_headlightRotation = new SoRotation;
// ResetTransform node prevents rotation from affecting scene graph
pHeadlightGroup->addChild(m_headlightRotation);
pHeadlightGroup->addChild(pHeadlightNode);
pHeadlightGroup->addChild(new SoResetTransform);
sceneRoot->addChild(pHeadlightGroup);
}

Most of the code is the same as the previous example. The InventorAddWand() and InventorUpdateWand() functions show one way to implement a simple virtual pointer geometry for the wand (or other) tracked input device. This implementation uses an SoLineSetSoLineSetSoLineSet . Another common implementation uses an SoCylinderSoCylinderSoCylinder . The code is straightforward and is not reproduced here.

The InventorFrameUpdate() function has several important modifications in this example. The first (not shown here) is an “ifdef” that allows the Open Inventor clock to be updated using the CAVETime global variable rather than the operating system clock. This can be important for synchronizing multiple render processes in a cluster, network, or collaboration environment. The second modification is a call to the new function InventorHandleEvents(), which will create Open Inventor event objects based on the wand position, orientation, and controller buttons. Finally we check if any sensors have been added to the Open Inventor timer queue or delay queue and need to be processed. (This is work that is normally done by the Open Inventor viewer class in a desktop application.) Processing these queues is important for correct operation of some Open Inventor applications.

The InventorHandleEvents() function is called from InventorFrameUpdate(). If there has been a change in the state of a wand controller button, it creates an SoControllerButtonEventSoControllerButtonEventSoControllerButtonEvent , stores the necessary information including wand position and orientation, and passes the event to the scene graph using an SoHandleEventActionSoHandleEventActionSoHandleEventAction . Otherwise, if the wand position or orientation have changed more than a tolerance value, it creates an SoTrackerEventSoTrackerEventSoTrackerEvent , stores the necessary information and passes that event to the scene graph. Note that this example uses the wand interface functions built into CAVELib, but it could easily use the trackd™ library directly. Various nodes in the scene graph may respond to these events. For example, an SoSelectionSoSelectionSoSelection node will respond to press and release of controller button 1 in the same way it responds to press and release of mouse button 1 in a desktop application. Draggers will respond to both button events and motion events. Note that tracked input devices are typically polled continuously for their current value, while Open Inventor expects discrete events indicating a significant change in value. Therefore we use a tolerance value when comparing the wand position and orientation because we do not want to process events when nothing is really happening.

Example 26.8. The (modified) InventorFrameUpdate function in the CaveHandleEvents example

// Check for wand changes that should trigger Inventor events.// (sceneRoot is needed to apply the HandleEventAction)
InventorHandleEvents(sceneRoot);
// If application has any time sensors, make this call to process them.
SoDB::getSensorManager()->processTimerQueue();
// In case there are tasks scheduled in the delay queue or idle queue,// make this call. Note SoVRMLTimeSensor nodes that are activated and// deactivated will not reset until the idle queue is processed.
SoDB::getSensorManager()->processDelayQueue(TRUE);

Example 26.9. The InventorHandleEvents function in the CaveHandleEvents example

Thermo Fisher Scientific is the leading provider of advanced 3D visualization and analysis software tools for developers, engineers and scientists in natural resources, medical and life sciences, and engineering.