The gmtl::Matrix44f Helper Class

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.

High-Level Description

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.

Using gmtl::Matrix44f

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.

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

Assignment

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.

Equality/Inequality Comparison

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.

Transposing

The transpose operation works conceptually as matrix1 = transpose(matrix2). 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.

Finding the Inverse

The inverse operation works conceptually as matrix1 = inverse(matrix2). 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.

Addition

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.

Subtraction

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.

Multiplication

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 mat1 = mat1 * mat2 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 mat1 = mat2 * mat1 so that the second matrix (mat2) comes as the first factor. In both cases, the result of the multiplication is stored in mat1.

Scaling by a Scalar Value

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

Making an Identity Matrix Quickly

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.

Zeroing a Matrix in a Single Step

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.

Making an XYZ, a ZYX, or a ZXY Euler Rotation Matrix

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 is a ZXY Euler rotation matrix. The previous two operations are destroyed.

Making a Translation Transformation Matrix

To make a translation matrix, there are two methods with each having two different types of arguments specifying the translation. The first makes a matrix with only the given translation (all other transformation information is destroyed):

gmtl::Matrix44f mat;
gmtl::Vec3f trans(4.0, -4.231, 1.0);

mat = gmtl::makeTrans<gmtl::Matrix44f>(trans);

To change the translation of a transformation matrix without completely obliterating all other transformations, use the following instead:

gmtl::Vec3f trans(4.0, -4.231, 1.0);

gmtl::setTrans(mat, trans);

Making a Scale Transformation Matrix

To make a transformation matrix that only scales, a simple method is provided. It works as follows:

gmtl::Matrix44f mat;
gmtl::Vec3f scale( 1.5, 1.5, 1.5 );

mat = gmtl::makeScale<Matrix44f>(scale);

The result is that mat is a transformation matrix that will perform a scale operation. In this specific case, the scaling happens uniformly for x, y, and z.

Extracting Specific Transformation Information

Finally, methods are provided for extracting transformations from a given matrix. The individual rotations and the translation can be read. For the following examples, assume that mat is a gmtl::Matrix44f object representing arbitrary translation, rotation, and scaling transformations. To get the Z-axis rotation information (an Euler angle), use the following:

float z_rot = (gmtl::makeRot<gmtl::EulerAngleXYZf>(mat))[2];

The value return is in radians. We can also get the X-axis rotation.

float x_rot = (gmtl::makeRot<gmtl::EulerAngleXYZf>(mat))[0];

Getting translations is even simpler because translations are collected into a single vector easily.

gmtl::Vec3f trans;
gmtl::setTrans(trans, mat);

After this, the translation in mat is stored in trans. The same can be done with a gmtl::Vec4f instead of the gmtl::Vec3f.

Converting to an OpenGL Performer Matrix

SGI's OpenGL Performer likes to work with its own pfMatrix class, and to facilitate the use of it with gmtl::Matrix44f, two conversion functions are provided for making conversions. The first works as follows:

gmtl::Matrix44f vj_mat;
pfMatrix pf_mat;

// Perform operations on vj_mat...

pf_mat = vrj::GetPfMatrix(vj_mat);

where vj_mat is passed by reference for efficiency. (pf_mat gets a copy of a pfMatrix which is a slow operation.) To convert a pfMatrix to a gmtl::Matrix44f, do the following:

pfMatrix pf_mat;
gmtl::Matrix44f vj_mat;

// Perform operations on pf_mat...

vj_mat = vrj::GetVjMatrix(pf_mat);

Here again, pf_mat is passed by reference for efficiency, and vj_mat gets a copy of a gmtl::Matrix44f. Both of these functions are found in the header vrj/Draw/Pf/PfUtil.h.

The Gory Details

Now it is time for the really nasty part. Reading this could cause difficulty in understanding the overwhelming amount of information just presented. Do not read any further unless you absolutely have to or you just like to confuse yourself.

C, C++, and mathematics use matrices in row-major order. Access indices are shown in Table 3.1, “Row-major access indices”

Table 3.1. Row-major access indices

(0,0)(0,1)(0,2)(0,3)<--- Array
(1,0)(1,1)(1,2)(1,3)<--- Array
(2,0)(2,1)(2,2)(2,3)<--- Array
(3,0)(3,1)(3,2)(3,3)<--- Array

OpenGL ordering specifies that the matrix has to be column-major in memory. Thus, to provide programmers with a way to pass a transformation matrix to OpenGL in one step (via glMultMatrixf()), the gmtl::Matrix44f class maintains its internal matrix in column-major order. Note that in the following table, the given indices are what the cells have to be called in C/C++ notation because we are putting them back to back. This is illustrated in Table 3.2, “Column-major access indices”.

Table 3.2. Column-major access indices

(0,0)(1,0)(2,0)(3,0)
(0,1)(1,1)(2,1)(3,1)
(0,2)(1,2)(2,2)(3,2)
(0,3)(1,3)(2,3)(3,3)
^^^^
ArrayArrayArrayArray

As mentioned, all of this is done so that a given gmtl::Matrix44f that acts as a full transformation matrix can be passed to OpenGL directly (more or less). For example, with a given gmtl::Matrix44f object mat upon which painstaking transformations have been performed, the following can be done:

glMultMatrixf(mat.getData());

That could not be simpler. All the transformation efforts have culminated into one statement.

For further information, the best possible source of information, especially for this class, is the header file. Read it; understand it; love it.