[Not finished but I published anyway.] In the previous tutorial we changed the Wireframe to use matrices. Now we need to update the code for displaying and transforming wireframes so they work with matrices. In this tutorial we will:

- Fix the display to show the new wireframe object
- Convert the transformating functions into matrix transformations

By the end of the tutorial we should be back where we started two tutorials ago, but our code will be a lot more efficient.

As discussed previously, to translate an object means to add a constant to every one of its x-, y- or z-coordinates. A translation matrix is a 2D matrix that looks like this (where dx is the number of units you want to translate the object in the x-coordinate, dy in the y-coordinate and so on):

The reason for defining a matrix like this is so that when we multiple the node matrix by this matrix, the transformation occurs. If you're not familiar with matrix multiplication, then Wikipedia should help. Breifly, the value (i, j) in the resulting matrix is first value in the i^{th} row times the first value in the j^{th} column plus the second value in the i^{th} row times the second value in the j^{th} column and so on. This is the dot product of the vectors given by the i^{th} row and the j^{th} column. Note that this requires that the number of columns in the first matrix must equal the number of rows in the second matrix, so all our transformation matrices must have 4 rows.

For example, if we have two nodes and we multiply by the transformation matrix, the first term in the result matrix (which is the x value of the first node) is 1.x + 0.y + 0.z + dx.1, or x + dx. This is the reason why we have the row of ones in the node matrix. If you want to explore the results of multiplier matrices, I have an online matrix multipler that will accept simple variables, such as x and y (but not x1 - you have to use just letters): http://petercollingridge.appspot.com/matrix_multiplier

So this might seems an overly complicated way to do things, but in programming terms it is quite straightforward and will make more complex transformations a lot easier later on. Also, NumPy is very efficient at multiplying matrices, so it's quite quick. Real 3D graphics programs use the GPU is basically designed to do lots of matrix mutliplications all at once.

We can can give our Wireframe class a function for applying any matrix:

def transform(self, matrix): """ Apply a transformation defined by a given matrix. """ self.nodes = np.dot(self.nodes, matrix)

This uses the numpy function dot(), which multiplies two matrices. We now write a function in wireframe.py to create a translation matrix:

def translationMatrix(dx=0, dy=0, dz=0): """ Return matrix for translation along vector (dx, dy, dz). """ return np.array([[1,0,0,0], [0,1,0,0], [0,0,1,0], [dx,dy,dz,1]])

Our object can now be translated in any direction with:

matrix = translationMatrix(-10, 12, 0) cube.transform(matrix)

A scaling matrix can be defined like this:

def scaleMatrix(sx=0, sy=0, sz=0): """ Return matrix for scaling equally along all axes centred on the point (cx,cy,cz). """ return np.array([[sx, 0, 0, 0], [0, sy, 0, 0], [0, 0, sz, 0], [0, 0, 0, 1]])

If you work through the result of multiplying this matrix with some nodes, you will see that each x value is multiplied by sx, each y value by sy and each z value by sz.

The rotation matrices are given below. If you work through the multiplication of these, you'll see that, for example, rotating about the x-axis, does not affect the x-coordinates, but the y- and z-coordinates are changed by a function of both the y- and z-values.

def rotateXMatrix(radians): """ Return matrix for rotating about the x-axis by 'radians' radians """ c = np.cos(radians) s = np.sin(radians) return np.array([[1, 0, 0, 0], [0, c,-s, 0], [0, s, c, 0], [0, 0, 0, 1]]) def rotateYMatrix(radians): """ Return matrix for rotating about the y-axis by 'radians' radians """ c = np.cos(radians) s = np.sin(radians) return np.array([[ c, 0, s, 0], [ 0, 1, 0, 0], [-s, 0, c, 0], [ 0, 0, 0, 1]]) def rotateZMatrix(radians): """ Return matrix for rotating about the z-axis by 'radians' radians """ c = np.cos(radians) s = np.sin(radians) return np.array([[c,-s, 0, 0], [s, c, 0, 0], [0, 0, 1, 0], [0, 0, 0, 1]])

Now out Wireframe object have a transform() method and we've defined our transformation matrices we just need to update how Wireframedisplay works.

First we need to make a slight change to the key_to_function mapping:

key_to_function = { pygame.K_LEFT: (lambda x: x.translateAll([-10, 0, 0])), pygame.K_RIGHT:(lambda x: x.translateAll([ 10, 0, 0])), pygame.K_DOWN: (lambda x: x.translateAll([0, 10, 0])), pygame.K_UP: (lambda x: x.translateAll([0, -10, 0])),

Instead of defining a direction as an axis letter and a magnitude, we define a vector, so moving 10 units along the x-axis is defined as [10, 0, 0]. Then to apply the translation, we need to create the relevant matrix and apply it:

def translateAll(self, vector): """ Translate all wireframes along a given axis by d units. """ matrix = wf.translationMatrix(*vector) for wireframe in self.wireframes.itervalues(): wireframe.transform(matrix)

If you're surprised by the *vector command, all it's doing is converting the list of values in the vector (e.g. [10, 0, 0]) into three separate values, so when the translation matrix is made, they fill the dx, dy and dz parameters. Instead we could have written:

matrix = wf.translationMatrix(vector[0],vector[1],vector[2])

Alternatively, we could have created the four different translation matrices to start with and wrote a translateAll() function to pass them directly to wireframe.transform(), which would have been more efficient, but less flexible.