Table of Contents
This chapter comprises the bulk of information about application development. This makes sense when one considers the importance of computer grahpics in the context of immersive applications. In each section of this chapter, we explain the use of different graphics application programming interfaces (APIs) within the scope of VR Juggler. While the sections of this chapter are tied to specific APIs, we highly recommend that all prospective programmers of VR Juggler applications read the first section about OpenGL applications. This section covers core fundamentals of the VR Juggler OpenGL Draw Manager that apply to the use of Open Scene Graph and OpenSG with VR Juggler.
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.
In Figure 5.1, “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.
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 ...
}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.
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.
In this section, we present a tutorial that demonstrates simple rendering with OpenGL calls. The tutorial overview is as follows:
Description: Simple OpenGL application that draws a cube in the environment.
Objectives: Understand 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
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 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 ...
// 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));
...
glPushMatrix();
// Push on offset
15 glMultMatrixf(box_offset.getData());
...
drawCube();
glPopMatrix();
...
20 }![]() | This creates a |
![]() | The new matrix is pushed onto the OpenGL modelview matrix stack. |
![]() | 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.
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
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.
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.
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.
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.
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.
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.
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.
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 5.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.
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
}
}Within this section, we provide the details of context-specific data in VR Juggler and justify the rules presented in the previous section.
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.)
In this section, we present a tutorial that demonstrates the use of OpenGL display lists with VR Juggler context-specific data. The tutorial overview is as follows:
Description: Drawing a cube using a display list in the
draw() member function.
Objective: Understand 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
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 };We now show the implementation of
contextApp::contextInit(). Here the
display list is created and stored using context-specific data.
Recall Example 5.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 ...
}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
const float units = getDrawScaleFactor();
gmtl::Matrix44f wand_matrix(mWand->getData(units));
...
glPushMatrix();
10 glPushMatrix();
glMultMatrixf(wand_mat.getData());
glCallList(*mCubeDlId);
glPopMatrix();
...
15 glPopMatrix();
}