VR Juggler

The Programmer's Guide

Version 2.2

Permission is granted to copy, distribute and/or modify this document under the terms of the GNU Free Documentation License, Version 1.2 or any later version published by the Free Software Foundation; with the Invariant Sections being Appendix A, GNU Free Documentation License, with no Front-Cover Texts, and with no Back-Cover Texts. A copy of the license is included in Appendix A, GNU Free Documentation License.

$Date: 2007-12-13 22:21:26 -0600 (Thu, 13 Dec 2007) $


Table of Contents

I. Introduction
1. Getting Started
Necessary Experience
Required Background
Other VR Software Tools
Organization
2. Application Basics
Application Object Overview
No main()—“Don't call me, I'll call you
Application Objects Derive from Base Classes for Specific Graphics APIs
Writing an Application Means Filling in the Blanks
Benefits of Application Objects
Allow for Run-Time Changes
Low Coupling
Allows Implementation Changes
Multi-Language Interaction
VR Juggler Startup
No main()—Sort Of
Structure of a main() Function
Mac OS X Considerations
Kernel Loop
Definition of a Frame
Base Application Object Interface
Initialization
Frame Functions
Draw Manager-Specific Application Classes
OpenGL Application Class
OpenGL Performer Application Class
3. Helper Classes
The gmtl::Vec<S, T> Helper Class
High-Level Description
Using gmtl::Vec3f and gmtl::Vec4f
Creating Vectors and Setting Their Values
Inversion (Finding the Negative of a Vector)
Normalization
Length Calculation
Multiplication by a Scalar
Division by a Scalar
Converting to an OpenGL Performer Vector
Assignment
Equality/Inequality Comparison
Dot Product
Cross Product
Addition
Subtraction
Full Transformation by a Matrix
The Gory Details
The gmtl::Matrix44f Helper Class
High-Level Description
Using gmtl::Matrix44f
Creating Matrices and Setting Their Values
Assignment
Equality/Inequality Comparison
Transposing
Finding the Inverse
Addition
Subtraction
Multiplication
Scaling by a Scalar Value
Making an Identity Matrix Quickly
Zeroing a Matrix in a Single Step
Making an XYZ, a ZYX, or a ZXY Euler Rotation Matrix
Making a Translation Transformation Matrix
Making a Scale Transformation Matrix
Extracting Specific Transformation Information
Converting to an OpenGL Performer Matrix
The Gory Details
Device Proxies and Device Interfaces
High-Level Description of Device Proxies
High-Level Description of Device Interfaces
Using Device Interfaces
Stupefied Proxies
The Gory Details
II. Application Programming
4. Application Authoring Basics
Application Review
Basic Application Information
Draw Manager-Specific Application Classes
Getting Input
How to Get Input
Where to Get Input
Tutorial: Getting Input
5. Using Graphics Programming Interfaces
OpenGL Applications
Clearing the Color and Depth Buffers
OpenGL Drawing: vrj::GlApp::draw()
Tutorial: Drawing a Cube with OpenGL
Context-Specific Data
Using Context-Specific Data
Context-Specific Data Details
Tutorial: Drawing a Cube using OpenGL Display Lists
OpenGL Performer Applications
Scene Graph Initialization: vrj::PfApp::initScene()
Scene Graph Access: vrj::PfApp::getScene()
Tutorial: Loading a Model with OpenGL Performer
Other vrj::PfApp Methods
pfExit(): To Call or Not to Call
OpenSG Applications
Scene Graph Initialization: vrj::OpenSGApp::initScene()
Scene Graph Access: vrj::OpenSGApp::getScene()
Tutorial: Loading a Model with OpenSG
Open Scene Graph Applications
Scene Graph Initialization: vrj::OsgApp::initScene()
Scene Graph Access: vrj::OsgApp::getScene()
Tutorial: Loading a Model with Open Scene Graph
VTK Applications
6. Additional Application Programming Topics
Cluster Application Programming
Shared Input Data
Application-Specific Shared Data
General Cluster Programming Issues
Troubleshooting Cluster Problems
Adding Audio
Using Sonix Directly
Using the VR Juggler Sound Manager
7. Porting to VR Juggler from the CAVElibs
The Initialize, Draw, and Frame Routines
Getting Input from Devices
Configuration
Important Notes
Shared Memory
OpenGL Context-Specific Data
Source Code
The Form of a Basic CAVElibs™ Program
The Form of a Basic VR Juggler Program
8. Porting to VR Juggler from GLUT
Window Creation and Management
The Initialize, Draw, and Frame Routines
Getting Input from Devices
Configuration
Important Notes
Shared Memory
OpenGL Context-Specific Data
Source Code
The Form of a Basic GLUT Program
The Form of a Basic VR Juggler Program
III. Advanced Topics
9. System Interaction
10. Multi-threading
Techniques
Tutorial: Perform Computations Asynchronously to Rendering with intraFrame()
Class Declaration and Data Members
The preFrame() Member Function
The draw() Member Function
Exercise
Helper Classes
vpr::Thread
vpr::BaseThreadFunctor
Using the vpr::Semaphore Interface
Using the vpr::Mutex Interface
Using Data Buffering
Triple Buffering
Optimizing Triple Buffering
Using Triple Buffering in an Application
Tutorial: Perform Computations Using Triple Buffering
11. Run-Time Reconfiguration
How Run-Time Reconfiguration Works
Reasons to Use Run-Time Reconfiguration
Using Run-Time Reconfiguration in an Application
Create Application-Specific Configuration Definitions
Implement the Dynamic Reconfiguration Interface
Procesing Configuration Elements
Loading and Saving Configurations
Tutorial: Using Application-Specific Configurations
Class Declaration
Application Configuration
The configCanHandle() Member Function
The configAdd() Member Function
The draw() Member Function
Exercise
12. Extending VR Juggler
Device Drivers
Custom Simulators
Simulator Components
13. Advanced Topics
Customizing Render Thread Processor Affinity
Making a Custom NSApplication Delegate on Mac OS X
Data Members
Designated Initializer
-setLoadConfigs:
-applicationShouldTerminateAfterLastWindowClosed:
-applicationDidFinishLaunching:
-application:openFile:
-application:openFiles:
Defining Custom Cocoa/VR Juggler Bridging on Mac OS X
IV. Appendices
A. GNU Free Documentation License
PREAMBLE
APPLICABILITY AND DEFINITIONS
VERBATIM COPYING
COPYING IN QUANTITY
MODIFICATIONS
COMBINING DOCUMENTS
COLLECTIONS OF DOCUMENTS
AGGREGATION WITH INDEPENDENT WORKS
TRANSLATION
TERMINATION
FUTURE REVISIONS OF THIS LICENSE
ADDENDUM: How to use this License for your documents
Glossary of Terms
Index

List of Figures

2.1. vrj::App hierarchy
2.2. Kernel loop sequence
2.3. Application object interface
2.4. vrj::GlApp interface extensions to vrj::App
2.5. vrj::PfApp interface extensions to vrj::App
4.1. VR Juggler kernel control loop
5.1. vrj::GlApp application class
5.2. VR Juggler OpenGL system
5.3. vrj::PfApp application class
5.4. vrj::OpenSGApp application class
5.5. vrj::OsgApp application class
6.1. Basic Sonix Interface
6.2. The Sonix Design
6.3. Use of Plug-ins in Sonix

List of Tables

3.1. Row-major access indices
3.2. Column-major access indices

List of Examples

2.1. Ignoring Command Line Arguments on Mac OS X
2.2. Handling Optional Command Line Arguments on Mac OS X
3.1. Using gadget::DigitalInterface in an Application Object
3.2. Using gadget::KeyboardMouseInterface in an Application Object
3.3. Requesting Positional Data in Application-Specific Units
5.1. Initializing context-specific data
5.2. Using pfExit() with a Heap-Allocated Application Object
5.3. Using pfExit() with a Stack-Allocated Application Object
6.1. Declaration of a Serializable Type
6.2. Serializing an Application-Specific Type
6.3. Sample Third-Party Type
6.4. Serializing a Third-Party Type Using vpr::SerializableObjectMixin<T>
6.5. Declaring Instances of cluster::UserData<T>
6.6. Initializing Application-Specific Shared Data
6.7. Application-Specific Shared Data Configuration
6.8. Writing to Application-Specific Shared Data
6.9. Reading from Application-Specific Shared Data in latePreFrame()
6.10. Reading from Application-Specific Shared Data in draw()
6.11. Calculating Frame Deltas Using vpr::Interval
6.12. Initializing the Sonix Sound APi
6.13. Setting Up a Sonix Sound Handle
6.14. Sonix Frame Update
6.15. Complete Sonix Program Using OpenAL
6.16. Reconfiguring Sonix at Run Time
6.17. Example Sonix Sound Manager Conifguration
6.18. Declaring Sound Handles in Application Object Class
6.19. Initializing Sound Handles in an Application Object
6.20. Triggering Sounds in an Application Object
11.1. Complete listing of config_app.jdef
11.2. ConfigApp.jconf
13.1. MyDelegate.mm: Basic Delegate Implementation

Part I. Introduction

Table of Contents

1. Getting Started
Necessary Experience
Required Background
Other VR Software Tools
Organization
2. Application Basics
Application Object Overview
No main()—“Don't call me, I'll call you
Application Objects Derive from Base Classes for Specific Graphics APIs
Writing an Application Means Filling in the Blanks
Benefits of Application Objects
Allow for Run-Time Changes
Low Coupling
Allows Implementation Changes
Multi-Language Interaction
VR Juggler Startup
No main()—Sort Of
Structure of a main() Function
Mac OS X Considerations
Kernel Loop
Definition of a Frame
Base Application Object Interface
Initialization
Frame Functions
Draw Manager-Specific Application Classes
OpenGL Application Class
OpenGL Performer Application Class
3. Helper Classes
The gmtl::Vec<S, T> Helper Class
High-Level Description
Using gmtl::Vec3f and gmtl::Vec4f
Creating Vectors and Setting Their Values
Inversion (Finding the Negative of a Vector)
Normalization
Length Calculation
Multiplication by a Scalar
Division by a Scalar
Converting to an OpenGL Performer Vector
Assignment
Equality/Inequality Comparison
Dot Product
Cross Product
Addition
Subtraction
Full Transformation by a Matrix
The Gory Details
The gmtl::Matrix44f Helper Class
High-Level Description
Using gmtl::Matrix44f
Creating Matrices and Setting Their Values
Assignment
Equality/Inequality Comparison
Transposing
Finding the Inverse
Addition
Subtraction
Multiplication
Scaling by a Scalar Value
Making an Identity Matrix Quickly
Zeroing a Matrix in a Single Step
Making an XYZ, a ZYX, or a ZXY Euler Rotation Matrix
Making a Translation Transformation Matrix
Making a Scale Transformation Matrix
Extracting Specific Transformation Information
Converting to an OpenGL Performer Matrix
The Gory Details
Device Proxies and Device Interfaces
High-Level Description of Device Proxies
High-Level Description of Device Interfaces
Using Device Interfaces
Stupefied Proxies
The Gory Details

Chapter 1. Getting Started

In this book, we present a “how-to” for writing VR Juggler applications. We will explain concepts used in VR Juggler and present carefully annotated example code whenever appropriate. There are two groups of people who should read this book:

  1. Those who are required to read it in order to do a project for work or school. To those in this category, fear not—VR Juggler is very simple to use after getting through the initial learning stages. It is a powerful tool that will allow the creation of interesting and powerful applications very quickly.

  2. Those who are just interested in creating compelling, interesting VR applications. VR Juggler facilitates the construction of extremely powerful applications that will run on nearly any combination of hardware architecture and software platform.

Necessary Experience

To help readers get the most from this book, recommendations follow to provide an idea of what previous experience is necessary. Various programming skills are needed, of course, but programming for VR requires more than just knowledge of a given programming language. VR Juggler takes advantage of many programming design patterns and advanced concepts to make it more powerful, more flexible, and more extensible. A good background in mathematics is helpful for performing the myriad transformations that must be applied to three-dimensional (3D) geometry.

Required Background

To get the most from this chapter, there are a few prerequisites:

  • C++ programming experience

  • Some graphics programming background (e.g., OpenGL, OpenGL Performer, etc.)

  • Reasonable mathematical background (linear algebra knowledge is very useful)

For some of the advanced sections of this book, it is recommended that readers review the VR Juggler architecture book. This is optional, though it may be helpful in gaining a quicker understanding of some topics and concepts.

Other VR Software Tools

Readers who already have experience with other VR software development environments can easily skim through this book and find the relevant new information. The book is designed for easy skimming. Simply look at the headings to get a good determination of what should be read and what may be skipped.

Organization

This book is organized into three main parts:

  1. Introduction: The introduction to the key VR Juggler application development concept, application objects. We also describe common helper classes that simplify the process of writing applications.

  2. Writing applications: The presentation of application development including how to get input from devices and how to write applications for each of the supported graphics application programmer interfaces (APIs).

  3. Advanced topics: An extension of the previous chapters showing how to incorporate run-time reconfiguration into applications and how to write multi-threaded applications.

Chapter 2. Application Basics

In VR Juggler, all applications are written as objects that are handled by the kernel. The objects are known as application objects, and we will use that term frequently throughout this text. Application objects are introduced and explained in this chapter.

Application Object Overview

VR Juggler uses the application object to create the VR environment with which the users interact. The application object implements interfaces needed by the VR Juggler virtual platform.

No main()—“Don't call me, I'll call you

Since VR Juggler applications are objects, developers do not write the traditional main() function. Instead, developers create an application object that implements a set of pre-defined interfaces. The VR Juggler kernel controls the application's processing time by calling the object's interface implementation methods.

In traditional programs, the main() function defines the point where the thread of control enters the application. After the main() function is called, the application starts performing any necessary processing. When the operating system (OS) starts the program, it gives the main() function some unit of processing time. After the time unit (quantum) for the process expires, the OS performs what is called a “context switch” to change control to another process. VR Juggler achieves similar functionality but in a slightly different manner.

The application objects correspond to processes in a normal OS. The kernel is the scheduler, and it allocates time to an application by invoking the methods of the application object. Because the kernel has additional information about the resources needed by the applications, it maintains a very strict schedule to define when the application is granted processing time. This is the basis to maintain coherence across the system.

Application Objects Derive from Base Classes for Specific Graphics APIs

The first step in defining an application object is to implement the basic interfaces defined by the kernel and the Draw Managers. There is a base class for the interface that the kernel expects (vrj::App) and a base class handled by each Draw Manager interface (vrj::PfApp, vrj::GlApp, etc.). See Figure 2.1, “vrj::App hierarchy” for a visual representation of the complete application interface hierarchy. The interface defined in vrj::App specifies methods for initialization, shutdown, and execution of the application. This is the abstract type that is seen by the VR Juggler kernel. The Draw Manager interfaces specified in the vrj::*App classes define the API-specific functions necessary to render the virtual environment. For example, an interface used by a Draw Manager could have functions for drawing the scene and for initializing context-specific information.

Figure 2.1. vrj::App hierarchy

vrj::App hierarchy

Writing an Application Means Filling in the Blanks

To implement an application in VR Juggler, developers simply need to “fill in the blanks” of the appropriate interfaces. To simplify this process, there are default implementations of most methods in the interfaces. Hence, the user must only provide implementations for the aspects they want to customize. If an implementation is not provided in the user application object, the default is used, but it is important to know that in most cases, the default implementation does nothing.

Tip

When overriding a virtual method defined by a VR Juggler application class, it is best to call the parent class method implementation before performing any application-specific processing. For example, if a user-defined application object overrides vrj::App::init() in the class userApp, the method userApp::init() should invoke vrj::App::init() before performing its own initialization steps.

Benefits of Application Objects

As stated earlier, the most common approach for VR application development is one where the application defines the main() function. That main() function in turn calls library functions when needed. (This is the model followed by software packages such as the CAVElibs™ and the Diverse Toolkit.) The library in this model only executes code when directed to do so by the application. As a result, the application developer is responsible for coordinating the execution of the different VR system components. This can lead to complex applications.

Allow for Run-Time Changes

As a virtual platform, VR Juggler does not use the model described above because VR Juggler needs to maintain control of the system components. This control is necessary to make changes to the virtual platform at run time. As the controller of the execution, the kernel always knows the current state of the applications, and therefore, it can manage the run-time reconfigurations of the virtual environment safely. With run-time reconfiguration, it is possible to switch applications, start new devices, reconfigure running devices, and send reconfiguration information to the application object.

Low Coupling

Application objects lead to a robust architecture as a result of low coupling and well-defined inter-object dependencies. The application interface defines the only communication path between the application and the virtual platform, and this allows restriction of inter-object dependencies. This decreased coupling allows changes in the system to be localized, and thus, changes to one object will not affect another unless the interface itself is changed. The result is code that is more robust and more extensible.

Because the application is simply an object, it is possibly to load and unload applications dynamically. When the virtual platform initializes, it waits for an application to be passed to it. When the application is given to the VR Juggler kernel at run time, the kernel performs a few initialization steps and then executes the application.

Allows Implementation Changes

Since applications use a distinct interface to communicate with the virtual platform, changes to the implementation of the virtual platform do not affect the application. Changes could include bug fixes, performance tuning, or new device support.

Multi-Language Interaction

By treating applications as objects, we can mix programming languages in the VR Juggler kernel. For example, an application object could be written in Python, C#, or even VB.NET, but the VR Juggler kernel (written in standard C++) will still see it as an instance of the abstract interface vrj::App. The use of application objects has allowed such extensions to VR Juggler to be written without requiring any changes to VR Juggler.

VR Juggler Startup

In this section, we describe one way to start VR Juggler. We will use the traditional main() function in C++, but this is not the only way to do it. We have written Python applications that start the VR Juggler kernel, and it is possible to write a VR Juggler daemon that loads applications on demand at runtime. In other words, the VR Juggler startup procedure is quite flexible, and we choose to focus on the simplest method here.

No main()—Sort Of

Previously, we explained how VR Juggler applications do not have a main() function, but further explanation is required. While it is true that user applications do not have a main() function because they are objects, there must still be a main() somewhere that starts the system. This is because the operating system uses main() as the starting point for all applications. In typical VR Juggler applications, there is a main(), but it only starts the VR Juggler kernel and gives the kernel the application to run. It then waits for the kernel to shut down before exiting.

Structure of a main() Function

The following is a typical example of a main() function that will start the VR Juggler kernel and hand it an instance of a user application object. The specifics of what is happening in this code are described below.

  1 #include <vrj/Kernel/Kernel.h>
    #include <simpleApp.h>
    
    int main(int argc, char* argv[])
  5 {                                                                         (1)
       vrj::Kernel* kernel = vrj::Kernel::instance(); // Get the kernel       (2)
       simpleApp* app      = new simpleApp();         // Create the app object
                                                                              (3)
       kernel->loadConfigFile(...);             // Configure the kernel       (4)
 10    kernel->start();                         // Start the kernel thread    (5)
       kernel->setApplication(app);             // Give application to kernel
       kernel->waitForKernelStop();             // Block until kernel stops
    
       return 0;
 15 }

1

This line finds (and may create) the VR Juggler kernel. The kernel reference is stored in the handle so that we can use it later.

2

We instantiate a copy of the user application object (simpleApp) here. Notice that we include the header file that defines the simpleApp class.

3

This statement represents the code that will be in the main() function for passing configuration files to the kernel's loadConfigFile() method. These configuration files may come from the command line or from some other source. If reading the files from the command line, it can be as simple as looping through all the arguments and passing each one to the kernel.

4

As a result of this statement, the VR Juggler kernel begins running. It creates a new thread of execution for the kernel, and the kernel begins its internal processing. From this point on, any changes made reconfigure the kernel. These changes can come in the form of more configuration files or in the form of an application object to execute. At this point, it is important to notice that the kernel knows nothing about the application. Moreover, there is no need for it to know about configuration files yet. This demonstrates how the VR Juggler kernel executes independently from the user application. The kernel will simply work on its own controlling and configuring the system even without an application to run.

5

This statement finally tells the kernel what application it should run. The method call reconfigures the kernel so that it will now start invoking the application object's member functions. It is at this time that the application is now running in the VR system.

Mac OS X Considerations

VR Juggler 2.2 introduces support for Cocoa on Mac OS X. Cocoa itself is quite different than the X11 or Win32 APIs as far as restrictions on threading and the implementation of the main() function. The fundamental issue, however, is that both the NSApplication singleton object and the vrj::Kernel singleton object want to be in charge of application execution. A balance has been struck that generally allows VR Juggler applications to look and act the same on Mac OS X as on any other platform while still taking advantage of unique features that Cocoa and Mac OS X have to offer.

Application Bundles

First and foremost, when using a Cocoa-aware version of VR Juggler, applications have to be constructed as bundles. The details of OS X application bundles are far beyond the scope of this document. However, VR Juggler provides just about everything that is required to make an application bundle. This makes sense because VR Juggler is an application framework that dictates how applications are written and executed. Thus, it also provides the core information required for proper application construction and execution on Mac OS X.

To get started, it is helpful to understand what the application bundle will look like when we have everything in place. An application bundle is a directory structure with a special name. For example, MPApp.app will show up in the Finder as an application named “MPApp” that can be double-clicked. The name can contain spaces if so desired.

Under the base directory is the Contents subdirectory. It will contain the files Info.plist and PkgInfo.

Next, there are two subdirectories MacOS and Resources. Compiled binaries should normally go into the MacOS subdirectory. If nothing else, the bundle executable (the compiled application binary) must go in the MacOS directory. Shared libraries can go into the Resources directory if desired. Generally, though, the Resources directory will contain platform-independent data files.

In the resources directory, there will be another subdirectory en.lproj[1] which in turn contains MainMenu.nib from $VJ_BASE_DIR/share/vrjuggler/data/bundle. This is a critical part of the application bundle, and it is very important that this NIB behave correctly. The NIB defines the user interface for the VR Juggler application, and what is provided with VR Juggler is set up and ready to go for the most common cases. It is possible to use a different MainMenu.nib, but customizing it will require care.

Info.plist

The files and directories needed for application bundle creation can be found in the directory $VJ_BASE_DIR/share/vrjuggler/data/bundle. The first of these is Contents/Info.plist, the basic property list for an application bundle. Open it with the Property List Editor application and change the @APP_NAME@ strings accordingly for the name of the application being constructed. For the CFBundleExecutable property, be sure to change @APP_NAME@ to be the name of the executable that will be in the Contents/MacOS directory. Other properties to change are those that include copyright and version information.

PkgInfo

The contents of Contents/PkgInfo define the application bundle as an application bundle. The contents of the file will often be APPL????, though other characters are allowed in place of the ???? part.

Resources Directory

The Contents/Resources directory contains data files related to application execution. The file vrjuggler.plist, the use of which is highly recommended, must be put in this directory. The application bundle icon file (a file with the extension .icns) is also stored in this directory. A default icon file, vrjuggler.icns, can be used, or a custom one can be made. The file to use must be named in Contents/Info.plist.

vrjuggler.plist

For VR Juggler application bundles, a useful file is vrjuggler.plist. The default version of this file from $VJ_BASE_DIR/share/vrjuggler/data/bundle disables VR Juggler configuration file loading through NSApplication channels and identifies the NSApplication delegate class type that will be used (see the section called “Making a Custom NSApplication Delegate on Mac OS X”). These are set using the properties VRJConfigHandling and VRJDelegateClass respectively.

The “configuration file loading through NSApplication channels” bit has to do with associating VR Juggler configuration files with application bundles. There are different means by which data can be delivered to applications on Mac OS X. For example, double-clicking on a file in the Finder (or selecting Open With in the context menu for the file) causes a registered handler application to be opened. The file is given to the application once it has opened through the NSApplication event system. By setting the VRJConfigHandling property to false in Contents/Resources/vrjuggler.plist, this capability is disabled for the application bundle in question. That is, the application bundle will ignore the NSApplication events pertaining to configuration files to be loaded as a result of double-clicking on the said files. Configuration files can still be opened on the fly using the File menu defined in the default NIB (see below).

en.lproj Directory

The en.lproj subdirectory of Contents/Resources contains information translated in the English language. The most important item in this directory is MainMenu.nib (discussed next), but the optional file $VJ_BASE_DIR/share/vrjuggler/data/bundle/InfoPlist.strings, or some other version of same, can be copied into this directory. Other language-specific directories can be created as subdirectories of Contents/Resources as necessary. They, too, must contain a MainMenu.nib.

MainMenu.nib

This is the NIB for the VR juggler application bundle. It has been designed using Interface Builder with a simple user interface that knows how to interact with a VR Juggler application. The interface includes the File menu with the item for opening VR Juggler configuration files on the fly. Other menus and menu items can be added by making a custom MainMenu.nib. Using the version from $VJ_BASE_DIR/share/vrjuggler/data/bundle as a starting point is a good idea. Note that MainMenu.nib is a directory, and it should be copied recursively to Contents/Resources/en.lproj.

Application Execution

The main() function implementation show in the section called “Structure of a main() Function” will work without any problems on Mac OS X. However, many of the VR Juggler sample and test applications have a slightly more complicated main() function. In particular, these applications usually determine whether the user has passed in any arguments through the command line and exit with an error message explaining how to run the application if none were given. The example below shows how this is commonly done with an if statement before anything else:

  1 @include <iostream>
    #include <cstdlib>
    #include <vrj/Kernel/Kernel.h>
    #include <simpleApp.h>
  5 
    int main(int argc, char* argv[])
    {
       if ( argc <= 1 )
       {
 10       std::cout << "Usage: " << argv[0]
                    << "cfgfile[0] cfgfile[1] ... cfgfile[n]"
                    << std::endl;
          std::exit(EXIT_FAILURE);
       }
 15 
       vrj::Kernel* kernel = vrj::Kernel::instance(); // Get the kernel
       simpleApp* app      = new simpleApp();         // Create the app object
    
       kernel->loadConfigFile(...);             // Configure the kernel
 20    kernel->start();                         // Start the kernel thread
       kernel->setApplication(app);             // Give application to kernel
       kernel->waitForKernelStop();             // Block until kernel stops
    
       return 0;
 25 }

This approach will not necessarily work on Mac OS X because users normally expect to be able to launch an application by double-clicking on its icon in the Finder and then loading data into it through a GUI. If a user launched the above application that way, the application would exit immediately, and the user would have to open the Console application to find out what went wrong. Launching from the command line, while perfectly valid on Mac OS X, is simply not what users expect. At the same time, remote launching of VR juggler applications in a cluster will almost certainly require launching without the use of the Finder and passing in arguments through argv. This is definitely the case when using Maestro.

If a VR Juggler application will be run on Mac OS X, the programmer has to decide whether command line processing is critical to the execution of the application. There are three options available to VR Juggler programmers on Mac OS X: rely exclusively on command line arguments to launch, handle no command line arguments, or allow the use of command line arguments but do not require them. Given that VR Juggler applications written on all other platforms are highly likely to rely exclusively on command line arguments to launch, the first choice is expected to be the most commonly chosen approach. Nevertheless, we will explain all three.

Relying Exclusively on Command Line Arguments to Launch

If a VR Juggler application used on Mac OS X will rely solely on command line arguments to launch, it is operating in exactly the same manner as on all other platforms. The main() function can be written as is shown in this document to exit if no command line arguments are supplied. There is still one more thing to do, though. In the Resources directory of the application bundle, there must be a property list file in the bundle called Contents/Resources/vrjuggler.plist. This property list must have the property VRJConfigHandling set to false. This is the default setting for this property if the vrjuggler.plist file that comes with VR Juggler was used in constructing the application bundle.

To execute the application, run the program in <appname>.app/Contents/MacOS from a command prompt and pass in the command line arguments. The appropriate event handling will be set up so that the application will behave just as any other Mac OS X application.

Handling No Command Line Arguments at Launch Time

If no command line arguments are to be handled (i.e., the application is to behave the same as typical Mac OS X applications), then the main() function shown above needs to change. Specifically, it cannot exit with an error message if no command line arguments are passed in. To keep the application portable, a preprocessor conditional can be used, as shown in Example 2.1, “Ignoring Command Line Arguments on Mac OS X”. Then, in Contents/Resources/vrjuggler.plist, set the VRJConfigHandling property to true. To execute the application, double-click the application bundle icon in the Finder or use the open command from a command prompt.

Example 2.1. Ignoring Command Line Arguments on Mac OS X

  1 @include <iostream>
    #include <cstdlib>
    #include <vrj/Kernel/Kernel.h>
    #include <simpleApp.h>
  5 
    int main(int argc, char* argv[])
    {
    #if ! defined(VRJ_USE_COCOA)
       if ( argc <= 1 )
 10    {
          std::cout << "Usage: " << argv[0]
                    << "cfgfile[0] cfgfile[1] ... cfgfile[n]"
                    << std::endl;
          std::exit(EXIT_FAILURE);
 15    }
    #endif
    
       vrj::Kernel* kernel = vrj::Kernel::instance(); // Get the kernel
       simpleApp* app      = new simpleApp();         // Create the app object
 20 
    #if ! defined(VRJ_USE_COCOA)
       kernel->loadConfigFile(...);             // Configure the kernel
    #endif
       kernel->start();                         // Start the kernel thread
 25    kernel->setApplication(app);             // Give application to kernel
       kernel->waitForKernelStop();             // Block until kernel stops
    
       return 0;
    }

Allowing Optional Use of Command Line Arguments

A compromise can be struck between the previous two options by allowing optional use of command line arguments. The compromise is simple: do not require command line arguments but still handle them if they are given. The form of the main() function that allows this is shown in Example 2.2, “Handling Optional Command Line Arguments on Mac OS X”. Note that the #if around the call to vrj::Kernel::loadConfigFile() has been removed.

Example 2.2. Handling Optional Command Line Arguments on Mac OS X

  1 @include <iostream>
    #include <cstdlib>
    #include <vrj/Kernel/Kernel.h>
    #include <simpleApp.h>
  5 
    int main(int argc, char* argv[])
    {
    #if ! defined(VRJ_USE_COCOA)
       if ( argc <= 1 )
 10    {
          std::cout << "Usage: " << argv[0]
                    << "cfgfile[0] cfgfile[1] ... cfgfile[n]"
                    << std::endl;
          std::exit(EXIT_FAILURE);
 15    }
    #endif
    
       vrj::Kernel* kernel = vrj::Kernel::instance(); // Get the kernel
       simpleApp* app      = new simpleApp();         // Create the app object
 20 
       kernel->loadConfigFile(...);             // Configure the kernel
       kernel->start();                         // Start the kernel thread
       kernel->setApplication(app);             // Give application to kernel
       kernel->waitForKernelStop();             // Block until kernel stops
 25 
       return 0;
    }

The only decision left to make is what value to use for the VRJConfigHandling property in Contents/Resources/vrjuggler.plist. If we set it to false (recall that that is the default in the $VJ_BASE_DIR/share/vrjuggler/data/bundle/vrjuggler.plist version), the application cannot be used as a handler for VR Juggler configuration files. If we set it to true, we have to be prepared for configuration files to come in other than through the command line or through the use of the File menu. The default NSApplication delegate (VRJBasicDelegate) can deal with either case. The default behavior for VR Juggler sample applications is to allow configuration files to be specified on the command line through the use of a main() function similar to that shown in Example 2.2, “Handling Optional Command Line Arguments on Mac OS X” and to leave the property VRJConfigHandling in Contents/Resources/vrjuggler.plist set to false.

Kernel Loop

Before proceeding into application object details, we must understand how VR Juggler calls the application, and we must know what a frame is. In the code above, the statement on line 9 tells the kernel thread to start running. When the kernel begins its execution, it follows the sequence shown in Figure 2.2, “Kernel loop sequence”. The specific methods called are described in more detail in the following section. This diagram will be useful in understanding the order in which the application object methods are invoked.

Figure 2.2. Kernel loop sequence

Kernel loop sequence

Definition of a Frame

The VR Juggler kernel calls each of the methods in the application object based on a strictly scheduled frame of execution. The frame of execution is shown in Figure 2.2, “Kernel loop sequence”; it makes up all the lines within the “while(!quit)” clause.

During the frame of execution, the kernel calls the application methods and performs internal updates (the updateAllData() method call). Because the kernel has complete control over the frame, it can make changes at pre-defined “safe” times when the application is not doing any processing. At these times, the kernel can change the virtual platform configuration as long as the interface remains the same.

The frame of execution also serves as a framework for the application. That is, the application can expect that when preFrame() is called, the devices have just been updated. Applications can rely upon the system being in well-defined stages of the frame when the kernel invokes the application object's methods.

Base Application Object Interface

Within this section, we provide a brief overview of the member functions from the base VR Juggler application interface. This interface is defined by vrj::App, and the member functions are shown in Figure 2.3, “Application object interface”. Refer to Figure 2.2, “Kernel loop sequence” for a visual presentation of the order in which the methods are invoked.

The base interface of the application object defines the following functions:

  • init()

  • apiInit()

  • preFrame()

  • intraFrame()

  • postFrame()

As previously described, the VR Juggler kernel calls these functions from its control loop to allocate processing time to them. These functions handle initialization and computation. Other member functions that can be used for reconfiguration, focus control, resetting, and exiting will be covered later in this book.

Figure 2.3. Application object interface

Application object interface

Initialization

The following is a description of the application objects related to the initialization of a VR Juggler application. The order of presentation is the same as the order of execution when the application is executed by the kernel.

vrj::App::init()

The init() method is called by the kernel to initialize any application data. When the kernel prepares to start a new application, it first calls init() to signal the application that it is about to be executed.

Timing

This member function is called immediately after the kernel is told to start running the application and before any graphics API handling has been started by VR Juggler.

Uses

Typical applications will utilize this method to load data files, create lookup tables, or perform some steps that should be done only once per execution. In other words, this method is the place to perform any pre-processing steps needed by the application to set up its data structures.

vrj::App::apiInit()

This member function is for any graphics API-specific initialization required by the application. Data members that cannot be initialized until after the graphics API is started should be initialized here.

Note

In OpenGL, there is no concept of initializing the API, so this method is normally empty in such applications.

Timing

This member function is called after the graphics API has been started but before the kernel frame is started.

Uses

In most cases, scene graph loading and other API-specific initialization should be done in this method.

Frame Functions

Once the application object has been initialized by the VR Juggler kernel, the kernel frame loop begins. Each frame, there are specific application object methods that are invoked, and understanding the timing and potential uses of these methods can improve the functionality of the immersive application. In some cases, it is possible to use these member functions to optimize the application to improve the frame rate and the level of interactivity.

vrj::App::getDrawScaleFactor()

As of VR Juggler 2.0 Alpha 1, applications can specify the units of measure that are the basis for the graphics they render. The default unit of measure is feet (identified by the constant scale factor gadget::PositionUnitConversion::ConvertToFeet) to maintain backwards compatibility with the previous VR Juggler semantics. By overriding this method, applications can identify the unit of measure they expect. The default implementation is the following:

float vrj::App::getDrawScaleFactor()
{
   return gadget::PositionUnitConversion::ConvertToFeet;
}

Overriding this method means changing the rendering scale factor used by the VR Juggler Draw Managers. The current list of constants (defined in gadget/Position/PositionUnitConversion.h) is as follows:

  • gadget::PositionUnitConversion::ConvertToFeet

  • gadget::PositionUnitConversion::ConvertToInches

  • gadget::PositionUnitConversion::ConvertToMeters

  • gadget::PositionUnitConversion::ConvertToCentimeters

Because the value returned is simply a scaling factor, user applications can define whatever units they want. Note that internally, VR Juggler is treating all units as meters, so the scaling factor converts from meters to the desired units.

vrj::App::preFrame()

The preFrame() method is called when the system is about to trigger drawing. This is the time that the application object should do any last-minute updates of data based on input device status. It is best to avoid doing any time-consuming computation in this method. The time used in this method contributes to the overall device latency in the system. The devices will not be re-sampled before rendering begins.

Timing

This method is called before triggering rendering of the current frame.

Uses

In general, this method should be reserved for “last-millisecond” data updates in response to device input (latency-critical code).

vrj::App::latePreFrame()

The latePreFrame() method is called after preFrame() and after shared application-specific data is synchronized among the cluster nodes (see the section called “Cluster Application Programming” for more details) but before the scene is rendered. Scene graph-based application objects making use of application-specific data in a cluster configuration should perform scene graph updates based on the most recently received copy of the shared application data. Application objects not using a scene graph can make state updates in this method or in the rendering method (draw() in the case of vrj::GlApp). The writer node must have written to the shared application data in preFrame() to minimize the latency of the data.

Timing

This method is called after application-specific data is sychronized among the cluster nodes but before triggering rendering of the current frame.

Uses

When using shared application-specific data with a scene-graph based application object in a cluster configuration, the nodes that read from the shared data (those where cluster::UserData<T>::isLocal() returns false) should perform state updates based on the freshly received update to the shared data.

vrj::App::intraFrame()

The code in this method executes in parallel with the rendering method. That is, it executes while the current frame is being drawn. This is the place to put any processing that can be done in advance for the next frame. By doing parallel processing in this method, the application can increase its frame rate because drawing and computation can be parallelized. Special care must be taken to ensure that any data being used for rendering does not change while rendering is happening. One method for doing this is buffering. Use of synchronization primitives is not recommended because that technique could lower the frame rate.

Timing

This method is invoked after rendering has been triggered but before the rendering has finished.

Uses

The primary use of this method is performing time-consuming computations, the results of which can be used in the next frame.

vrj::App::postFrame()

Finally, the postFrame() method is available for final processing at the end of the kernel frame loop. This is a good place to do any data updates that are not dependent upon input data and cannot be overlapped with the rendering process (see the discussion on vrj::App::intraFrame() above).

Timing

This method is invoked after rendering has completed but before VR Juggler updates devices and other internal data.

Uses

Some possible uses of this method include “cleaning up” after the frame has been rendered or synchronizing with external networking or computational processes.

Draw Manager-Specific Application Classes

Beyond the basic methods common to all applications, there are methods that are specific to a given Draw Manager. The application classes are extended for each of the specific Draw Managers. The graphics API-specific application classes derive from vrj::App and extend this interface further. They add extra “hooks” that support the abilities of the specific API.

OpenGL Application Class

The OpenGL application base class adds several methods to the application interface that allow rendering of OpenGL graphics. The extensions to the base vrj::App class are shown in Figure 2.4, “vrj::GlApp interface extensions to vrj::App. In the following, we describe the method vrj::GlApp::draw(), the most important element of the interface. More details about the vrj::GlApp class are provided in the section called “OpenGL Applications”, found in Chapter 4, Application Authoring Basics.

Figure 2.4. vrj::GlApp interface extensions to vrj::App

vrj::GlApp interface extensions to vrj::App

vrj::GlApp::draw()

The “draw function” is called by the OpenGL Draw Manager when it needs to render the current scene in an OpenGL graphics window. It is called for each active OpenGL context.

OpenGL Performer Application Class

The OpenGL Performer application base class adds interface functions that deal with the OpenGL Performer scene graph. Some of the interface extensions are shown in Figure 2.5, “vrj::PfApp interface extensions to vrj::App. The following is a description of only two methods in the vrj::PfApp interface. More detailed discussion on this class is provided in the section called “OpenGL Performer Applications”, found in Chapter 4, Application Authoring Basics.

Figure 2.5. vrj::PfApp interface extensions to vrj::App

vrj::PfApp interface extensions to vrj::App

vrj::PfApp::initScene()

The initScene() member function is called when the application should create the scene graph it will use.

vrj::PfApp::getScene()

The getScene() member function is called by the Performer Draw Manager when it needs to know what scene graph it should render for the application.



[1] This is the English language project data. Translations of MainMenu.nib would go into the appropriate language-specific subdirectory.

Chapter 3. Helper Classes

Within this chapter, we present information on some helper classes that are provided for use with VR Juggler. These classes are intended to make it easier for application programmers to write their code. Ultimately, we want application programmers to focus more on compelling immersive content and less on the many details that are involved with 3D graphics programming. The classes presented in this chapter focus on mathematical computations and on input from hardware devices. VR Juggler uses the Graphics Math Template Library or GMTL (part of the Generic Graphics Toolkit software) for mathematical computation. An overview of the most commonly used GMTL data types and operations is presented here. In addition to the GMTL operations, special attention is paid to Gadgeteer, the input system used by VR Juggler, and its device interfaces and device proxies.

The gmtl::Vec<S, T> Helper Class

This section is intended to provide an introduction to how the helper class gmtl::Vec<S, T> works and how it can be used in VR Juggler applications. It begins with a high-level description of the classes which forms the necessary basis for understanding them in detail. Then, examples of how to use all the available operations in the interfaces for these classes are provided. It concludes with a description of the internal details of the classes.

High-Level Description

The class gmtl::Vec<S, T> is designed to work the same way as a mathematical vector, typically of 3 or 4 dimensions. There are predefined vector types that would normally be used in a VR application that are provided for convenience. That is, a gmtl::Vec3f object can be thought of as a vector of the form <x, y, z>. Similarly, a gmtl::Vec4f can be thought of as a vector of the form <x, y, z, w>. An existing understanding of mathematical vectors is sufficient to know how these classes can be used. The question then becomes, how are they used? We will get to that later, and readers who have experience with vectors can skip ahead. If vectors are an unfamiliar topic, it may be convenient to think of these classes as three- and four-element C++ arrays of floats respectively. Most benefits of the vector concept are lost with that simpler idea, however. Therefore, if the reader needs to think of them as arrays, then arrays should probably be used until vectors feel more comfortable. Once the use of vectors seems familiar and straightforward, readers are encouraged to come back and read further.

Vectors are typically used to contain spatial data or something similar. For convenience, however, they can be visualized as a more general-purpose container for numerical data upon which well-defined operations can be performed. There is no need to constrain thinking of them as only holding the coordinates for some point in space or some other limited-scope use. The GMTL vectors use by VR Juggler retain this generality and can be used wherever vectors come in handy.

gmtl::Vec3f and gmtl::Vec4f, as specific implementations of mathematical vectors, hide vector operations on single-precision floating-point numbers (float) behind a simple-to-use interface. For a single vector, the following standard vector operations are available:

  • Inversion (changing the sign of all elements)

  • Normalization

  • Calculation of length

  • Multiplication by a scalar

  • Division by a scalar

  • Conversion to a Performer vector

For two vectors, the following operations can be performed:

  • Assignment

  • Equality/inequality comparison

  • Dot product

  • Cross product

  • Addition

  • Subtraction

Using GMTL vectors should be straightforward if readers understand these operations and keep in mind that gmtl::Vec3f and gmtl::Vec4f can be thought of at this high level.

Using gmtl::Vec3f and gmtl::Vec4f

With an understanding of these classes as standard mathematical vectors, it is time to learn how to deal with them at the C++ level. In some cases, the mathematical operators are overloaded to simplify user code; in other cases, a named method must be invoked on an object. Before any of that, however, make sure that the source file includes the gmtl/Vec.h header file. From here on, the available operations are presented in the order they were listed in the previous section. We begin with creating the objects and setting their values.

Creating Vectors and Setting Their Values

Before doing anything with vectors, some must be created. The examples here use gmtl::Vec3f, but the example is equally applicable to gmtl::Vec4f. To create a gmtl::Vec3f, use the default constructor which initializes the vector to <0.0, 0.0, 0.0>:

gmtl::Vec3f vec1;

After creating the vector vec1, its elements can be assigned values all at once as follows:

vec1.set(1.0, 1.5, -1.0);

or individually:

vec1[0] = 1.0;
vec1[1] = 1.5;
vec1[2] = -1.0;

Note that in the last example, the individual elements of the vector can be accessed exactly as with a normal array. To do the above steps all at once when the vector is created, give the element values when declaring the vector:

gmtl::Vec3f vec1(1.0, 1.5, -1.0);

All of the above code has exactly the same results but accomplishes them in different ways. This flexibility is just one of the ways that GMTL vectors are more powerful than C++ arrays (of the same size, of course).

Inversion (Finding the Negative of a Vector)

Once a vector is created, the simplest operation that can be performed on it is finding its inverse. The following code demonstrates just that:

gmtl::Vec3f vec1(1.0, 1.5, -1.0), vec2;

vec2 = -vec1;

The vector vec2 now has the value <-1.0, -1.5, 1.0>. That is all there is to it. (Readers interested in details should note that the above does a copy operation to return the negative values.)

Normalization

Normalizing a vector is another simple operation (at the interface level anyway). The following code normalizes a vector:

gmtl::Vec3f vec1(1.0, 1.5, -1.0);

gmtl::normalize( vec1 );

The vector vec1 is now normalized. Clean and simple.

Besides normalizing a given vector, a vector can be tested to determine if it has already been normalized. This is done as follows (assuming the vector vec has already been declared before this point):

if ( gmtl::isNormalized( vec1 ) )
{
   // Go here if vec is normalized
}

Length Calculation

Part of normalizing a vector requires finding its length first. To get a vector's length, do the following:

gmtl::Vec3f vec1(1.0, 1.5, -1.0);
float length;

length = gmtl::length( vec1 );

In this case, length is assigned the value 2.061553 (or more accurately, the square root of 4.25). Finding the length of a vector appears simple from the programmer's perspective, but it has some hidden costs. Namely, it requires a square root calculation. For optimization purposes, GMTL provides a function called gmtl::lengthSquared() that returns the length of the vector without calculating the square root.

Multiplication by a Scalar

The GMTL vector classes provide an easy way to multiply a vector by a scalar. There are several ways to do it depending on what is required. Examples of each method follow.

To multiply a vector by a scalar and store the result in ano