0
$\begingroup$

I'm trying to write my own 3D engine using Python and Pygame.

I have successfully implemented rotation and projection matrices to display 3D models. However I am so far unable to implement translation and scaling matrices, I have instead been opting for manually displace the model by Z-axis (after rotation has been applied).

Also resizing the object and displacing it by X/Y-axises (after applying projection).

After trying to add scaling and translation matrices and also a camera, this is the result:

With only rotation and perspective matrices and manual displacement:

Apologiez for posting so much code but I'm truly stuck right now. I've tried to add as many comments as possible.

Very thankful for any help!

Code with camera and translation/scaling matrices:

import pygame
import math 
import copy
import random
import obj_loader as obj

pygame.init()

screen_width = 1920
screen_height = 1080
screen = pygame.display.set_mode([screen_width, screen_height])
clock = pygame.time.Clock()
FPS = 60
run = True

class Renderer():
    def __init__(self):
        
        # random colors for model sides
        self.colors = []
        for _ in range(6):
            self.colors.append([random.randint(0,255), random.randint(0,255), random.randint(0,255)])

        #  Models:
        # "default" (cube)
        # "cube"
        # "pylon"
        # "wireframe" (cube)
        # "teapot"

        model = "default"
        self.rotation = [0,1,0]
        self.rotation_speed = 0.015
        self.wireframes = 0
        self.color_fill = 1

        self.rotx = 0; self.roty = 0; self.rotz = 0

        # use basic cube or load model from obj-file
        if model == "default":
            
            self.size = 1

            self.model = [
            [[0, 0, 0, 1],    [0, 1, 0, 1],    [1, 1, 0, 1], self.colors[0]],
            [[0, 0, 0, 1],    [1, 1, 0, 1],    [1, 0, 0, 1], self.colors[0]],                                 
            [[1, 0, 0, 1],    [1, 1, 0, 1],    [1, 1, 1, 1], self.colors[1]],
            [[1, 0, 0, 1],    [1, 1, 1, 1],    [1, 0, 1, 1], self.colors[1]],                                       
            [[1, 0, 1, 1],    [1, 1, 1, 1],    [0, 1, 1, 1], self.colors[2]],
            [[1, 0, 1, 1],    [0, 1, 1, 1],    [0, 0, 1, 1], self.colors[2]],                                         
            [[0, 0, 1, 1],    [0, 1, 1, 1],    [0, 1, 0, 1], self.colors[3]],
            [[0, 0, 1, 1],    [0, 1, 0, 1],    [0, 0, 0, 1], self.colors[3]],                                          
            [[0, 1, 0, 1],    [0, 1, 1, 1],    [1, 1, 1, 1], self.colors[4]],
            [[0, 1, 0, 1],    [1, 1, 1, 1],    [1, 1, 0, 1], self.colors[4]],                                         
            [[1, 0, 1, 1],    [0, 0, 1, 1],    [0, 0, 0, 1], self.colors[5]],
            [[1, 0, 1, 1],    [0, 0, 0, 1],    [1, 0, 0, 1], self.colors[5]]]

            for tri in self.model:
                for point in tri:
                    point[0] -= 0.5; point[1] -= 0.5; point[2] -= 0.5

        # load from obj-file
        else:

            self.size = 3
            self.model = obj.load_obj(model)

            # add random colors to model sides
            self.colors = []
            amount_vertices = len(self.model)
            for _ in range(amount_vertices):
                self.colors.append([random.randint(0,255), random.randint(0,255), random.randint(0,255)])

            for i, tri in enumerate(self.model):
                tri.append(self.colors[i])
        
        # create empty projection matrix
        self.projection = []
        for _ in range(4):
            self.projection.append([0] * 4) 

        self.aspect_ratio = screen_width/screen_height
        n = 0.1
        f = 1000     

        # projection matrix
        self.projection[0][0] = 1 / (math.tan(math.radians(90/2)) * self.aspect_ratio)
        self.projection[0][1] = 0
        self.projection[0][2] = 0
        self.projection[0][3] = 0
        
        self.projection[1][0] = 0
        self.projection[1][1] = 1 / math.tan(math.radians(90/2))
        self.projection[1][2] = 0
        self.projection[1][3] = 0

        self.projection[2][0] = 0
        self.projection[2][1] = 0
        self.projection[2][2] = f / (f - n)
        self.projection[2][3] = (-f * n) / (f - n)

        self.projection[3][0] = 0
        self.projection[3][1] = 0
        self.projection[3][2] = 1
        self.projection[3][3] = 0

        # camera variables and matrix
        self.camera_pos = [0,0,-1]
        self.u          = [1,0,0]
        self.v          = [0,1,0]
        self.n          = [0,0,1]

        self.camera = [
            [self.u[0], self.u[1], self.u[2], -self.camera_pos[0]],
            [self.v[0], self.v[1], self.v[2], -self.camera_pos[1]],
            [self.n[0], self.n[1], self.n[2], -self.camera_pos[2]],
            [0,         0,         0,         1                  ]
        ]

        # create rotation matrices
        self.update_roty(); self.update_roty(); self.update_rotz()

        # translation matrix
        self.translation = [
            [1, 0, 0, 100],
            [0, 1, 0, 100],
            [0, 0, 1, 0],
            [0, 0, 0, 1]
        ]
        
        # scaling matrix
        scale = 100
        self.scaling = [
            [scale, 0,     0,     0],
            [0,     scale, 0,     0],
            [0,     0,     scale, 0],
            [0,     0,     0,     1]
        ]

    # rotation X matrix
    def update_rotx(self):
        self.rotation_x = [
            [1, 0, 0, 0],
            [0, math.cos(self.rotx), math.sin(self.rotx), 0],
            [0, -math.sin(self.rotx), math.cos(self.rotx), 0],
            [0, 0, 0, 0]]

    # rotation Y matrix
    def update_roty(self):
        self.rotation_y = [
            [math.cos(self.roty), 0, math.sin(self.roty), 0],
            [0, 1, 0, 0],
            [-math.sin(self.roty), 0, math.cos(self.roty), 0],
            [0, 0, 0, 0]]
        
    # rotation Z matrix
    def update_rotz(self):
        self.rotation_z = [
            [math.cos(self.rotz), -math.sin(self.rotz), 0, 0],
            [math.sin(self.rotz), math.cos(self.rotz), 0, 0],
            [0, 0, 1, 0],
            [0, 0, 0, 0]]

    # Vector matrix multiplication
    def Matrix_MultiplyVector(self, i, m):

        vx = i[0] * m[0][0] + i[1] * m[0][1] + i[2] * m[0][2] + m[0][3]
        vy = i[0] * m[1][0] + i[1] * m[1][1] + i[2] * m[1][2] + m[1][3]
        vz = i[0] * m[2][0] + i[1] * m[2][1] + i[2] * m[2][2] + m[2][3]
        vw = i[0] * m[3][0] + i[1] * m[3][1] + i[2] * m[3][2] + m[3][3]

        if vw != 0:
            vx /= vw
            vy /= vw
            vz /= vw
        return [vx,vy,vz,vw]

    # main render function
    def draw(self):

        # update rotation matrices
        self.rotx += self.rotation_speed
        self.roty += self.rotation_speed
        self.rotz += self.rotation_speed
        self.update_rotx(); self.update_roty(); self.update_rotz()

        triangles_to_draw = []

        # loop through triangles in model
        for tri in self.model:
            
            # create copy of triangle to project
            tricopy = copy.deepcopy(tri)
            proj_points = []

            # skip 4th element in triangle because it's color
            for i, _ in enumerate(tri):
                if i == 3:
                    continue
                
                # translate triangles
                tricopy[i] = self.Matrix_MultiplyVector(tricopy[i], self.scaling)

                # rotate triangles
                if self.rotation[0] == 1:
                    tricopy[i] = self.Matrix_MultiplyVector(tricopy[i], self.rotation_x)
                if self.rotation[1] == 1:
                    tricopy[i] = self.Matrix_MultiplyVector(tricopy[i], self.rotation_y)
                if self.rotation[2] == 1:
                    tricopy[i] = self.Matrix_MultiplyVector(tricopy[i], self.rotation_z)
                
                # scale triangles
                tricopy[i] = self.Matrix_MultiplyVector(tricopy[i], self.translation)
                

            # calculate normal of triangle
            line1 = [
                tricopy[1][0] - tricopy[0][0],
                tricopy[1][1] - tricopy[0][1],
                tricopy[1][2] - tricopy[0][2],
            ]

            line2 = [
                tricopy[2][0] - tricopy[0][0],
                tricopy[2][1] - tricopy[0][1],
                tricopy[2][2] - tricopy[0][2],
            ]

            Nx = line1[1] * line2[2] - line1[2] * line2[1]
            Ny = line1[2] * line2[0] - line1[0] * line2[2]
            Nz = line1[0] * line2[1] - line1[1] * line2[0]
            l = math.sqrt(Nx * Nx + Ny * Ny + Nz * Nz)
            Nx  /= l; Ny /= l; Nz /= l 

            # draw only if triangles normal compared with cameras normal is < 0
            if (Nx * (tricopy[0][0] - self.camera_pos[0]) + Ny * (tricopy[0][1] - self.camera_pos[1]) + Nz * (tricopy[0][2] - self.camera_pos[2])) < 0:

                for i, _ in enumerate(tri):
                    # skip 4th element in triangle because it's color
                    if i == 3: 
                        continue

                    # camera and projection matrices
                    tricopy[i] = self.Matrix_MultiplyVector(tricopy[i], self.camera)
                    tricopy[i] = self.Matrix_MultiplyVector(tricopy[i], self.projection)

                    
                # calculate mean Z value and add triangles projected points to list
                mean_z = (tricopy[0][2] + tricopy[1][2] + tricopy[2][2]) / 3
                proj_points = [(tricopy[0][0], tricopy[0][1]), (tricopy[1][0], tricopy[1][1]), (tricopy[2][0], tricopy[2][1]), tricopy[3], mean_z]
                triangles_to_draw.append(proj_points)
        
        # sort by mean Z value
        triangles_to_draw.sort(key=lambda i: (i[4]), reverse=True)
        
        # draw triangles
        for i, tri in enumerate(triangles_to_draw):

            if self.color_fill == 1:
                try:
                    pygame.draw.polygon(screen, tri[3], (tri[0], tri[1], tri[2]))
                except:
                    print(tri[3])
            if self.wireframes == 1:
                pygame.draw.polygon(screen, [230,168,50], (tri[0], tri[1], tri[2]), 1)

renderer = Renderer()

# main loop
while run:

    screen.fill([47,79,83])
    mx, my = pygame.mouse.get_pos()

    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            run = False
        
        if event.type == pygame.KEYDOWN:
            if event.key == pygame.K_ESCAPE:
                run = False
                
    keys = pygame.key.get_pressed()

    # draw model
    renderer.draw()

    clock.tick(FPS)
    pygame.display.flip()

pygame.quit()

Code with just perspective and rotation matrices:

import pygame
import math 
import copy
import random
import obj_loader as obj

pygame.init()

screen_width = 1920
screen_height = 1080
screen = pygame.display.set_mode([screen_width, screen_height])
clock = pygame.time.Clock()
FPS = 60
run = True

class Renderer():
    def __init__(self):
        
        # random colors for model sides
        self.colors = []
        for _ in range(6):
            self.colors.append([random.randint(0,255), random.randint(0,255), random.randint(0,255)])

        #  Models:
        # "default" (cube)
        # "cube"
        # "pylon"
        # "wireframe" (cube)
        # "teapot"

        model = "default"
        self.rotation = [0,1,0]
        self.rotation_speed = 0.015
        self.wireframes = 0
        self.color_fill = 1

        self.rotx = 0; self.roty = 0; self.rotz = 0

        # use basic cube or load model from obj-file
        if model == "default":
            
            self.model = [
            [[0, 0, 0, 1],    [0, 1, 0, 1],    [1, 1, 0, 1], self.colors[0]],
            [[0, 0, 0, 1],    [1, 1, 0, 1],    [1, 0, 0, 1], self.colors[0]],                                 
            [[1, 0, 0, 1],    [1, 1, 0, 1],    [1, 1, 1, 1], self.colors[1]],
            [[1, 0, 0, 1],    [1, 1, 1, 1],    [1, 0, 1, 1], self.colors[1]],                                       
            [[1, 0, 1, 1],    [1, 1, 1, 1],    [0, 1, 1, 1], self.colors[2]],
            [[1, 0, 1, 1],    [0, 1, 1, 1],    [0, 0, 1, 1], self.colors[2]],                                         
            [[0, 0, 1, 1],    [0, 1, 1, 1],    [0, 1, 0, 1], self.colors[3]],
            [[0, 0, 1, 1],    [0, 1, 0, 1],    [0, 0, 0, 1], self.colors[3]],                                          
            [[0, 1, 0, 1],    [0, 1, 1, 1],    [1, 1, 1, 1], self.colors[4]],
            [[0, 1, 0, 1],    [1, 1, 1, 1],    [1, 1, 0, 1], self.colors[4]],                                         
            [[1, 0, 1, 1],    [0, 0, 1, 1],    [0, 0, 0, 1], self.colors[5]],
            [[1, 0, 1, 1],    [0, 0, 0, 1],    [1, 0, 0, 1], self.colors[5]]]

            for tri in self.model:
                for point in tri:
                    point[0] -= 0.5; point[1] -= 0.5; point[2] -= 0.5

        # load from obj-file
        else:

            self.model = obj.load_obj(model)

            # add random colors to model sides
            self.colors = []
            amount_vertices = len(self.model)
            for _ in range(amount_vertices):
                self.colors.append([random.randint(0,255), random.randint(0,255), random.randint(0,255)])

            for i, tri in enumerate(self.model):
                tri.append(self.colors[i])
        
        # create empty projection matrix
        self.projection = []
        for _ in range(4):
            self.projection.append([0] * 4) 

        self.aspect_ratio = screen_width/screen_height
        n = 0.1
        f = 1000     

        # projection matrix
        self.projection[0][0] = 1 / (math.tan(math.radians(90/2)) * self.aspect_ratio)
        self.projection[0][1] = 0
        self.projection[0][2] = 0
        self.projection[0][3] = 0
        
        self.projection[1][0] = 0
        self.projection[1][1] = 1 / math.tan(math.radians(90/2))
        self.projection[1][2] = 0
        self.projection[1][3] = 0

        self.projection[2][0] = 0
        self.projection[2][1] = 0
        self.projection[2][2] = f / (f - n)
        self.projection[2][3] = (-f * n) / (f - n)

        self.projection[3][0] = 0
        self.projection[3][1] = 0
        self.projection[3][2] = 1
        self.projection[3][3] = 0

        # camera variables and matrix
        self.camera_pos = [0,0,-4]
        self.u          = [1,0,0]
        self.v          = [0,1,0]
        self.n          = [0,0,1]

        self.camera = [
            [self.u[0], self.u[1], self.u[2], -self.camera_pos[0]],
            [self.v[0], self.v[1], self.v[2], -self.camera_pos[1]],
            [self.n[0], self.n[1], self.n[2], -self.camera_pos[2]],
            [0,         0,         0,         1                  ]
        ]

        # create rotation matrices
        self.update_roty(); self.update_roty(); self.update_rotz()

        # translation matrix
        self.translation = [
            [1, 0, 0, 0],
            [0, 1, 0, 0],
            [0, 0, 1, 0],
            [0, 0, 0, 1]
        ]
        
        # scaling matrix
        scale = 100
        self.scaling = [
            [scale, 0,     0,     0],
            [0,     scale, 0,     0],
            [0,     0,     scale, 0],
            [0,     0,     0,     1]
        ]

    # rotation X matrix
    def update_rotx(self):
        self.rotation_x = [
            [1, 0, 0, 0],
            [0, math.cos(self.rotx), math.sin(self.rotx), 0],
            [0, -math.sin(self.rotx), math.cos(self.rotx), 0],
            [0, 0, 0, 0]]

    # rotation Y matrix
    def update_roty(self):
        self.rotation_y = [
            [math.cos(self.roty), 0, math.sin(self.roty), 0],
            [0, 1, 0, 0],
            [-math.sin(self.roty), 0, math.cos(self.roty), 0],
            [0, 0, 0, 0]]
        
    # rotation Z matrix
    def update_rotz(self):
        self.rotation_z = [
            [math.cos(self.rotz), -math.sin(self.rotz), 0, 0],
            [math.sin(self.rotz), math.cos(self.rotz), 0, 0],
            [0, 0, 1, 0],
            [0, 0, 0, 0]]

    # Vector matrix multiplication
    def Matrix_MultiplyVector(self, i, m):

        vx = i[0] * m[0][0] + i[1] * m[0][1] + i[2] * m[0][2] + m[0][3]
        vy = i[0] * m[1][0] + i[1] * m[1][1] + i[2] * m[1][2] + m[1][3]
        vz = i[0] * m[2][0] + i[1] * m[2][1] + i[2] * m[2][2] + m[2][3]
        vw = i[0] * m[3][0] + i[1] * m[3][1] + i[2] * m[3][2] + m[3][3]

        if vw != 0:
            vx /= vw
            vy /= vw
            vz /= vw
        return [vx,vy,vz,vw]

    # main render function
    def draw(self):

        # update rotation matrices
        self.rotx += self.rotation_speed
        self.roty += self.rotation_speed
        self.rotz += self.rotation_speed
        self.update_rotx(); self.update_roty(); self.update_rotz()

        triangles_to_draw = []

        # loop through triangles in model
        for tri in self.model:
            
            # create copy of triangle to project
            tricopy = copy.deepcopy(tri)
            proj_points = []

            # skip 4th element in triangle because it's color
            for i, _ in enumerate(tri):
                if i == 3:
                    continue
                
                # rotate triangles
                if self.rotation[0] == 1:
                    tricopy[i] = self.Matrix_MultiplyVector(tricopy[i], self.rotation_x)
                if self.rotation[1] == 1:
                    tricopy[i] = self.Matrix_MultiplyVector(tricopy[i], self.rotation_y)
                if self.rotation[2] == 1:
                    tricopy[i] = self.Matrix_MultiplyVector(tricopy[i], self.rotation_z)
                
            # calculate normal of triangle
            line1 = [
                tricopy[1][0] - tricopy[0][0],
                tricopy[1][1] - tricopy[0][1],
                tricopy[1][2] - tricopy[0][2],
            ]

            line2 = [
                tricopy[2][0] - tricopy[0][0],
                tricopy[2][1] - tricopy[0][1],
                tricopy[2][2] - tricopy[0][2],
            ]

            Nx = line1[1] * line2[2] - line1[2] * line2[1]
            Ny = line1[2] * line2[0] - line1[0] * line2[2]
            Nz = line1[0] * line2[1] - line1[1] * line2[0]
            l = math.sqrt(Nx * Nx + Ny * Ny + Nz * Nz)
            Nx  /= l; Ny /= l; Nz /= l 

            # draw only if triangles normal compared with cameras normal is < 0
            if (Nx * (tricopy[0][0] - self.camera_pos[0]) + Ny * (tricopy[0][1] - self.camera_pos[1]) + Nz * (tricopy[0][2] - self.camera_pos[2])) < 0:

                for i, _ in enumerate(tri):
                    # skip 4th element in triangle because it's color
                    if i == 3: 
                        continue

                    # camera and projection matrices
                    tricopy[i] = self.Matrix_MultiplyVector(tricopy[i], self.camera)
                    tricopy[i] = self.Matrix_MultiplyVector(tricopy[i], self.projection)

                    tricopy[i][0] += 2; tricopy[i][0] *= 0.25 * screen_width
                    tricopy[i][1] += 2; tricopy[i][1] *= 0.25 * screen_height
                    
                # calculate mean Z value and add triangles projected points to list
                mean_z = (tricopy[0][2] + tricopy[1][2] + tricopy[2][2]) / 3
                proj_points = [(tricopy[0][0], tricopy[0][1]), (tricopy[1][0], tricopy[1][1]), (tricopy[2][0], tricopy[2][1]), tricopy[3], mean_z]
                triangles_to_draw.append(proj_points)
        
        # sort by mean Z value
        triangles_to_draw.sort(key=lambda i: (i[4]), reverse=True)
        
        # draw triangles
        for i, tri in enumerate(triangles_to_draw):

            if self.color_fill == 1:
                try:
                    pygame.draw.polygon(screen, tri[3], (tri[0], tri[1], tri[2]))
                except:
                    print(tri[3])
            if self.wireframes == 1:
                pygame.draw.polygon(screen, [230,168,50], (tri[0], tri[1], tri[2]), 1)

renderer = Renderer()

# main loop
while run:

    screen.fill([47,79,83])
    mx, my = pygame.mouse.get_pos()

    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            run = False
        
        if event.type == pygame.KEYDOWN:
            if event.key == pygame.K_ESCAPE:
                run = False
                
    keys = pygame.key.get_pressed()

    # draw model
    renderer.draw()

    clock.tick(FPS)
    pygame.display.flip()

pygame.quit()
```
$\endgroup$

0

Browse other questions tagged or ask your own question.