25.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”.

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:

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. 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( C++ | Java | .NET )). Finally we specify the number of Open Inventor render caches so that each graphics pipe will have its own display lists and texture objects.

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 25.1. The main function in the CaveHelloCone example


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

.NET
            
          


Example 25.3. The InventorDraw function in the CaveHelloCone example


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

.NET
            
          

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:

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( C++ | Java | .NET )) 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( C++ | Java | .NET ) 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. 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 25.6. The (modified) InventorDraw function in CaveReadFile example


C++
// 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);
       

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:

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( C++ | Java | .NET ). Another common implementation uses an SoCylinder( C++ | Java | .NET ). 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( C++ | Java | .NET ), stores the necessary information including wand position and orientation, and passes the event to the scene graph using an SoHandleEventAction( C++ | Java | .NET ). Otherwise, if the wand position or orientation have changed more than a tolerance value, it creates an SoTrackerEvent( C++ | Java | .NET ), 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( C++ | Java | .NET ) 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 25.8. The InventorHandleEvents function in the CaveHandleEvents example


C++
// 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, 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;
  }