Matrix transformations

[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.

Translation matrix

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

Matrix multiplication

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 ith row times the first value in the jth column plus the second value in the ith row times the second value in the jth column and so on. This is the dot product of the vectors given by the ith row and the jth 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)

Scaling 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.

Rotation matrices

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

Applying transformations

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.

Comments

Hi Peter,

I can't figure out how to set up numpy. I've downlaoded it but when I try to import it to a program I get an import error, "No module named numpy". Do I need to do something to install numpy or do you think I just don't have it saved in the right place?

You need to install it once you have downloaded it. How you do this depends on your operating system. This might help: http://stackoverflow.com/questions/7562834/installing-numpy

I have a mac and all the instructions for instalation appear to be incredibly complicated and require me to download compilers for C and fortran. Am I just overthinking this or is the installation procces really just this involved.  

Hi Peter,

I would like to write a VERY simple program for analysis of framed structures. This would need to display node-edge type structures with zooming, panning, selection of edges by clicking/ rubber banding. I thought Pygame would be right for this until I realised that it's only 2-D. Isn't there any 3-D library to handle the typical stuff like display, selction, etc. ?

Thanks

I think there are a few Python 3D graphics libraries, but I've not tried any. I have heard panda3D is quite good: http://www.panda3d.org/

Post new comment

The content of this field is kept private and will not be shown publicly.