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