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