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 and much 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 wireframe method 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):
  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).

Calculating the angle of a node

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.

Converting from an angle to a coordinate

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 ProjectionViewer method should look like this (remember to import math at the start of the program):

def rotateZ(self, model, radians):
    (cx,cy,cz) = self.models[model].findCentre()
    
    for node in self.models[model].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 with:

pv.rotateZ('cube', 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, model, radians):
    (cx,cy,cz) = self.models[model].findCentre()
    
    for node in self.models[model].nodes:
        y      = node.y - cy
        z      = node.z - cz
        d      = math.hypot(y, z)
        theta  = math.atan2(y, z) + radians
        node.y = cy + d * math.sin(theta)
        node.z = cz + d * math.cos(theta)

def rotateY(self, model, radians):
    (cx,cy,cz) = self.models[model].findCentre()
    
    for node in self.models[model].nodes:
        x      = node.x - cx
        z      = node.z - cz
        d      = math.hypot(x, z)
        theta  = math.atan2(x, z) + radians
        node.x = cx + d * math.sin(theta)
        node.z = cz + d * math.cos(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 show you how to uses matrices to deal with tranformations more efficiently.

We can update our key_to_function dictionary to bind these new function calls to key presses. For example:

key_to_function = {
...
    pygame.K_q: (lambda x, obj: x.rotateX(obj,  0.1)),
    pygame.K_w: (lambda x, obj: x.rotateX(obj, -0.1)),
    pygame.K_a: (lambda x, obj: x.rotateY(obj,  0.1)),
    pygame.K_s: (lambda x, obj: x.rotateY(obj, -0.1)),
    pygame.K_z: (lambda x, obj: x.rotateZ(obj,  0.1)),
    pygame.K_x: (lambda x, obj: x.rotateZ(obj, -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.

AttachmentSize
displayWireframe3.txt4.27 KB
wireframe2.txt1.71 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.

Post new comment

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