Saturday, 17th December 2011

# Rotation in 3D

In the previous tutorial, we added the ability to apply some basic transformations to our wireframe cube, but it still looked like a square. In this tutorial, we will:

- Add the ability to rotate wireframes about three axes

Rotations are more complex that the previous two transformations but more interesting. Translations add a constant to coordinates, while scaling multiples a constant by the coordinate, so both preserve the shape of the projection (i.e. square). Rotations on the other hand change the values of two coordiates e.g. x and y, by a function of both those coordinates. This means that the coordinates effectively interact, so the z-coordinate will come into play by influencing the value of the x and y coordinates. Thus we will finally see a different side of our 3D object. Hopefully this will become clear with an example.

## Defining an axis

Rotations are defined by an angle and a vector through which the rotation occurs. The easiest example is to rotate the cube through an axis parallel to the z-axis. For example, as we look at our cube end on, we rotate the square we see about its centre. As we are rotating about the z-axis, only the x and y coordinates will change, so we won't see our z-coordinates just yet. So it is essentially a 2D problem.

First let's create a method for Wireframe to find its centre. The centre is just the mean of the x, y and z coordinates. In the Wireframe class add:

def findCentre(self): """ Find the centre of the wireframe. """ num_nodes = len(self.nodes) meanX = sum([node.x for node in self.nodes]) / num_nodes meanY = sum([node.y for node in self.nodes]) / num_nodes meanZ = sum([node.z for node in self.nodes]) / num_nodes return (meanX, meanY, meanZ)

## Converting from Cartesian to a polar coordinates

Since we are going to rotate points about an angle, it's easier to switch to using polar coordinates. This means rather than refer to a point as being x units along the screen and y units up the screen, we refer to it as being an angle and distance from the point of rotation. For example, below, we convert (x, y) to (θ, d).

We can find the angle, θ (in radians) using a handy function from the math module called `atan2()`

, which also deals with orthogonal situations. The angle measured, is between the vector and the x-axis. This isn't particularly important, so long as we're consistent when we convert back to a coordinate later. The distance, d, in the diagram is calculated as the hypotenuse of the triangle formed by the vector:

import math d = math.hypot(y-cy, x-cx) theta = math.atan2(y-cy, x-cx)

## Converting from polar to a Cartesian coordinates

Now we have an angle, θ, we add the angle of our rotation, then convert back to Cartesian coordinates.

The new x-coordinate is the width of the triangle (the distance between cy and y), which is, by simple trigonometry, d.cos(θ). The new y-coordinate is the height of the triangle, which is d.sin(θ). The complete Wireframe method should look like this (remember to `import math`

at the start of the program):

def rotateZ(self, (cx,cy,cz), radians): for node in self.nodes: x = node.x - cx y = node.y - cy d = math.hypot(y, x) theta = math.atan2(y, x) + radians node.x = cx + d * math.cos(theta) node.y = cy + d * math.sin(theta)

We can now rotate the projected square about its centre with:

cube.rotateZ(cube.findCentre(), 0.1)

We still haven't yet seen into the third dimension, but by analogy to a rotation around a vector parallel to the z-axis, we can also rotate our object about vectors parallel to the x- and y-axes with the following methods:

def rotateX(self, (cx,cy,cz), radians): for node in self.nodes: y = node.y - cy z = node.z - cz d = math.hypot(y, z) theta = math.atan2(y, z) + radians node.z = cz + d * math.cos(theta) node.y = cy + d * math.sin(theta) def rotateY(self, (cx,cy,cz), radians): for node in self.nodes: x = node.x - cx z = node.z - cz d = math.hypot(x, z) theta = math.atan2(x, z) + radians node.z = cz + d * math.cos(theta) node.x = cx + d * math.sin(theta)

Now - finally - we can see all aspects of our cube. Having three separate rotation methods is not the most efficient way to do things - in a later tutorial I demonstrate how matrices can deal with tranformations more efficiently.

## Key Controls

Next we need to add a rotate function to the ProjectionViewer class. The method below rotates all wireframes about their centres and a given axis. Depending on what you want, it may be more useful to rotate all objects about the origin or about the centre of the screen.

def rotateAll(self, axis, theta): """ Rotate all wireframe about their centre, along a given axis by a given angle. """ rotateFunction = 'rotate' + axis for wireframe in self.wireframes.itervalues(): centre = wireframe.findCentre() getattr(wireframe, rotateFunction)(centre, theta)

Finally we can update our `key_to_function`

dictionary to bind these new function calls to keys. For example:

key_to_function = { ... pygame.K_q: (lambda x: x.rotateAll('X', 0.1)), pygame.K_w: (lambda x: x.rotateAll('X', -0.1)), pygame.K_a: (lambda x: x.rotateAll('Y', 0.1)), pygame.K_s: (lambda x: x.rotateAll('Y', -0.1)), pygame.K_z: (lambda x: x.rotateAll('Z', 0.1)), pygame.K_x: (lambda x: x.rotateAll('Z', -0.1))}

So at last we can rotate our cube and see it from every angle. In the next tutorial we'll switch to using matrices so we can carry out transformations faster so we can look at object more complex than a cube.

Attachment | Size |
---|---|

displayWireframe3.txt | 3.62 KB |

wireframe3.txt | 3.26 KB |

## Comments

Very good article. I found it very helpful, and I am looking forward to the completion of part 4.

Cool stuff! Are you going to go into perspective next? That would be super useful :).

Thanks! I know I haven't updated this in a while, but I hope to soon, including perspective and proper matrix transformations. I'll try to write it soon.

Can you make a shading tutorial on how to shade surfaces such as a cube?

Just a minor note, when discussing the trig terms, you mixed up x and y. You said that the x-coordinate is the height of the triangle, and y-coordinate the width. Your code is obviously right, just the writing is off. This is just errata, this is THE best tutorial that I have seen for this kind of stuff. You are truly enlightening at this stuff.

Thanks for that, you're right of course. I was being careless. I've updated the text.

Which software do you use in order to create images like this: http://www.petercollingridge.co.uk/sites/files/peter/rotate_square2.png?

The images were created using Inkscape, which is free to download from inkscape.org.

Thanks for this, im using a different programming language, but the techniques and explanations are still applicable and very helpful

Hi

Very helpful article... awesome

This is a wonderful tutorial, just for the sake of accuracy, in your rotateX function you have y and z backwards. I alos replaced my rotate functions with this:

def rotate(self,axis,center,radians):

axies = ['x','y','z']

spot = axies.index(axis)

axies = axies[spot+1:]+axies[:spot+1]

center = center[spot+1:]+center[:spot+1]

for node in self.nodes:

a = getattr(node,axies[1])-center[1]

b = getattr(node,axies[0])-center[0]

d = math.hypot(a,b)

theta = math.atan2(a,b)+radians

setattr(node,axies[0],center[0]+d*math.cos(theta))

setattr(node,axies[1],center[1]+d*math.sin(theta))

please continue with this. These tutorials are really the best i have found on pygame.

I am really keen to study every bit of these. I really am a beginner, I mean i know the basics of python and pygame but this should really help me to grasp and be able to practive some useful concepts.

Thankyou (and please complete this 3d one)

and make more, these are

great!!## Post new comment