Pygame rotating cubes around axis

geometry, pygame, python

Solution

It is not sufficient to sort the faces of each cube separately by its depth. You've to sort the faces of all objects of the entire scene by its depth.

Create a list of tuples, which consists of the projected (transformed) points ofa face and the average depth (z value):

polygons = []
for cube in self._cubes:
    transformed_vectors = cube.transform_vectors(self._angle)
    avg_z = cube.calculate_average_z(transformed_vectors)
    for z in avg_z:
        face_index = z[0]
        face = cube._faces[face_index]
        pointlist = cube.create_polygon(face, transformed_vectors)
        polygons.append((pointlist, z[1]))

Draw the faces of all objects in (reverse) sorted order:

for poly in sorted(polygons, key=lambda x: x[1], reverse=True):
    pygame.draw.polygon(self.screen, Color.SILVER.value,poly[0])
    pygame.draw.polygon(self.screen, Color.BLACK.value, poly[0], 3)

Minimal example: repl.it/@Rabbid76/PyGame-3D

import math
import pygame

def project(vector, w, h, fov, distance):
    factor = math.atan(fov / 2 * math.pi / 180) / (distance + vector.z)
    x = vector.x * factor * w + w / 2
    y = -vector.y * factor * w + h / 2
    return pygame.math.Vector3(x, y, vector.z)

def rotate_vertices(vertices, angle, axis):
    return [v.rotate(angle, axis) for v in vertices]
def scale_vertices(vertices, s):
    return [pygame.math.Vector3(v[0]*s[0], v[1]*s[1], v[2]*s[2]) for v in vertices]
def translate_vertices(vertices, t):
    return [v + pygame.math.Vector3(t) for v in vertices]
def project_vertices(vertices, w, h, fov, distance):
    return [project(v, w, h, fov, distance) for v in vertices]

class Mesh():

    def __init__(self, vertices, faces):
        self.__vertices = [pygame.math.Vector3(v) for v in vertices]
        self.__faces = faces

    def rotate(self, angle, axis):
        self.__vertices = rotate_vertices(self.__vertices, angle, axis)
    def scale(self, s):
        self.__vertices = scale_vertices(self.__vertices, s)
    def translate(self, t):
        self.__vertices = translate_vertices(self.__vertices, t)

    def calculate_average_z(self, vertices):
        return [(i, sum([vertices[j].z for j in f]) / len(f)) for i, f in enumerate(self.__faces)]

    def get_face(self, index):
        return self.__faces[index]
    def get_vertices(self):
        return self.__vertices

    def create_polygon(self, face, vertices):
        return [(vertices[i].x, vertices[i].y) for i in [*face, face[0]]]
       
class Scene:
    def __init__(self, mehses, fov, distance):
        self.meshes = mehses
        self.fov = fov
        self.distance = distance 
        self.euler_angles = [0, 0, 0]

    def transform_vertices(self, vertices, width, height):
        transformed_vertices = vertices
        axis_list = [(1, 0, 0), (0, 1, 0), (0, 0, 1)]
        for angle, axis in reversed(list(zip(list(self.euler_angles), axis_list))):
            transformed_vertices = rotate_vertices(transformed_vertices, angle, axis)
        transformed_vertices = project_vertices(transformed_vertices, width, height, self.fov, self.distance)
        return transformed_vertices

    def draw(self, surface):
        
        polygons = []
        for mesh in self.meshes:
            transformed_vertices = self.transform_vertices(mesh.get_vertices(), *surface.get_size())
            avg_z = mesh.calculate_average_z(transformed_vertices)
            for z in avg_z:
            #for z in sorted(avg_z, key=lambda x: x[1], reverse=True):
                pointlist = mesh.create_polygon(mesh.get_face(z[0]), transformed_vertices)
                polygons.append((pointlist, z[1]))
                #pygame.draw.polygon(surface, (128, 128, 192), pointlist)
                #pygame.draw.polygon(surface, (0, 0, 0), pointlist, 3)

        for poly in sorted(polygons, key=lambda x: x[1], reverse=True):
            pygame.draw.polygon(surface, (128, 128, 192), poly[0])
            pygame.draw.polygon(surface, (0, 0, 0), poly[0], 3)
        

vertices = [(-1,-1,1), (1,-1,1), (1,1,1), (-1,1,1), (-1,-1,-1), (1,-1,-1), (1,1,-1), (-1,1,-1)]
faces = [(0,1,2,3), (1,5,6,2), (5,4,7,6), (4,0,3,7), (3,2,6,7), (1,0,4,5)]

cube_origins = [(-1, -1, 0), (0, -1, 0), (1, -1, 0), (1, 0, 0), (1, 1, 0), (0, 1, 0), (-1, 1, 0), (-1, 0, 0)]
meshes = []
for origin in cube_origins:
    cube = Mesh(vertices, faces)
    cube.scale((0.5, 0.5, 0.5))
    cube.translate(origin)
    meshes.append(cube)

scene = Scene(meshes, 90, 5)

pygame.init()
window = pygame.display.set_mode((400, 300))
clock = pygame.time.Clock()

run = True
while run:
    clock.tick(60)
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            run = False

    window.fill((255, 255, 255))
    scene.draw(window)
    scene.euler_angles[1] += 1
    pygame.display.flip()

pygame.quit()

Problem

I have been playing around with the example of a rotating cube here. I have generated 2 cubes that should rotate around the Y-axis. However, it doesn't seem to work as expected and I can't figure out what the problem of it is. Here is a working code example: ``` import sys import math import pygame from pygame.math import Vector3 from enum import Enum class Color(Enum): BLACK = (0, 0, 0) SILVER = (192,192,192) class Cube(): def __init__(self, vectors, screen_width, screen_height, initial_angle=25): self._vectors = vectors self._angle = initial_angle self._screen_width = screen_width self._screen_height = screen_height # Define the vectors that compose each of the 6 faces self._faces = [(0,1,2,3), (1,5,6,2), (5,4,7,6), (4,0,3,7), (0,4,5,1), (3,2,6,7)] self._setup_initial_positions(initial_angle) def _setup_initial_positions(self, angle): tmp = [] for vector in self._vectors: rotated_vector = vector.rotate_x(angle).rotate_y(angle)#.rotateZ(self.angle) tmp.append(rotated_vector) self._vectors = tmp def transform_vectors(self, new_angle): # It will hold transformed vectors. transformed_vectors = [] for vector in self._vectors: # Rotate the point around X axis, then around Y axis, and finally around Z axis. mod_vector = vector.rotate_y(new_angle) # Transform the point from 3D to 2D mod_vector = self._project(mod_vector, self._screen_width, self._screen_height, 256, 4) # Put the point in the list of transformed vectors transformed_vectors.append(mod_vector) return transformed_vectors def _project(self, vector, win_width, win_height, fov, viewer_distance): factor = fov / (viewer_distance + vector.z) x = vector.x * factor + win_width / 2 y = -vector.y * factor + win_height / 2 return Vector3(x, y, vector.z) def calculate_average_z(self, vectors): avg_z = [] for i, face in enumerate(self._faces): # for each point of a face calculate the average z value z = (vectors[face[0]].z + vectors[face[1]].z + vectors[face[2]].z + vectors[face[3]].z) / 4.0 avg_z.append([i, z]) return avg_z def get_face(self, index): return self._faces[index] def create_polygon(self, face, transformed_vectors): return [(transformed_vectors[face[0]].x, transformed_vectors[face[0]].y), (transformed_vectors[face[1]].x, transformed_vectors[face[1]].y), (transformed_vectors[face[2]].x, transformed_vectors[face[2]].y), (transformed_vectors[face[3]].x, transformed_vectors[face[3]].y), (transformed_vectors[face[0]].x, transformed_vectors[face[0]].y)] class Simulation: def __init__(self, win_width=640, win_height=480): pygame.init() self.screen = pygame.display.set_mode((win_width, win_height)) self.clock = pygame.time.Clock() cube = Cube([ Vector3(0, 0.5, -0.5), Vector3(0.5, 0.5, -0.5), Vector3(0.5, 0, -0.5), Vector3(0, 0, -0.5), Vector3(0, 0.5, 0), Vector3(0.5, 0.5, 0), Vector3(0.5, 0, 0), Vector3(0, 0, 0) ], win_width, win_height) cube2 = Cube([ Vector3(0.5, 0.5, -0.5), Vector3(1, 0.5, -0.5), Vector3(1, 0, -0.5), Vector3(0.5, 0, -0.5), Vector3(0.5, 0.5, 0), Vector3(1, 0.5, 0), Vector3(1, 0, 0), Vector3(0.5, 0, 0) ], win_width, win_height) self._angle = 30 self._cubes = [cube, cube2] def run(self): while True: for event in pygame.event.get(): if event.type == pygame.QUIT: pygame.quit() sys.exit() self.clock.tick(50) self.screen.fill(Color.BLACK.value) for cube in self._cubes: transformed_vectors = cube.transform_vectors(self._angle) avg_z = cube.calculate_average_z(transformed_vectors) # Draw the faces using the Painter's algorithm: # Distant faces are drawn before the closer ones. for avg_z in sorted(avg_z, key=lambda x: x[1], reverse=True): face_index = avg_z[0] face = cube._faces[face_index] pointlist = cube.create_polygon(face, transformed_vectors) pygame.draw.polygon(self.screen, Color.SILVER.value,pointlist) pygame.draw.polygon(self.screen, Color.BLACK.value, pointlist, 3) # break self._angle += 1 pygame.display.flip() if __name__ == "__main__": Simulation().run() ``` Both cubes should rotate around the Y-axis in this example. For the future I'd like to have a solution so they can rotate around any axis.

Original source