Chapter 3. Helper Classes

Table of Contents

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

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 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>.

Division by a Scalar

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>.

Converting to an OpenGL Performer Vector

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.

Assignment

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.

Equality/Inequality Comparison

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.

Dot Product

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.

Cross Product

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.

Addition

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>.

Subtraction

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>.

Full Transformation by a Matrix

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:

V' = M * V

where V and V' are vectors and M is a 4×4 transformation matrix.

The Gory Details

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.