In this tutorial I will show you how to build a simulation of 3D point rotation using Python and Pygame. I will begin by giving you a brief background on 3d computer graphics theory. It will help you understand the code that will be presented afterwards. I assume you have a basic knowledge of Pygame. If you are new to Pygame consider reading first the article A Fast Introduction to Pygame. The video below shows the simulation in action.
To make 3D simulations, first of all, we must describe the objects in 3D space. The most common way of describing 3D objects is through the use of a 3D cartesian coordinate system. We will use the system shown in the figure below, where Y increases going up, X increases going right, and Z increases going into the screen. This is called a left handed system. An alternative system, is the right handed system where Z increases in the opposite direction.
Next, with points defined in 3D cartesian coordinate system, we can apply transformations such as translation, rotation, and scaling. Note that unlike 2D rotations that occur in one plane, rotations in 3D space occur along an arbitrary axis. Below I present the formulas for 3D rotation along the X, Y, and Z axes.
Rotation along X: y' = y*cos(a) - z*sin(a) z' = y*sin(a) + z*cos(a) x' = x Rotation along Y: z' = z*cos(a) - x*sin(a) x' = z*sin(a) + x*cos(a) y' = y Rotation along Z: x' = x*cos(a) - y*sin(a) y' = x*sin(a) + y*cos(a) z' = z
When we are finished applying the transformations, we must draw the 3D object into the screen. But, since the screen is 2D we will have to convert the 3D coordinates to 2D coordinates. This operation is called 3D projection, and there are many ways of doing it. I will cover just the common one that is the perspective projection. Below I present the formula.
3D Perspective Projection x' = x * fov / (z + viewer_distance) + half_screen_width y' = -y * fov / (z + viewer_distance) + half_screen_height z' -> for now, z is useless
In the formula, fov is a constant value that defines the “field of vision”. I like to use values between 128 and 256. view_distance is the distance from the object to the viewer.
With all the points converted to 2D space, we can finally draw the object using Pygame 2D drawing functions.
I present below the code to simulate the rotation of 8 points/vertices of a cube. To start with, let’s define the Point3D class to represent points in 3D space:
import sys, math, pygame class Point3D: def __init__(self, x = 0, y = 0, z = 0): self.x, self.y, self.z = float(x), float(y), float(z) def rotateX(self, angle): """ Rotates the point around the X axis by the given angle in degrees. """ rad = angle * math.pi / 180 cosa = math.cos(rad) sina = math.sin(rad) y = self.y * cosa - self.z * sina z = self.y * sina + self.z * cosa return Point3D(self.x, y, z) def rotateY(self, angle): """ Rotates the point around the Y axis by the given angle in degrees. """ rad = angle * math.pi / 180 cosa = math.cos(rad) sina = math.sin(rad) z = self.z * cosa - self.x * sina x = self.z * sina + self.x * cosa return Point3D(x, self.y, z) def rotateZ(self, angle): """ Rotates the point around the Z axis by the given angle in degrees. """ rad = angle * math.pi / 180 cosa = math.cos(rad) sina = math.sin(rad) x = self.x * cosa - self.y * sina y = self.x * sina + self.y * cosa return Point3D(x, y, self.z) def project(self, win_width, win_height, fov, viewer_distance): """ Transforms this 3D point to 2D using a perspective projection. """ factor = fov / (viewer_distance + self.z) x = self.x * factor + win_width / 2 y = -self.y * factor + win_height / 2 return Point3D(x, y, 1)
First,we have the constructor that initializes instances of the class. Next, we have methods to rotate a point around X, Y, and Z axes. These functions return the rotated point. Finally, we have the function that projects a point from 3D to 2D space.
Next, we define the Simulation class that will do the rest of the job:
class Simulation: def __init__(self, win_width = 640, win_height = 480): pygame.init() self.screen = pygame.display.set_mode((win_width, win_height)) pygame.display.set_caption("Simulation of 3D Point Rotation (http://codentronix.com)") self.clock = pygame.time.Clock() self.vertices = [ Point3D(-1,1,-1), Point3D(1,1,-1), Point3D(1,-1,-1), Point3D(-1,-1,-1), Point3D(-1,1,1), Point3D(1,1,1), Point3D(1,-1,1), Point3D(-1,-1,1) ] self.angleX, self.angleY, self.angleZ = 0, 0, 0 def run(self): while 1: for event in pygame.event.get(): if event.type == pygame.QUIT: sys.exit() self.clock.tick(50) self.screen.fill((0,0,0)) for v in self.vertices: # Rotate the point around X axis, then around Y axis, and finally around Z axis. r = v.rotateX(self.angleX).rotateY(self.angleY).rotateZ(self.angleZ) # Transform the point from 3D to 2D p = r.project(self.screen.get_width(), self.screen.get_height(), 256, 4) x, y = int(p.x), int(p.y) self.screen.fill((255,255,255),(x,y,2,2)) self.angleX += 1 self.angleY += 1 self.angleZ += 1 pygame.display.flip() if __name__ == "__main__": Simulation().run()
When this class is instantiated, it starts with initializing Pygame, and then setting up a window. Next, we create a clock object to lock the frame rate of the animation. Then, we define 8 points in 3D space. After that, we define the variables that will hold angles of rotation around each axis.
The run() function runs the main loop where the points are rotated and drawn. First, it checks if a QUIT event is pending, and if so quits the application. Then, it locks the frame rate to 50 FPS, and after that clears the screen. Then, it traverses the vertex list, rotating each one around X, Y, and Z axes. Then, it projects the point, and draws it into the screen. After traversing the vertex list, finally, it increases the angle variables, and then updates the screen.
The last line of code runs the simulation.
Click here to download the full source code.
With a little bit of 3d computer graphics theory, we were able to make a simple 3d simulation using Pygame. In the next tutorial we will expand on the theory and code from this tutorial to make a rotating wireframe cube.