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:

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

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

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

  4. 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 (SoGLRenderAction SoGLRenderAction SoGLRenderAction ). 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.1. The main function in the CaveHelloCone example

int
main(int argc, char **argv)
{
#ifndef WIN32
  // UNIX specific initialization
  XInitThreads();
#if defined(sun)
  glXInitThreadsSUN();
#endif
#endif

  // Configure CAVELib (reads config files etc)
    CAVEConfigure(NULL, NULL, NULL);

  // Global app initialization (before starting render threads)
    InventorInit(argc, (const char**)argv);

  //-----------------------------------------------------------------
  // Make the scene graph root node
    SoSeparator *sceneRoot = new SoSeparator;
    sceneRoot->ref();

  // Move the geometry up a little so we can see it
    SoTransform *myTranslate = new SoTransform;
    myTranslate->translation.setValue(SbVec3f(0,5,-3));

  // Make a red cone
    SoMaterial *myMaterial = new SoMaterial;
                                                // Red
    myMaterial->diffuseColor.setValue(1.0, 0.0, 0.0);

  // Make a light so we can see the geometry
    sceneRoot->addChild(new SoDirectionalLight);

  // Add the nodes to the scene graph
    sceneRoot->addChild(myTranslate);
    sceneRoot->addChild(myMaterial);
    sceneRoot->addChild(new SoCone);
  //-----------------------------------------------------------------

  // Set render thread init callback
  // (will be called exactly once for each render thread)
    CAVEInitApplication((CAVECALLBACK)InventorThreadInit, 0);

  // Set render thread draw callback
  // (each render thread will call one or times per frame for drawing)
  // Pass the root of the scene graph to be drawn
    CAVEDisplay((CAVECALLBACK)InventorDraw, 1, sceneRoot);

  // Init CAVELib (starts the rendering threads etc)
    CAVEInit();

  // Loop until user quits
    while(!CAVEgetbutton(CAVE_ESCKEY))
    {
    SLEEP(50);      // Don't use all the cpu time doing nothing
    }

  CAVEExit();
    return 0;
  }
       
            
          


Example 26.2. The InventorInit function in the CaveHelloCone example

// Open Inventor global initialization
// (called before render threads are created)

void
InventorInit(int argc, const char **argv)
{
    // Get number of pipes (rendering threads)
      int numPipes = CAVENumPipes();

    // Allocate per-pipe (per render thread) data
      oivPipeInfo = new oivPipeInfo_t[numPipes];
      memset(oivPipeInfo, 0, numPipes*sizeof(oivPipeInfo_t));

    // Initialize Open Inventor
    // Be sure to call threadInit so MT support is enabled
      SoDB::threadInit();

    // Render caching
    // Set to 0 to disable render caching
    // Else set to (at least) number of pipes
    // (because pipes usually cannot share display lists)
      if (numPipes > SoDB::getNumRenderCaches())
      SoDB::setNumRenderCaches(numPipes);
}
       
            
          


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;
}
       
            
          


Example 26.4. The InventorDraw function in the CaveHelloCone example

// Open Inventor frame draw
// (CAVELib will call this function one or more times per frame from
// each render thread)
//
void
InventorDraw(SoSeparator *sceneRoot)
{
      int id = CAVEPipeNumber();

    // Push all OpenGL attributes
    // (so Open Inventor will not affect objects drawn by CAVELib)
      glPushAttrib(GL_ALL_ATTRIB_BITS);

    // Clear window
      glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

    // Get the Open Inventor render action for this thread.
      SoGLRenderAction *pAction = oivPipeInfo[id].renderAction;

    // Viewport
    // SoGLRenderAction always sets the SoViewportRegionElement.
    // Always set up the viewport correctly (both window size
    // and actual viewport size) in case the size has changed.
      int vpx,vpy,vpwidth,vpheight;
      int wx,wy,wwidth,wheight;
      CAVEGetViewport(&vpx, &vpy, &vpwidth, &vpheight);
      CAVEGetWindowGeometry(&wx, &wy, &wwidth, &wheight);
      SbViewportRegion vp(wwidth, wheight);       // Initialize with window size
      vp.setViewportPixels(vpx, vpy, vpwidth, vpheight);
      pAction->setViewportRegion(vp);

    // Open Inventor traversal state
    // First make sure the traversal state has been created, so we can
    // pre-load some state elements with values already set by CAVELib.
      SoState *pState = pAction->getState();
      if (pState == NULL)
      {
      pAction->setUpState();
        pState = pAction->getState();
      }

    // Traversal state Part 1: Material properties
    //
    // During traversal Open Inventor tracks the OpenGL state to avoid
    // unnecessary attribute setting. However it also remembers OpenGL
    // state between traversals, which is not valid here because we're
    // pushing and popping the OpenGL state. Reset the state tracker.
      SoGLLazyElement *pLazyElem = SoGLLazyElement::getInstance(pState);
      pLazyElem->reset(pState, SoLazyElement::ALL_MASK);

    // Render
      pAction->apply(sceneRoot);

    // restore the OpenGL attributes
      glPopAttrib();
}
       
            
          


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 (SoPointLight SoPointLight SoPointLight ) 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 SoGLRenderAction SoGLRenderAction SoGLRenderAction 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 (SoLOD SoLOD SoLOD ), Billboard (SoBillboard SoBillboard SoBillboard ), and ProximitySensor (SoVRMLProximitySensor SoVRMLProximitySensor SoVRMLProximitySensor ). 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.

Example 26.5. The InventorAddHeadlight function in CaveReadFile example

// 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);
  }
       


Example 26.6. The InventorFrameUpdate function in CaveReadFile example

// Open Inventor frame update
// (CAVELib will call this exactly once per frame from each render thread)
//
void
  InventorFrameUpdate()
  {
  if (CAVEMasterDisplay())
    {
    // Update Open Inventor's "realtime" (so sensors/engines will work)
      SoSFTime *realTime = (SoSFTime *) SoDB::getGlobalField("realTime");
      realTime->setValue(SbTime::getTimeOfDay());

    // Update the headlight rotation
    // (compute the rotation from default light direction to view direction)
      float eyevec[3];
      CAVEGetVector(CAVE_HEAD_FRONT, eyevec);

    // NOTE: This is the global variable initialized in InventorAddHeadlight
      SbRotation camrot(SbVec3f(0,0,-1),SbVec3f(eyevec[0], eyevec[1], eyevec[2]));
      m_headlightRotation->rotation.setValue(camrot);

    // Sync with other display threads
      CAVEDisplayBarrier();
    }
  else
    {
    // Non-master threads must wait for master to do its thing
    CAVEDisplayBarrier();
    }
  }
       


Example 26.7. The (modified) InventorDraw function in CaveReadFile example

// Traversal state Part 2: View volume
//
// Get the head position, orientation and projection info from CAVELib.
// Then set the Inventor view volume element using the info from CAVELib.
//
// Note: Open Inventor's render caching algorithm needs to track which
//       node set each element. Since we're setting at the top of the
//       scene graph, we'll pretend it was "sceneRoot" that did it.
  float xeye,yeye,zeye;
  float eyevec[3];
  float frustum[6];
  float viewmat[4][4];
  CAVEGetEyePosition(CAVEEye, &xeye, &yeye, &zeye);
  CAVEGetVector(CAVE_HEAD_FRONT, eyevec);
  CAVEGetProjection(CAVEWall, CAVEEye, frustum, viewmat);

  SbRotation camrot(SbVec3f(0,0,-1),SbVec3f(eyevec[0], eyevec[1], eyevec[2]));
  SbViewVolume viewVol;
  viewVol.frustum(frustum[0], frustum[1], frustum[2], frustum[3], frustum[4],
        frustum[5]);
  viewVol.rotateCamera(camrot);
  viewVol.translateCamera(SbVec3f(xeye, yeye, zeye));
  SoViewVolumeElement::set(pState, sceneRoot, viewVol);

// Traversal state Part 3: ModelView and Projection matrices
//
// CAVELib has already sent ModelView and Projection matrices to OpenGL.
// Initialize Inventor's current matrix elements with the current matrices.
  SbMatrix viewMatrix, projMatrix;
  glGetFloatv(GL_MODELVIEW_MATRIX, (float*)viewMatrix);
  glGetFloatv(GL_PROJECTION_MATRIX, (float*)projMatrix);
  SoViewingMatrixElement::set(pState, sceneRoot, viewMatrix, FALSE);
  SoProjectionMatrixElement::set(pState, sceneRoot, projMatrix, FALSE);

// Render caching
// Since we're pretending that sceneRoot set the traversal state elements
// above, the render caching algorithm must think this node has changed.
  sceneRoot->touch();

// Normal vectors
// If input file doesn't have unit vectors we'll get screwy lighting.
// Open Inventor automatically enables GL_NORMALIZE, but only once.
// Since we're inside a push/pop, we'll have to do it every time.
  glEnable(GL_NORMALIZE);
       


Handle Events with CAVELib

This example located in $OIVHOME/src/Inventor/contrib/ImmersiveVR/CAVELib/CaveHandleEvents.cxx is conceptually similar to Examples 1 through 3 in Chapter 15 of the Inventor Mentor. It shows how to:

  1. Implement a “pointer” for the wand input device.

    See new function: InventorAddWand().

  2. Update the wand pointer geometry when the wand moves.

    See new function: InventorUpdateWand().

  3. Update the clock using CAVETime and process the sensor queues.

    See new code in function InventorFrameUpdate().

  4. Convert changes in wand state into Inventor events.

    When a button is pressed we generate an SoControllerButtonEvent SoControllerButtonEvent SoControllerButtonEvent .

    When the wand moves we generate an SoTrackerEvent SoTrackerEvent SoTrackerEvent .

    See new function: InventorHandleEvents().

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 SoLineSet SoLineSet SoLineSet . Another common implementation uses an SoCylinder SoCylinder SoCylinder . 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 SoControllerButtonEvent SoControllerButtonEvent SoControllerButtonEvent , stores the necessary information including wand position and orientation, and passes the event to the scene graph using an SoHandleEventAction SoHandleEventAction SoHandleEventAction . Otherwise, if the wand position or orientation have changed more than a tolerance value, it creates an SoTrackerEvent SoTrackerEvent SoTrackerEvent , 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 SoSelection SoSelection SoSelection 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

// Open Inventor event handler
// (InventorFrameUpdate will call this function to handle events)
// In this example we only check for button1 changes (because we know
// that's the only thing SoSelection, draggers, SoVRMLTouchSensor, etc
// actually care about). The code is easily extended to more devices.

  void
  InventorHandleEvents(SoSeparator *sceneRoot)
  {
    static SbVec3f lastWandPos(-1,-1,-1);
    static SbVec3f lastWandOri(-1,-1,-1);       // Really 3 Euler angles!

  // Define tolerance for detecting change in wand position/orientation
    const float tolerance = 0.00001f;

  // Get change in wand button state (if any)
    int btn1 = CAVEButtonChange(1);

  // Get current wand tracker info
    SbVec3f wandPos, wandOri;
    CAVEGetPosition (CAVE_WAND, (float*)&wandPos);
                                                // Euler angles
    CAVEGetOrientation(CAVE_WAND, (float*)&wandOri);
    wandOri *= M_PI/180.;                       // Degrees to radians

  // Check if button 1 state changed since last time
  // 0 means no change, 1 means pressed, -1 means released
    if (btn1 != 0)
    {

    // Create a controller button change event
    // Store which button was pressed and the new state
    // Also store tracker info in case Inventor needs to do picking
    SoControllerButtonEvent *pEvent = m_buttonEvent;
      pEvent->setTime(SbTime::getTimeOfDay());
      pEvent->setButton(SoControllerButtonEvent::BUTTON1);
      pEvent->setState((btn1 > 0) ? SoButtonEvent::DOWN : SoButtonEvent::UP);
      pEvent->setPosition3 (wandPos);
      pEvent->setOrientation(wandOri[0], wandOri[1], wandOri[2]);

    // Send the event to the scene graph
      m_handleEventAction->setEvent(pEvent);
      m_handleEventAction->apply(sceneRoot);
    }

  // Check for significant change in wand position/orientation
  else if (! wandPos.equals(lastWandPos, tolerance) ||
    ! wandOri.equals(lastWandOri, tolerance))
    {

    // Create a tracker change event
    // Store the tracker info
    SoTrackerEvent *pEvent = m_trackerEvent;
      pEvent->setTime(SbTime::getTimeOfDay());
      pEvent->setPosition3 (wandPos);
      pEvent->setOrientation(wandOri[0], wandOri[1], wandOri[2]);

    // Send the event to the scene graph
      m_handleEventAction->setEvent(pEvent);
      m_handleEventAction->apply(sceneRoot);

    // Also update the wand pointer geometry
      InventorUpdateWand();
    }

  // Remember the current tracker values
    lastWandPos = wandPos;
    lastWandOri = wandOri;
  }