Version 2.0
Copyright © 2001–2005 Iowa State University
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: 2006-09-01 09:14:44 -0500 (Fri, 01 Sep 2006) $
Table of Contents
gmtl::Vec<S, T> Helper
Classgmtl::Vec3f and
gmtl::Vec4fgmtl::Matrix44f Helper Classgmtl::Matrix44fList of Figures
vrj::App hierarchyvrj::GlApp interface extensions to
vrj::Appvrj::PfApp interface extensions to
vrj::Appvrj::GlApp application classvrj::PfApp application classvrj::OpenSGApp application
classvrj::OsgApp application classList of Tables
List of Examples
gadget::DigitalInterface in
an Application Objectgadget::KeyboardMouseInterface in an
Application ObjectpfExit() with a Heap-Allocated
Application ObjectpfExit() with a Stack-Allocated
Application Objectvpr::SerializableObjectMixin<T>cluster::UserData<T>latePreFrame()draw()vpr::Intervalconfig_app.jdefConfigApp.jconfTable of Contents
gmtl::Vec<S, T> Helper
Classgmtl::Vec3f and
gmtl::Vec4fgmtl::Matrix44f Helper Classgmtl::Matrix44fIn 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:
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.
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.
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.
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.
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.
This book is organized into three main parts:
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.
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).
Advanced topics: An extension of the previous chapters showing how to incorporate run-time reconfiguration into applications and how to write multi-threaded applications.
Table of Contents
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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 {
vrj::Kernel* kernel = vrj::Kernel::instance(); // Get the kernel
simpleApp* app = new simpleApp(); // Create the app object
kernel->loadConfigFile(...); // Configure the kernel
10 kernel->start(); // Start the kernel thread
kernel->setApplication(app); // Give application to kernel
kernel->waitForKernelStop(); // Block until kernel stops
return 0;
15 }![]() | 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. |
![]() | We instantiate a copy of the user application object
( |
![]() | This statement represents the code that will be in the
|
![]() | 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. |
![]() | 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. |
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.
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.
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.
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.
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.
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.
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.
In OpenGL, there is no concept of initializing the API, so this method is normally empty in such applications.
This member function is called after the graphics API has been started but before the kernel frame is started.
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.
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.
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.
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.
This method is called after application-specific data is sychronized among the cluster nodes but before triggering rendering of the current frame.
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.
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.
This method is invoked after rendering has been triggered but before the rendering has finished.
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).
This method is invoked after rendering has completed but before VR Juggler updates devices and other internal data.
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.
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.
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.
The initScene() member function is
called when the application should create the scene graph it will
use.
Table of Contents
gmtl::Vec<S, T> Helper
Classgmtl::Vec3f and
gmtl::Vec4fgmtl::Matrix44f Helper Classgmtl::Matrix44fWithin 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.
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.
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.
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.
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).
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.)
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
}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.
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 another vector, do the following:
gmtl::Vec3f vec1(1.0, 1.5, -1.0), vec2; vec2 = 3 * vec1;
(The order of the factors in the multiplication can be swapped
depending on preference or need.) Here, vec2 gets
the value <3.0, 4.5, -3.0>.
To multiply a vector by a scalar and store the result in the same vector, do the following:
gmtl::Vec3f vec1(1.0, 1.5, -1.0); vec1 *= 3;
After this, vec1 has the value <3.0,
4.5, -3.0>.
Very similar to multiplying by a scalar, division by scalars is also possible. While the examples are almost identical, they are provided here for clarity.
To divide a vector by a scalar and store the result in another vector, do the following:
gmtl::Vec3f vec1(1.0, 1.5, -1.0), vec2; vec2 = vec1 / 3;
Here, vec2 gets the value <0.333333,
0.5, -0.333333>. Note that the scalar must come after the vector
because the operation would not make sense otherwise.
To divide a vector by a scalar and store the result in the same vector, do the following:
gmtl::Vec3f vec1(1.0, 1.5, -1.0); vec1 /= 3;
After this, vec1 has the value
<0.333333, 0.5, -0.333333>.
SGI's OpenGL Performer likes to work with its own
pfVec3 class, and to facilitate the use of it
with gmtl::Vec3f, two conversion functions
are provided for converting a gmtl::Vec3f to
a pfVec3 and vice versa. The first works as
follows:
gmtl::Vec3f vj_vec; pfVec3 pf_vec; // Do stuff to vj_vec... pf_vec = vrj::GetPfVec(vj_vec);
where vj_vec is passed by reference for
efficiency. (pf_vec gets a copy of a
pfVec3.) To convert a
pfVec3 to a
gmtl::Vec3f, do the following:
pfVec3 pf_vec; gmtl::Vec3f vj_vec; // Do stuff to pf_vec... vj_vec = vrj::GetVjVec(pf_vec);
Here again, pf_vec is passed by reference
for efficiency, and vj_vec gets a copy of a
gmtl::Vec3f. Both of these functions are
found in the header
vrj/Draw/Pf/PfUtil.h.
We have already demonstrated vector assignment, though it was not pointed out explicitly. It works just as vector assignment in mathematics. The C++ code that does assignment is as follows:
gmtl::Vec3f vec1(1.0, 1.5, -1.0), vec2; vec2 = vec1;
After the assignment, vec2 has the value
<-1.0, -1.5, 1.0>. Ta da! Note that this is a copy operation
which is the case for all the types of assignments of GMTL
vectors.
To compare the equality of two vectors, there are three available methods (one is just the complement of the other, though):
gmtl::Vec3f vec1(1.0, 1.5, -1.0), vec2(1.5, 1.0, -1.0);
if ( gmtl::isEqual(vec1, vec2) )
{
// Go here if vec1 and vec2 are equal.
}or
gmtl::Vec3f vec1(1.0, 1.5, -1.0), vec2(1.5, 1.0, -1.0);
if ( vec1 == vec2 )
{
// Go here if vec1 and vec2 are equal.
}or
gmtl::Vec3f vec1(1.0, 1.5, -1.0), vec2(1.5, 1.0, -1.0);
if ( vec1 != vec2 )
{
// Go here if vec1 and vec2 are not equal.
}Choose whichever method is most convenient.
Given two vectors, finding the dot product is often needed. GMTL vectors provide a way to do this quickly so that programmers can save themselves the time of typing in the formula over and over. It works as follows:
gmtl::Vec3f vec1(1.0, 1.5, -1.0), vec2(1.5, 1.0, -1.0); float dot_product; dot_product = gmtl::dot(vec1, vec2);
Now, dot_product has the value 4.0.
Besides the dot product of two vectors, the cross product is another commonly needed result. It is calculated thusly:
gmtl::Vec3f vec1(1.0, 1.5, -1.0), vec2(1.5, 1.0, -1.0), vec3; vec3 = gmtl::cross(vec1, vec2);
The result is that vec3 gets a copy of
vec1 cross vec2.
Adding two vectors can be done one of two ways. The first method returns a resulting vector, and the second method performs the addition and stores the result in the first vector.
gmtl::Vec3f vec1(1.0, 1.5, -1.0), vec2(1.5, 1.0, -1.0), vec3; vec3 = vec1 + vec2;
Now, vec3 has the value <2.5, 2.5,
-2.0>.
gmtl::Vec3f vec1(1.0, 1.5, -1.0), vec2(1.5, 1.0, -1.0); vec1 += vec2;
This time, vec1 has the value <2.5, 2.5,
-2.0>.
Subtracting two vectors gives the same options as addition, and while the code is nearly identical, it is provided for the sake of clarity.
gmtl::Vec3f vec1(1.0, 1.5, -1.0), vec2(1.5, 1.0, -1.0), vec3; vec3 = vec1 - vec2;
Now, vec3 has the value <-0.5, 0.5,
0.0>.
gmtl::Vec3f vec1(1.0, 1.5, -1.0), vec2(1.5, 1.0, -1.0); vec1 -= vec2;
In this case, vec1 has the value <-0.5,
0.5, 0.0>.
It is often helpful to apply a transformation to a vector.
Transformations are represented by a matrix, so it is necessary to
multiply a matrix and a vector. The function
gmtl::xform() does this job. For the following
example, assume that there is a
gmtl::Matrix44f transformation matrix
xform_mat:
gmtl::Vec3f vec(1.0, 1.0, 1.0), result_vec; gmtl::xform(result_vec, xform_mat, vec1);
Depending on the transformations contained within
xform_mat, result_vec will be
transformed fully. The operation as a mathematical equation would
be:

where V and V' are vectors and M is a 4×4 transformation matrix.
The details behind gmtl::Vec3f and
gmtl::Vec4f really are not all that gory.
Internally, they are represented as three- and four-element arrays
of floats respectively. Access to these arrays is
provided through the member function
getData(). For example, this access can be
used in the following way:
gmtl::Vec3f pos(4.0, 1.0982, 10.1241); glVertex3fv(pos.getData());
Granted, this particular example is rather silly and much
slower than just listing the values as the individual arguments to
glVertex3f(), but it should get the point
across.
In general, the getData() member
function should be treated very carefully. Access to it is provided
mainly so that operations similar to this example can be performed
quickly. An example of abusing access to
getData() follows:
gmtl::Vec4f my_vec; my_vec.getData()[0] = 4.0; my_vec.getData()[1] = 1.0982; my_vec.getData()[2] = 10.1241; my_vec.getData()[3] = 1.0;
Do not do this. It can be confusing to readers of the code who do not necessarily need to know the details of the internal representation. Instead, use one of the methods described above for creating vectors and assigning the elements values.
This section is intended to provide an introduction into how the
helper class gmtl::Matrix44f works and how it can be used in VR Juggler
applications. It begins with a high-level description of the class,
which forms the necessary basis for understanding it in detail. Then,
examples of how to use all the available operations in the interfaces
for the class are provided. It concludes with a description of the
internal C++ details of gmtl::Matrix44f.
Abstractly, gmtl::Matrix44f represents a 4×4 matrix of single-precision
floating-point values. The class includes implementations of the
standard matrix operations such as transpose, scale, and multiply.
More specifically, it is a mechanism to facilitate common matrix
operations used in computer graphics, especially those associated
with a transform matrix. On the surface, it is
nearly identical to a 4×4 C++ array of floats, but
there is one crucial difference: a
gmtl::Matrix44f keeps its internal matrix in
column-major order rather than in row-major order. More detail on this is given below, but this is done
because OpenGL maintains its internal matrices using the same memory
layout. At the conceptual level, this does not matter—it is related
only to the matrix representation in the computer's memory. Access
to the elements is still in row-major order. In any case,
understanding how C++ multidimensional arrays work means
understanding 90% of what there is to know about
gmtl::Matrix44f. The class provides a degree
convenience not found with a normal C++ array, especially when
programming with OpenGL. The complications surrounding the
gmtl::Matrix44f class are identical to those
with OpenGL matrix handling, and with an understanding of that, then
all that is left to learn is the interface of
gmtl::Matrix44f.
As a representation of mathematical matrices,
gmtl::Matrix44f implements several common
operations performed on matrices to relieve the users of some
tedious, repetitive effort. The general mathematical operations
are:
Assignment
Equality/inequality comparison
Transposing
Finding the inverse
Addition
Subtraction
Multiplication
Scaling by a scalar value
The operations well-suited for use with computer graphics are:
Creating an identity matrix quickly
Zeroing a matrix in a single step
Creating an XYZ, a ZYX, or a ZXY Euler rotation matrix
Constraining rotation about a specific axis or axes
Making a matrix using direction cosines
Making a matrix from a quaternion
Making a rotation transformation matrix about a single axis
Making a translation transformation matrix
Making a scale transformation matrix
Extracting specific transformation information
Converting to an OpenGL Performer matrix
What is presented here involves some complicated concepts that
are far beyond the scope of this documentation. Without an
understanding of matrix math (linear algebra) and an understanding
of how transformation matrices work in OpenGL, this document will
not be very useful. It is highly recommended that readers be
familiar with these topics before proceeding. Otherwise, with this
high-level description in mind, we now continue on to explain the
gmtl::Matrix44f class at the C++
level.
Keeping the idea of a normal mathematical matrix in mind, we
are now ready to look at the C++ use of the
gmtl::Matrix44f class. Most of the interface
is defined using methods, but there are a few cases where
mathematical operators have been overloaded to make code easier to
read. Before going any further, whenever using a
gmtl::Matrix44f, make sure to include
gmtl/Matrix.h first. The operations presented
above are now described in detail in the order in which they were
listed above. We begin with creating the objects and setting their
values.
Before doing anything with matrices, some must be created
first. To create a gmtl::Matrix44f, the
default constructor can be used. It initializes the matrix to be an
identity matrix:
gmtl::Matrix44f mat1;
After creating this matrix mat1, its 16
elements can be assigned values all at once as follows:
mat1.set(0.0, 1.0, 2.3, 4.1,
8.3, 9.0, 2.2, 1.0,
5.6, 9.9, 9.7, 8.2,
3.8, 0.9, 2.1, 0.1);or with a float array:
float mat_vals[16] =
{
0.0, 8.3, 5.6, 3.8,
1.0, 9.0, 9.9, 0.9,
2.3, 2.2, 9.7, 2.1,
4.1, 1.0, 1.0, 0.1
};
mat1.set(mat_vals);Note that when explicitly listing the values with
set(), they are specified in
row-major order. When put into a 16-element
array of floats, however, they must be ordered so that
they can be copied into the gmtl::Matrix44f
in column-major order. This is the one
exception in the interface where access is column-major (which
probably means that the interface has a bug).
To set all the values of a new matrix in one step, they can be given as arguments when declaring the matrix:
gmtl::Matrix44f mat1(0.0, 1.0, 2.3, 4.1,
8.3, 9.0, 2.2, 1.0,
5.6, 9.9, 9.7, 8.2,
3.8, 0.9, 2.1, 0.1);All of the above code has exactly the same results but accomplishes those results in different ways.
To read the elements in a
gmtl::Matrix44f object, programmers can use
either the overloaded [] operator or the overloaded () operator. The
overloaded [] operator returns the specified row of the
gmtl::Matrix44f, and an element in that row
can then be read using [] again. The code looks exactly the same as
with a normal C++ two-dimensional array:
gmtl::Matrix44f mat1(0.0, 1.0, 2.3, 4.1,
8.3, 9.0, 2.2, 1.0,
5.6, 9.9, 9.7, 8.2,
3.8, 0.9, 2.1, 0.1);
float val;
val = mat1[3][0];Here, val is assigned the value 3.8. Using
the overloaded () operator results in code that looks similar to the
way the matrix element would be referenced in mathematics:
gmtl::Matrix44f mat1(0.0, 1.0, 2.3, 4.1,
8.3, 9.0, 2.2, 1.0,
5.6, 9.9, 9.7, 8.2,
3.8, 0.9, 2.1, 0.1);
float val;
val = mat1(3, 0);Again, val is assigned the value 3.8. Both
of these operations are row-major.
Assigning one gmtl::Matrix44f to
another happens using the normal = operator as follows:
gmtl::Matrix44f mat1(0.0, 1.0, 2.3, 4.1, 8.3, 9.0, 2.2, 1.0,
5.6, 9.9, 9.7, 8.2, 3.8, 0.9, 2.1, 0.1);
gmtl::Matrix44f mat2;
mat2 = mat1;This makes a copy of
mat1 in mat2 which can be a
slow operation.
To compare the equality of two matrices, there are three available methods (one is just the complement of the other, though):
gmtl::Matrix44f mat1(0.0, 1.0, 2.3, 4.1, 8.3, 9.0, 2.2, 1.0,
5.6, 9.9, 9.7, 8.2, 3.8, 0.9, 2.1, 0.1);
gmtl::Matrix44f mat2(0.0, 1.0, 2.3, 4.1, 8.3, 9.0, 2.2, 1.0,
5.6, 9.9, 9.7, 8.2, 3.8, 0.9, 2.1, 0.1);
if ( gmtl::isEqual(mat1, mat2) )
{
// Go here if mat1 and mat2 are equal.
}or
gmtl::Matrix44f mat1(0.0, 1.0, 2.3, 4.1, 8.3, 9.0, 2.2, 1.0,
5.6, 9.9, 9.7, 8.2, 3.8, 0.9, 2.1, 0.1);
gmtl::Matrix44f mat2(0.0, 1.0, 2.3, 4.1, 8.3, 9.0, 2.2, 1.0,
5.6, 9.9, 9.7, 8.2, 3.8, 0.9, 2.1, 0.1);
if ( mat1 == mat2 )
{
// Go here if mat1 and mat2 are equal.
}or
gmtl::Matrix44f mat1(0.0, 1.0, 2.3, 4.1, 8.3, 9.0, 2.2, 1.0,
5.6, 9.9, 9.7, 8.2, 3.8, 0.9, 2.1, 0.1);
gmtl::Matrix44f mat2(0.0, 1.0, 2.3, 4.1, 8.3, 9.0, 2.2, 1.0,
5.6, 9.9, 9.7, 8.2, 3.8, 0.9, 2.1, 0.1);
if ( mat1 != mat2 )
{
// Go here if mat1 and mat2 are not equal.
}Choose whichever method is most convenient.
The transpose operation works conceptually as
. The code is then:
gmtl::Matrix44f mat1;
gmtl::Matrix44f mat2(0.0, 1.0, 2.3, 4.1, 8.3, 9.0, 2.2, 1.0,
5.6, 9.9, 9.7, 8.2, 3.8, 0.9, 2.1, 0.1);
gmtl::transpose(mat1, mat2);The result is stored in mat1.
mat2 is passed by reference for
efficiency.
The inverse operation works conceptually as
. The code is then:
gmtl::Matrix44f mat1;
gmtl::Matrix44f mat2(0.0, 1.0, 2.3, 4.1, 8.3, 9.0, 2.2, 1.0,
5.6, 9.9, 9.7, 8.2, 3.8, 0.9, 2.1, 0.1);
gmtl::invert(mat1, mat2);The result is stored in mat1.
mat2 is passed by reference for
efficiency.
For the addition operation, the interface is defined so that
the sum of two matrices is stored in a third. There are two ways to
do addition with gmtl::Matrix44f: using the
add() method or using the overloaded + operator. Use of the former
is recommended, but the latter can be used if one prefers that style
of programming. Examples of both methods follow. The first block of
code only declares the gmtl::Matrix44f
objects.
gmtl::Matrix44f mat1(0.0, 1.0, 2.3, 4.1, 8.3, 9.0, 2.2, 1.0,
5.6, 9.9, 9.7, 8.2, 3.8, 0.9, 2.1, 0.1);
gmtl::Matrix44f mat2(0.0, 1.0, 2.3, 4.1, 8.3, 9.0, 2.2, 1.0,
5.6, 9.9, 9.7, 8.2, 3.8, 0.9, 2.1, 0.1);
gmtl::Matrix44f mat3;Using the gmtl::add() function:
gmtl::add(mat3, mat1, mat2);
Using the overloaded + operator:
mat3 = mat1 + mat2;
The result is stored (via a copy) in
mat3.
For the subtraction operation, the interface is defined so
that the difference of two matrices is stored in a third. There are
two ways to do subtraction with
gmtl::Matrix44f: using the
sub() method or using the overloaded -
operator. It is recommended that developers use the former, but the
latter can be used for stylistic purposes. Examples of both methods
follow. The first block of code only declares the
gmtl::Matrix44f objects.
gmtl::Matrix44f mat1(0.0, 1.0, 2.3, 4.1, 8.3, 9.0, 2.2, 1.0,
5.6, 9.9, 9.7, 8.2, 3.8, 0.9, 2.1, 0.1);
gmtl::Matrix44f mat2(0.0, 1.0, 2.3, 4.1, 8.3, 9.0, 2.2, 1.0,
5.6, 9.9, 9.7, 8.2, 3.8, 0.9, 2.1, 0.1);
gmtl::Matrix44f mat3;Using the gmtl::sub() method:
gmtl::sub(mat3, mat1, mat2);
Using the overloaded - operator:
mat3 = mat1 - mat2;
The result is stored (via a copy) in
mat3.
As in the case of addition and subtraction, the multiplication
interface is defined so that the product of two matrices is stored
in a third. This is likely to be the operation used most often since
transformation matrices are constructed through multiplication of
different transforms. For normal matrix multiplication, there are
two ways to do multiplication with
gmtl::Matrix44f: using the
gmtl::mult() function or using the overloaded *
operator. We recommend the use of the
gmtl::mult() function but the overloaded *
operator can be used by those who prefer that style of programming.
Examples of both methods follow. The first block of code only
declares the gmtl::Matrix44f objects.
gmtl::Matrix44f mat1(0.0, 1.0, 2.3, 4.1, 8.3, 9.0, 2.2, 1.0,
5.6, 9.9, 9.7, 8.2, 3.8, 0.9, 2.1, 0.1);
gmtl::Matrix44f mat2(0.0, 1.0, 2.3, 4.1, 8.3, 9.0, 2.2, 1.0,
5.6, 9.9, 9.7, 8.2, 3.8, 0.9, 2.1, 0.1);
gmtl::Matrix44f mat3;Using the gmtl::mult() function:
gmtl::mult(mat3, mat1, mat2);
Using the overloaded * operator:
mat3 = mat1 * mat2;
The result is stored (via a copy) in
mat3.
There are two more multiplication operations provided that help in handling the order of the matrices when they are multiplied. These two extra operations do post-multiplication and pre-multiplication of two matrices. An example of post-multiplication is:
gmtl::Matrix44f mat1(0.0, 1.0, 2.3, 4.1, 8.3, 9.0, 2.2, 1.0,
5.6, 9.9, 9.7, 8.2, 3.8, 0.9, 2.1, 0.1);
gmtl::Matrix44f mat2(0.0, 1.0, 2.3, 4.1, 8.3, 9.0, 2.2, 1.0,
5.6, 9.9, 9.7, 8.2, 3.8, 0.9, 2.1, 0.1);
gmtl::postMult(mat1, mat2);Conceptually, the operation is
so that the second matrix
(mat2) comes as the second factor. The same
result can be achieved using the overloaded *= operator:
gmtl::Matrix44f mat1(0.0, 1.0, 2.3, 4.1, 8.3, 9.0, 2.2, 1.0,
5.6, 9.9, 9.7, 8.2, 3.8, 0.9, 2.1, 0.1);
gmtl::Matrix44f mat2(0.0, 1.0, 2.3, 4.1, 8.3, 9.0, 2.2, 1.0,
5.6, 9.9, 9.7, 8.2, 3.8, 0.9, 2.1, 0.1);
mat1 *= mat2;An example of pre-multiplication is:
gmtl::Matrix44f mat1(0.0, 1.0, 2.3, 4.1, 8.3, 9.0, 2.2, 1.0,
5.6, 9.9, 9.7, 8.2, 3.8, 0.9, 2.1, 0.1);
gmtl::Matrix44f mat2(0.0, 1.0, 2.3, 4.1, 8.3, 9.0, 2.2, 1.0,
5.6, 9.9, 9.7, 8.2, 3.8, 0.9, 2.1, 0.1);
gmtl::preMult(mat1, mat2);Here, the conceptual operation is
so that the second matrix
(mat2) comes as the first factor. In both cases,
the result of the multiplication is stored in
mat1.
Scaling the values of a matrix by a scalar value can be done
using two different methods: the setScale()
method or the overloaded * and / operators that take a single scalar
value and returns a gmtl::Matrix44f. As with
the preceding operations, we recommend the use of the former, but
the latter is available for those who want it. Examples of both
methods follow. First, using the
gmtl::setScale() function works as:
gmtl::Matrix44f mat1(0.0, 1.0, 2.3, 4.1, 8.3, 9.0, 2.2, 1.0,
5.6, 9.9, 9.7, 8.2, 3.8, 0.9, 2.1, 0.1);
gmtl::setScale(mat1, 3.0);In computer graphics, an identity matrix is often needed when
performing transformations. Because of this,
gmtl::Matrix44f provides a method for
converting a matrix into an identity matrix in a single step (at the
user code level anyway):
gmtl::Matrix44f mat1(0.0, 1.0, 2.3, 4.1, 8.3, 9.0, 2.2, 1.0,
5.6, 9.9, 9.7, 8.2, 3.8, 0.9, 2.1, 0.1);
gmtl::identity(mat1);Of course, simply declaring mat1 with no
arguments would achieve the same result, but that is not such an
interesting example.
Before using a matrix, it is often helpful to zero it out to
ensure that there is no pollution from previous use. With a
gmtl::Matrix44f, this can be done in one
step:
gmtl::Matrix44f mat1(0.0, 1.0, 2.3, 4.1, 8.3, 9.0, 2.2, 1.0,
5.6, 9.9, 9.7, 8.2, 3.8, 0.9, 2.1, 0.1);
gmtl::zero(mat1);The result is that all elements of mat1 are
now 0.0.
All the rotation information for a transform can be contained in a single matrix using the methods for making an XYZ, a ZYX, or a ZXY Euler matrix. Code for all three follows:
vrj::Matrix mat1; float x_rot = 0.4, y_rot = 0.541, z_rot = 0.14221; gmtl::setRot(mat1, gmtl::EulerAngleXYZf(x_rot, y_rot, z_rot)); gmtl::setRot(mat1, gmtl::EulerAngleZYXf(z_rot, y_rot, x_rot)); gmtl::setRot(mat1, gmtl::EulerAngleZXYf(z_rot, x_rot, y_rot));
In every case, the matrix is zeroed before the rotation
transformation is stored. The result of the above code is that
mat1