Using Matrix Math in Transformations
In the preceding section, you had to use rotation and transformation calculations to view the triangle shape. Graphics programs often perform all kinds of calculations on the vertices of an object before finally drawing that object onscreen. Translation, scaling, and rotation can all be performed on a single shape just by calling the Translate(), Scale(), and Rotate() functions with the shape's vertices. However, performing so many calculations on many vertices can be time consuming, which is why graphics programmers often use matrix math to transform shapes.
A matrix is simply a table of numbers arranged in rows and columns. Similar to arrays in programming, the size of a matrix is defined by the number of rows and columns it has. For example, this is a 4x4 matrix, which has four rows and four columns:
4 3 2 1 5 4 2 8 3 7 0 5 9 3 6 1
On the other hand, the following is a 3x4 matrix, which has three rows and four columns:
4 7 2 4 4 6 7 3 4 5 2 2
Matrices are so similar to arrays, in fact, that arrays are typically used to represent matrices in computer programs. The 3x4 matrix might be represented in a program as follows:
int matrix[3][4] = {4, 7, 2, 4, 4, 6, 7, 3, 4, 5, 2, 2};
The advantage of matrices in graphics programming is that you can represent any number of transformations with a single matrix. For example, a single matrix can contain all the values you need to simultaneously translate, scale, and rotate a shape. To do this, you fill the matrix with the appropriate values and then you multiply the matrix times all of the shape's vertices. Of course, the trick is to know what values to place in the matrix. You also need to know how to multiply matrices. You'll learn both tricks in the following sections.
Using Matrix Data Types for 2D Graphics
First, you need data types for the matrices you'll be using in your programs. Programs that deal with 2D graphics typically use two types of matrices: 1x3 and 3x3. The 1x3 matrix is a special type of matrix known as a vector. Vectors can represent a vertex in a shape, by holding the vertex's X, Y, and W values. What's W? Although Direct3D sometimes has a special use for this extra value, W is really used most often to simplify the matrix operations. In most cases, W is equal to 1, which means a vector representing a vertex in a shape has this form:
X Y 1
The data type for a vector, then, looks like this:
typedef struct vector { int x, y, w; } VECTOR;
The 3x3 matrix will hold the values needed to transform a vertex, which will be held in the VECTOR data type (which is also a matrix). The data type for the 3x3 matrix looks like this:
typedef double MATRIX3X3[3][3];
Using Transformation Matrices
The first step in using matrices to transform a shape is to load the matrix with the appropriate values. What values you use and where you place them in the matrix depend on the type of transformations you're doing. A matrix that's set up to translate a shape looks like this:
1 0 0 0 1 0 xTrans yTrans 1
Just like when you were using a formula to translate the vertices of a shape, in the preceding matrix the xTrans and yTrans variables are the number of vertical and horizontal units, respectively, that you want to translate the shape. In a program, you'd initialize this matrix like this:
MATRIX3X3 m; m[0][0] = 1.0; m[0][1] = 0.0; m[0][2] = 0.0; m[1][0] = 0.0; m[1][1] = 1.0; m[1][2] = 0.0; m[2][0] = xTrans; m[2][1] = yTrans; m[2][2] = 1.0;
A matrix for scaling a shape looks like this:
xScaleFactor 0 0 0 yScaleFactor 0 0 0 1
Here, the variable xScaleFactor is how much you want to scale the shape horizontally, whereas yScaleFactor is how much to scale vertically. In a program, you'd initialize the scaling matrix like this:
MATRIX3X3 m; m[0][0] = xScaleFactor; m[0][1] = 0.0; m[0][2] = 0.0; m[1][0] = 0.0; m[1][1] = yScaleFactor; m[1][2] = 0.0; m[2][0] = 0.0; m[2][1] = 0.0; m[2][2] = 1.0;
Finally, a matrix for rotating a shape looks as follows:
cos(radians) sin(radians) 0 -sin(radians) cos(radians) 0 0 0 1
Here, the variable radians is the angle of rotation in radians. In a program, you'd initialize the rotation matrix like this:
MATRIX3X3 m; m[0][0] = cos(radians); m[0][1] = sin(radians); m[0][2] = 0.0; m[1][0] = -sin(radians); m[1][1] = cos(radians); m[1][2] = 0.0; m[2][0] = 0.0; m[2][1] = 0.0; m[2][2] = 1.0;
Composing Transformations
Earlier, I said that you can store in a matrix all the values you need to perform translation, scaling, and rotation simultaneously. In the previous section, you saw how each transformation looks when it's stored separately in a matrix. Now, you'll learn about composing transformations, which is the act of combining the translation, scaling, and rotation matrices into one main transformation matrix.
To compose two transformations, you multiply their matrices together, yielding a third master matrix. You can then compose another transformation by multiplying the new matrix by yet another transformation matrix. This composition of matrices can be repeated as often as necessary. Figure 3.11 illustrates an example of matrix composition. Figure 3.12 shows another way of looking at this matrix composition. In Figure 3.12, the results of each composition aren't shown.
Figure 3.11 Matrix composition.
Figure 3.12 Another view of matrix composition.
Now, if you only knew how to multiply matrices! A matrix can be multiplied by any other matrix as long as the first matrix has the same number of columns as the second matrix has rows. So a 1x3 matrix can be multiplied by a 3x3 matrix, which is fortunate because that's exactly what you need to do to multiply a matrix times a vector in 2D graphics programs. Also, a 3x3 matrix can be multiplied by a 3x3 matrix, something else you need to do in a 2D graphics program to compose transformations. You'll look at multiplying vectors a little later in this chapter, but Listing 3.7 is a function that multiplies two 3x3 matrices.
Listing 3.7 Multiplying 3x3 Matrices
void MultMatrix(MATRIX3X3& product, MATRIX3X3& matrix1, MATRIX3X3& matrix2) { for (int x=0; x<3; ++x) for (int y=0; y<3; ++y) { double sum = 0; for (int z=0; z<3; ++z) sum += matrix1[x][z] * matrix2[z][y]; product[x][y] = sum; } }
The function's three parameters are a reference to a 3x3 matrix in which to hold the product of the multiplication and references to the two matrices that should be multiplied. Listing 3.8 is an example of how to use the function.
Listing 3.8 Using the MultMatrix() Function
MATRIX3X3 m1, m2, m3; m1[0][0] = 1.0; m1[0][1] = 0.0; m1[0][2] = 0.0; m1[1][0] = 0.0; m1[1][1] = 1.0; m1[1][2] = 0.0; m1[2][0] = 0.0; m1[2][1] = 0.0; m1[2][2] = 1.0; m2[0][0] = 9.0; m2[0][1] = 8.0; m2[0][2] = 7.0; m2[1][0] = 6.0; m2[1][1] = 5.0; m2[1][2] = 4.0; m2[2][0] = 3.0; m2[2][1] = 2.0; m2[2][2] = 3.0; MultMatrix(m3, m1, m2);
Here, the code first declares three 3x3 matrices, m1, m2, and m3. Next, m1 and m2 are initialized, after which the call to MultMatrix3X3() multiplies m1 times m2 and stores the result in m3. Can you tell what m3 will hold after the multiplication? The answer is that m3 will contain exactly the same values as m2. Why? Because the values stored in m1 are what is known as an identity matrix, which, for a 3x3 matrix, looks like this:
1 0 0 0 1 0 0 0 1
The Identity Matrix
An identity matrix is sort of the matrix equivalent of the number 1. Just as any number times 1 equals the original number (for example, 5 x 1 = 5), so also any matrix times an identity matrix equals the original matrix (for example, m1 x I = m1). An identity matrix contains all zeroes except for the line of 1s that runs diagonally from the upper-left corner to the lower-right corner.
An identity matrix is often used in graphics programming to initialize the main matrix that'll be used to compose transformations. By initializing this main matrix to the identity matrix, you know that there aren't any strange values left over in the matrix that'll foul up your matrix multiplications.
Performing the Transformation
After you compose your transformations, you have a main matrix that contains the exact values you need to simultaneously translate, scale, and rotate a shape. To perform this transformation, you only need to multiply the main transformation matrix by each of the shape's vectors. This operation requires a matrix multiplication function that can handle not only 1x3 vectors and 3x3 matrices, but also can apply the multiplication to a whole list of vectors. Listing 3.9 is a function that does just that.
Listing 3.9 Transformations with Matrices
void Transform(SHAPE& shape, MATRIX3X3& m) { int transformedX, transformedY; for (int x=0; x<shape.numVerts; ++x) { transformedX = (int) (shape.vertices[x].x * m[0][0] + shape.vertices[x].y * m[1][0] + m[2][0]); transformedY = (int) (shape.vertices[x].x * m[0][1] + shape.vertices[x].y * m[1][1] + m[2][1]); shape.vertices[x].x = transformedX; shape.vertices[x].y = transformedY; } }
This function takes as parameters a reference to a SHAPE structure and a reference to a MATRIX3X3 array. When this function has finished, the vertices in the SHAPE structure, shape, will have been transformed by the values in the transformation matrix, m.
Using Some Matrix Utility Functions
Now that you have some idea of how the matrix operations work, you can start using them in your programs. To do that, however, you need a couple of utility functions that make handling matrices a little easier. First, you need a function that can initialize a matrix to an identity matrix. Such a function looks like Listing 3.10.
Listing 3.10 Initializing an Identity Matrix
void InitMatrix(MATRIX3X3& m) { m[0][0]=1; m[0][1]=0; m[0][2]=0; m[1][0]=0; m[1][1]=1; m[1][2]=0; m[2][0]=0; m[2][1]=0; m[2][2]=1; }
The InitMatrix() function takes as a parameter a reference to a MATRIX3X3 array into which the function loads the values that comprise a 3x3 identity matrix.
Another thing you'll need to do is copy a matrix. The CopyMatrix() function looks like Listing 3.11.
Listing 3.11 Copying a Matrix
void CopyMatrix(MATRIX3X3& dst, MATRIX3X3& src) { for (int i=0; i<3; ++i) for (int j=0; j<3; ++j) dst[i][j] = src[i][j]; }
This function takes as parameters references to the destination and source matrices, both of which are the type MATRIX3X3. The function copies the src matrix into the dst matrix.
Using Functions for Composing Transformations
The last task in writing functions for a 2D graphics program using matrices is to rewrite the Translate(), Scale(), and Rotate() functions so that they use the new matrix data types. The Translate() function ends up looking like Listing 3.12.
Listing 3.12 Translating with Matrices
void Translate(MATRIX3X3& m, int xTrans, int yTrans) { MATRIX3X3 m1, m2; m1[0][0]=1; m1[0][1]=0; m1[0][2]=0; m1[1][0]=0; m1[1][1]=1; m1[1][2]=0; m1[2][0]=xTrans; m1[2][1]=yTrans; m1[2][2]=1; MultMatrix(m2, m1, m); CopyMatrix(m, m2); }
This function takes as parameters a reference to the matrix that holds the current state of the transformation and the X and Y translation values. First, the function loads a local matrix with the values that create a translation matrix, after which it multiplies the translation matrix times the main transformation matrix. The result of the multiplication, stored in the local matrix m2, is then copied into the transformation matrix.
Rewriting the Scale() function for use with matrices results in Listing 3.13.
Listing 3.13 Scaling with Matrices
void Scale(MATRIX3X3& m, double xScale, double yScale) { MATRIX3X3 m1, m2; m1[0][0]=xScale; m1[0][1]=0; m1[0][2]=0; m1[1][0]=0; m1[1][1]=yScale; m1[1][2]=0; m1[2][0]=0; m1[2][1]=0; m1[2][2]=1; MultMatrix(m2, m1, m); CopyMatrix(m, m2); }
The Scale() function takes as parameters a reference to the current transformation matrix and the X and Y scaling factors. The function first initializes the local matrix m1 to the scaling matrix. It then multiplies the scaling matrix times the current transformation matrix, storing the results in the local matrix m2. The program finally copies m2 into the transformation matrix.
The last function you need is the matrix version of Rotate(). That function looks like Listing 3.14.
Listing 3.14 Rotating with Matrices
void Rotate(MATRIX3X3& m, int degrees) { MATRIX3X3 m1, m2; if (degrees == 0) return; double radians = 6.283185308 / (360.0 / degrees); double c = cos(radians); double s = sin(radians); m1[0][0]=c; m1[0][1]=s; m1[0][2]=0; m1[1][0]=-s; m1[1][1]=c; m1[1][2]=0; m1[2][0]=0; m1[2][1]=0; m1[2][2]=1; MultMatrix(m2, m1, m); CopyMatrix(m, m2); }
The Rotate() function takes as parameters a reference to the current transformation matrix and the number of degrees to rotate the shape. The function first checks whether degrees is zero. If it is, the function returns immediately to avoid a division-by-zero error. Then, the function converts the degrees to radians and calculates the cosine and sine of the angle. Next, Rotate() initializes the rotation matrix and multiplies that matrix times the current transformation matrix, storing the results in the local matrix m2. Finally, m2 gets copied into the transformation matrix.
Now that you have a set of matrix functions, you might want to see exactly how you would use those functions in a program to translate, scale, and rotate a shape. Listing 3.15 shows you how.
Listing 3.15 Performing Transformations with the Matrix Functions
MATRIX3X3 m; InitMatrix(m); Translate(m, 10, 15); Scale(m, 0.5, 0.5); Rotate(m, 45); Transform(shape1, m); DrawShape(shape1);
The code segment first declares a 3x3 transformation matrix called m. It then calls InitMatrix() to initialize m to an identity matrix. At this point, m looks like this:
1.0000000000000 0.0000000000000 0.0000000000000 0.0000000000000 1.0000000000000 0.0000000000000 0.0000000000000 0.0000000000000 1.0000000000000
The call to Translate() composes m with a translation matrix containing the values 10 and 15, which leaves m containing the translation. The transformation matrix, m, now looks like this:
1.0000000000000 0.0000000000000 0.0000000000000 0.0000000000000 1.0000000000000 0.0000000000000 10.000000000000 15.000000000000 1.0000000000000
After the call to Scale(), m contains the translation and scaling values:
0.5000000000000 0.0000000000000 0.0000000000000 0.0000000000000 0.5000000000000 0.0000000000000 10.000000000000 15.000000000000 1.0000000000000
Finally, the call to Rotate() leaves m containing the full transformationtranslation, scaling, and rotationfor the shape:
0.35355339055702 0.35355339062953 0.0000000000000 -0.35355339062953 0.35355339055702 0.0000000000000 10.000000000000 15.000000000000 1.0000000000000
The call to Transform() applies the translation matrix m to all of the vertices in shape1, after which DrawShape() draws the newly transformed shape onscreen.