When Points
are transformed using shift()
, shear()
,
or one of the other transformation functions, the
world_coordinates
are not modified directly. Instead,
another data member of class Point
is used to store the
information about the transformation, namely transform
of
type class Transform
. A Transform
object has a single
data element of type Matrix
and a number of member functions. A
Matrix
is
simply a
4 X 4
array1
of reals
defined using typedef real Matrix[4][4]
.
Such a matrix suffices for performing all
of the transformations (affine and perspective) possible in
three-dimensional space.2
Any combination of transformations can be represented by a single
transformation matrix. This means that consecutive transformations
of a Point
can be "saved up" and applied to its coordinates
all at once when needed, rather than updating them for each
transformation.
Transforms
work by performing matrix multiplication of
Matrix
with the homogeneous world_coordinates
of
Points
.
If a set of homogeneous coordinates
\alpha = (x, y, z, w)
and
Matrix M
=
a e i m
b f j n
c g k o
d h l p
then the set of homogeneous coordinates \beta resulting from
multiplying \alpha and M is calculated as follows:
\beta = \alpha\times M = ((xa + yb + zc + wd), (xe + yf + zg + wh), (xi + yj + zk + wl), (xm + yn + zo + wp))Please note that each coordinate of \beta can be influenced by all of the coordinates of \alpha.
Operations on matrices are very important in computer graphics applications and are described in many books about computer graphics and geometry. For 3DLDF, I've mostly used Huw Jones' Computer Graphics through Key Mathematics and David Salomon's Computer Graphics and Geometric Modeling.
It is often useful to declare and use Transform
objects in 3DLDF,
just as it is for transforms
in Metafont. Transformations can be
stored in Transforms
and then be used to transform Points
by means of Point::operator*=(const Transform&)
.
1. Transform t; 2. t.shift(0, 1); 3. Point p(1, 0, 0); 4. p *= t; 5. p.show("p:"); -| p: (1, 1, 0)
When a Transform
is declared (line 1), it is
initialized to an identity matrix. All identity matrices are
square, all of the elements of the main diagonal (upper left to lower
right) are 1, and all of the other elements are 0.
So a
4 X 4
identity matrix, as used in 3DLDF, looks like this:
1 0 0 0 0 1 0 0 0 0 1 0 0 0 0 1If a matrix A is multiplied with an identity matrix I, the result is identical to A, i.e., A * I = A. This is the salient property of an identity matrix.
The same affine transformations are applied in the same way to
Transforms
as they are to Points
, i.e., the functions
scale()
, shift()
,
shear()
, and rotate()
correspond to the Point
versions of these functions, and they take the same arguments:
Point p; Transform t; p.shift(3, 4, 5); t.shift(3, 4, 5); => p.transform == t p.show_transform("p:"); -| p: Transform: 0 0.707 0.707 0 -0.866 0.354 -0.354 0 -0.5 -0.612 0.612 0 0 0 0 1 t.show("t:"); -| t: 0 0.707 0.707 0 -0.866 0.354 -0.354 0 -0.5 -0.612 0.612 0 0 0 0 1
It is unfortunate that the terms ``array'', ``matrix'', and ``vector'' have different meanings in C++ and in normal mathematical usage. However, in practice, these discrepancies turn out not to cause many problems. Stroustrup, The C++ Programming Language, section 22.4, p. 662.
In fact, none of the operations for transformations require all of the elements of a 4 X 4 matrix. In many 3D graphics programs, the matrix operations are modified to use smaller transformation matrices, which reduces the storage requirements of the program. This is a bit tricky, because the affine transformations and the perspective transformation use different elements of the matrix. I consider that the risk of something going wrong, possibly producing hard-to-find bugs, outweighs any benefits from saving memory (which is usually no longer at a premium, anyway). In addition, there may be some interesting non-affine transformations that would be worth implementing. Therefore, I've decided to use full 4 X 4 matrices in 3DLDF.