OpenGL Applications

We can now describe how to write OpenGL applications in VR Juggler. An OpenGL-based VR Juggler application must be derived from vrj::GlApp. This in turn is derived from vrj::App. As was discussed in the application object section, vrj::App defines the base interface that VR Juggler expects of all applications. The vrj::GlApp class extends this interface by adding members that the VR Juggler OpenGL Draw Manager needs to render an OpenGL application correctly.

Figure 4.2. vrj::GlApp application class

vrj::GlApp application class

In Figure 4.2, “vrj::GlApp application class”, we see some of the methods added by the vrj::GlApp interface: draw(), contextInit(), and contextPreDraw(). These methods deal with OpenGL drawing and managing context-specific data (do not worry what context data is right now—we cover that in detail later). There are a few other member functions in the interface, but these cover 99% of the issues that most developers face. In the following sections, we will describe how to add OpenGL drawing to an application and how to handle context-specific data. There is a tutorial for each topic.

Clearing the Color and Depth Buffers

Before describing how to render using OpenGL with VR Juggler, we must cover the more basic topic of clearing the color and depth buffers. We describe this part before explaining how to render graphics because these steps will be common to all VR Juggler applications based on OpenGL.

In VR Juggler 1.1 and beyond, there is support for drawing multiple OpenGL viewports in a single VR Juggler display window. This feature is useful for tiled displays where each viewport renders a specific part of the scene. In order for an OpenGL-based application to work with multiple viewports, the color and depth buffers need to be cleared at the correct times.

In a user application, the method vrj::GlApp::bufferPreDraw() is overridden so that it clears the color buffer. For example, the following code clears the color buffer using black:

void userApp::bufferPreDraw()
{
   glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
   glClear(GL_COLOR_BUFFER_BIT);
}

Now we need to clear the depth buffer. This must be done separately from the color buffer to ensure proper stereo rendering. The depth buffer must be cleared in the application object's draw() method, usually as the first step:

void userApp::draw()
{
   glClear(GL_DEPTH_BUFFER_BIT);

   // Rendering the scene ...
}

OpenGL Drawing: vrj::GlApp::draw()

The most important (and visible) component of most OpenGL applications is the OpenGL drawing. The vrj::GlApp class interface defines a draw() member function to hold the code for drawing a virtual environment. Hence, any OpenGL drawing calls should be placed in the vrj::GlApp::draw() function of the user application object.

Adding drawing code to an OpenGL-based VR Juggler application is straightforward. The draw() method is called whenever the OpenGL Draw Manager needs to render a view of the virtual world created by the user's application. It is called for each defined OpenGL context, and it may be called multiple times per frame in the case of multi-surface setups and/or stereo configurations. Applications should never rely upon the number of times this member function is called per frame.

When the method is called, the OpenGL model view and projection matrices have been configured correctly to draw the scene. Input devices are guaranteed to be in the same state (position, value, etc.) for each call to the draw() method for a given frame.

Recommended Uses

The only code that should execute in this function is calls to OpenGL drawing routines. It is permissible to read from input devices to determine what to draw, but application data members should not be updated in this function.

Possible Misuses

The draw() method should not be used to perform any time-consuming computations. Code in this member function should not change the state of any application variables.

Tutorial: Drawing a Cube with OpenGL

Table 4.2. Tutorial Overview

DescriptionSimple OpenGL application that draws a cube in the environment
ObjectivesUnderstand how the draw() member function in vrj::GlApp works; create basic OpenGL-based VR Juggler applications
Member functions
  • vrj::App::init()

  • vrj::GlApp::draw()

Directory$VJ_BASE_DIR/share/samples/OGL/simple/SimpleApp
Files
  • simpleApp.h

  • simpleApp.cpp

Class Declaration

The following application class is called simpleApp. It is derived from vrj::GlApp and has custom init() and draw() methods declared. Note that the application declares several device interface members that are used by the application for getting device data.

  1 using namespace vrj;
    using namespace gadget;
    
    class simpleApp : public GlApp
  5 {
    public:
       simpleApp();
       virtual void init();
       virtual void draw();
 10 
    public:
       PositionInterface mWand;
       PositionInterface mHead;
       DigitalInterface mButton0;
 15    DigitalInterface mButton1;
    };

The draw() Member Function

The implementation of draw() is located in simpleApp.cpp. Its job is to draw the environment. A partial implementation follows.

  1 using namespace gmtl;
    
    void simpleApp::draw()
    {
  5    ...                                                     (1)
       // Create box offset matrix
       Matrix44f box_offset;		
       const EulerAngleXYZf euler_ang(Math::deg2Rad(-90.0f), Math::deg2Rad(0.0f),
                                      Math::deg2Rad(0.0f));
 10    box_offset = gmtl::makeRot<Matrix44f>(euler_ang);
       gmtl::setTrans(box_offset, Vec3f(0.0, 1.0f, 0.0f));     (2)
       ...
       glPushMatrix();                                         (3)
          // Push on offset
 15       glMultMatrixf(box_offset.getData());
          ...
          drawCube();
       glPopMatrix();
       ...
 20 }
1

This creates a gmtl::Matrix44f object that defines the offset of the cube in the virtual world.

2

The new matrix is pushed onto the OpenGL modelview matrix stack.

3

Finally, a cube is drawn.

In the above, there is no projection code in the function. When the function is called by VR Juggler, the projection matrix has already been set up correctly for the system. All the user application must do is draw the environment; VR Juggler handles the rest. In this example, the draw() member function renders a cube at an offset location.

Exercise

Change the code so that the cube is drawn at the position of the wand instead of at the box_offset location.

Context-Specific Data

Many readers may already be familiar with the specifics of OpenGL. In this section, we provide a very brief introduction to context-specific data within OpenGL, and we proceed to explain how it is used by VR Juggler. Those who are already familiar with context-specific data may skip ahead to the section called “Why it is Needed” or to the section called “Using Context-Specific Data”.

The OpenGL graphics API operates using a state machine that tracks the current settings and attributes set by the OpenGL code. Each window in which we render using OpenGL has a state machine associated with it. The state machines associated with these windows are referred to as OpenGL rendering contexts.

Each context stores the current state of an OpenGL renderer instance. The state includes the following:

  • Current color

  • Current shading mode

  • Current texture

  • Display lists

  • Texture objects

Why it is Needed

As outlined in the VR Juggler architecture documentation, VR Juggler uses a single memory area for all application data. All threads can see the same memory area and thus share the same copy of all variables. This makes programming normal application code very easy because programmers never have to worry about which thread can see which variables. In the case of context-specific data, however, it presents a problem.

To understand the problem, consider an environment where we use a single display list. That display list is created to draw some object in the scene. We would like to be able to call the display list in our draw() method and have it draw the primitives that were captured in it.

The following class skeleton shows an outline of this idea. Do not worry for now that we do not show the code where we allocate the display list—that will be covered later. For now, we see that there is a variable that stores the display list ID (mDispListId), and we use it in the draw() method.

using namespace vrj;

class userApp : public GlApp
{
public:
  draw();
public:
  int mDispListId;
};

userApp::draw()
{
   glCallList(mDispListId);
}

Now, imagine that we have a VR system configured that needs more than one display window (a multi-wall projection system, for example). There is a thread for each display, and all the display threads call draw() in parallel.

Since all threads share the same copy of the variables, they all use the same mDispListId when calling glCallList(). This is an error because we call draw from multiple windows (that is, multiple OpenGL rendering contexts). The display list ID is not the same in each context. What we need, then, is a way to use a different display list ID depending upon the OpenGL context within which we are currently rendering. Context-specific data comes to the rescue to address this problem.

Context-specific data provides us with a way to get a separate copy of a variable for each OpenGL rendering context. This may sound daunting at first, but VR Juggler manages this special variable so that it appears just as a normal variable. The developer never has to deal with contexts directly. VR Juggler transparently ensures that the correct copy of the variable is being used.

Context-Specific Variables in VR Juggler

The following shows how a context-specific variable appears in a VR Juggler application:

using namespace vrj;

class userApp : public GlApp
{
public:
   draw();
public:
   GlContextData<int> mDispListId; // Context-specific variable
};

userApp::draw()
{
   glCallList(*mDispListId);
}

This code looks nearly the same as the previous example. In this case, mDispListId is treated as a pointer, and it has a special template-based type that tells VR Juggler it is context-specific data. When defining a context-specific data member, use the vrj::GlContextData<T> template class and pass the “true” type of the variable to the template definition. From then on, it can be treated as a normal pointer.

Note

The types that are used for context-specific data must provide default constructors. The user cannot directly call the constructor for the data item because VR Juggler has to allocate new items on the fly as new contexts are created.

The Inner Workings of Context-Specific Variables

Curious readers are probably wondering how all of this works. To satisfy any curiosity, we now provide a brief description.

The context data items are allocated using a template-based smart pointer class (vrj::GlContextData<T>). Behind the scenes, VR Juggler keeps a list of currently allocated variables for each context. When the application wants to use a context data item, the smart pointer looks in the list and returns a reference to the correct copy for the current context.

This is all done in a fairly light-weight manner. It all boils down to one memory lookup and a couple of pointer dereferences. Not bad for all the power that it gives.

Using Context-Specific Data

The VR Juggler OpenGL graphics system is a complex, multi-headed beast. Luckily, developers do not have to understand how the system is working to use it correctly. As long as developers subscribe to several simple rules for allocating and using context data, everything will work fine. This section contains these rules, but it does not describe the rationale behind the rules. Those readers who are interested in the details of why these rules should be followed should please read the subsequent section. It contains much more (excruciating) detail.

The Rules

With the background in how to make a context-specific data member and how to use it in a draw() member function, we can move on to how and where the context-specific data should be allocated. If we want to create a display list, we need to know where we should allocate it.

Rule 1: Do not allocate context data in draw()

This is straightforward: do not allocate context data in the draw() member function. There are many reasons for this, but the primary one is that allocation tests would be occurring too many times and at incorrect times. There are better places to allocate context data.

Rule 2: Initialize static context data in contextInit()

The place to allocate static context-specific data is the vrj::GlApp::contextInit() member function. “Static” context data refers to context data that does not change during the application's execution. An example of static context data would be a display list to render an object model that is preloaded by the application and never changes. It is static because the display list only has to be generated once for each context, and the application can generate the display list as soon as it starts execution.

The contextInit() member function is called immediately after creation of any new OpenGL contexts. In other words, it is called whenever new windows open. When it is called, the newly created context is active. This method is the perfect place to allocate static context data because it is only called when we have a new context that we need to prepare (and also because that is what it is designed for).

The following code snippet shows a possible use of the application object's contextInit() method:

Example 4.1. Initializing context-specific data

  1 void userApp::contextInit()
    {
       // Allocate context specific data
       (*mDispListId) = glGenLists(1);
  5 
       glNewList((*mDispListId), GL_COMPILE);
          glScalef(0.50f, 0.50f, 0.50f);
          // Call func that draws a cube in OpenGL
          drawCube();
 10    glEndList();
       ...
    }

This shows the normal way that display lists should be allocated in VR Juggler. Allocate the display list, store it to a context-specific data member, and then fill the display list. Texture objects and other types of context-specific data are created in exactly the same manner.

Rule 3: Allocate and update dynamic context data in contextPreDraw()

The place to allocate dynamic context-specific data is the contextPreDraw() member function. “Dynamic” context data differs from static context data in that dynamic data may change during the application's execution. An example of dynamic data would be a display list for rendering an object from a data set that changes as the applications executes. This requires dynamic context data because the display list has to be regenerated every time the application changes the data set.

Consider also the following example. While running an application, the user requests to load a new model from a file. After the model data is loaded, it may be best to put the drawing functions into a fresh display list for rendering the model. In this case, vrj::GlApp::contextInit() cannot be used because it is only called when a new context is created. Here, all the windows have already been created. What we need, then, is a callback that is called once per existing context so that we can add and change the context-specific data. That is what contextPreDraw() does. It is called once per context for each VR Juggler frame with the current context active.

Please notice, however, that since this method is called often and is called in performance-critical areas, you should not do much work in it. Any time taken by this method directly decreases the draw performance of the application. In most cases, we recommend trying to make the function have a very simple early exit clause such as in the following example. This makes the average cost only that of a single comparison operation.

userApp::contextInit()
{
   if (have work to do)
   {
     // Do it
   }
}

Context-Specific Data Details

Within this section, we provide the details of context-specific data in VR Juggler and justify the rules presented in the previous section.

Figure 4.3. VR Juggler OpenGL system

VR Juggler OpenGL system

Do Not Allocate Context-Specific Data in draw()

Rule 1 says that context-specific data should not be allocated in an application object's draw() method. We have already stated that the main reason is that draw() is called too many times, and it is called at the wrong time for allocation of context-specific data. To be more specific, the draw() method is called for each surface, or for each eye, every frame. Static context-specific data only needs to be allocated when a new window is opened. (Dynamic context-specific data is handled separately.)

Tutorial: Drawing a Cube using OpenGL Display Lists

Table 4.3. Tutorial Overview

DescriptionDrawing a cube using a display list in the draw() member function
ObjectivesUnderstand how to use context-specific data in an application
Member functions
  • vrj::App::init()

  • vrj::GlApp::contextInit()

  • vrj::GlApp::draw()

Directory$VJ_BASE_DIR/share/samples/OGL/simple/contextApp
Files
  • contextApp.h

  • contextApp.cpp

Class Declaration and Data Members

The following code example shows the basics of declaring the class interface and data members for an application that will use context-specific data. This is an extension of the simple OpenGL application presented in the section called “Tutorial: Drawing a Cube with OpenGL”. Note the addition of the contextInit() declaration and the use of the context-specific data member mCubeDlId.

  1 using namespace vrj;
    
    class contextApp : public GlApp
    {
  5 public:
       contextApp() {;}
       virtual void init();
       virtual void contextInit();
       virtual void draw();
 10     ...
    public:
       // Id of the cube display list
       GlContextData<GLuint> mCubeDlId;
       ...
 15 };

The contextInit() Member Function

We now show the implementation of contextApp::contextInit(). Here the display list is created and stored using context-specific data. Recall Example 4.1, “Initializing context-specific data”, presented in the section called “Using Context-Specific Data”. That example was based on this tutorial application.

  1 void contextApp::contextInit()
    {
       // Allocate context specific data
       (*mCubeDlId) = glGenLists(1);
  5 
       glNewList((*mCubeDlId), GL_COMPILE);
          glScalef(0.50f, 0.50f, 0.50f);
          drawCube();
       glEndList();
 10    ...
    }

The draw() Member Function

Now that we have a display list ID in context-specific data, we can use it in the draw() member function. We render the display list by dereferencing the context-specific display list ID.

  1 using namespace gmtl;
    
    void contextApp::draw()
    {
  5    // Get Wand matrix
       gmtl::Matrix44f wand_matrix(mWand->getData());
       ...
       glPushMatrix();
          glPushMatrix();
 10          glMultMatrixf(wand_mat.getData());    
             glCallList(*mCubeDlId);
          glPopMatrix();
          ...
       glPopMatrix();
 15 }

Exercise

In the tutorial application code, replace the call to drawAxis() with a display list call.