Basic 3D transformations

In the previous tutorial we displayed a static cube wireframe, which appeared as a square. In order to get a sense of its three dimensions, we must be able to move it in three dimension. But first, we'll introduce some basic transformations, which don't require a third dimension. In this tutorial, we will:

  • Add the ability to translate wireframes
  • Add the ability to scale wireframes
  • Apply these transformations using the keyboard

By the end of the tutorial, we'll be able to move and scale the square as shown in the video below. The final code is at the bottom of the page as text files.

Translation in 3D

The simplest transformation is a translation: moving the wireframe along an axis by adding a constant to the x, y or z coordinate of every node. For this, we add the following method to the Wireframe class:

def translate(self, axis, d):
    """ Add constant 'd' to the coordinate 'axis' of each node of a wireframe """
        
    if axis in ['x', 'y', 'z']:
        for node in self.nodes:
            setattr(node, axis, getattr(node, axis) + d)

The translate() method takes the name of an axis and distance that the wireframe should be translated. It adds the distance, d, to the relevant coordinate of every node in the wireframe. We use getattr() and setattr() so we can easily define which attribute we want to change.

For example, to move our cube 100 pixels to the right, we get and set the attribute 'x':

cube.translate('x', 100)

Translating our cube along the x-axis

Because the y-axis of the screen starts at the top and points down, to move our cube up 40 pixels, we call:

cube.translate('y', -40)

Translating along the z-axis will have no noticeable effect at the moment.

Scaling in 3D

Scaling is also relatively straightforward. We could simply multiply the x, y and z values of each node by a scaling factor, which would have the effect of scaling the cube centred on the origin. However, for more flexibility we can scale from any centre. using the following method:

def scale(self, (centre_x, centre_y), scale):
    """ Scale the wireframe from the centre of the screen """

    for node in self.nodes:
        node.x = centre_x + scale * (node.x - centre_x)
        node.y = centre_y + scale * (node.y - centre_y)
        node.z *= scale

For example, if we know the centre of the screen then the function scales the distance of each node from the centre (ignoring the z coordinate). If we assume that the screen is at z=0, then nodes behind the screen move closer as we scale down, and further away as we scale up.

Scaling a square

For example, to scale our cube by three quarters, centred on the screen's centre:

cube.scale((200, 150), 0.75)

Keyboard controls

In order to easily use these transformations we can associate them with keys and call them (with some arbitrary value) in response to key presses. We've arranged our code so that the display of the wireframes using Pygame is in a file called displayWireframe2.py. This code should also handle keyboard inputs using Pygame. First we'll add some methods to the ProjectionViewer class to transform all its wireframes (and to calculate the centre of the screen):

def translateAll(self, axis, d):
    """ Translate all wireframes along a given axis by d units. """

    for wireframe in self.wireframes.itervalues():
        wireframe.translate(axis, d)

def scaleAll(self, scale):
    """ Scale all wireframes by a given scale, centred on the centre of the screen. """

    centre_x = self.width/2
    centre_y = self.height/2

    for wireframe in self.wireframes.itervalues():
        wireframe.scale((centre_x, centre_y), scale)

We can then associate these methods with keys with a dictionary that maps keys to lambda function. I've written in more detail on how this code works here. You can use whichever set of keys you find most logical.

key_to_function = {
    pygame.K_LEFT:   (lambda x: x.translateAll('x', -10)),
    pygame.K_RIGHT:  (lambda x: x.translateAll('x',  10)),
    pygame.K_DOWN:   (lambda x: x.translateAll('y',  10)),
    pygame.K_UP:     (lambda x: x.translateAll('y', -10)),
    pygame.K_EQUALS: (lambda x: x.scaleAll(1.25)),
    pygame.K_MINUS:  (lambda x: x.scaleAll( 0.8))}

Finally we need to check whether a key is pressed, and if it is, and it's one that's in key_to_function, we call the relevant function. We do this by adding the following code into the loop in the ProjectionViewer's run() method:

elif event.type == pygame.KEYDOWN:
    if event.key in key_to_function:
        key_to_function[event.key](self)

This is a slightly indirect way to do things - the function is called, sending the ProjectionViewer object (refered to by self), to the lambda function, which then calls the translate or scale function of ProjectionViewer. The advantage of this method is that it makes it easier to add or change key commands.

We can now manipulate our cube wireframe to a degree, but it still looks like a square. In the next tutorial, we'll introduce another transformation - rotation - which will finally allow us to see another side to our square and see that it really is a cube.

AttachmentSize
wireframe2.txt1.96 KB
displayWireframe2.txt2.95 KB

Post new comment

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