Search code examples
pythonpygame3d-rendering

Problems about projection matrix and why the picture it shows somtimes deform


I am currently working on 3d rendering, and I am trying to accomplish it without any addition library(Except for pygame, math, sys).I have done many research but still cannot quite understand the math(I am using Methods on wikipedia). It did output the right coordinate, but it somtimes has an severely deform. Here is what the result look like. I don't quite understand the matrix so it's very hard for me to debug. I can really use some help on why is it deforming or simply just how the matrix works, thanks a lot!
Here's my code:

import pygame
import math
import sys

width = 600
height = 480

class Triangle:
    def __init__(self, verts):
        for i in range(len(verts)):
            for r in range(3):
                verts[i][r] = int(float(verts[i][r]))
        self.verts = verts

The class Triangle is to make the code more easy to read

def getobj(filename):
    verts = []
    ind = []
    triangles = []
    data = None
    with open(filename, 'r') as f:
        data = f.readline()
        while data:
            data = data.rstrip("\n").split(" ")
            if data[0] == 'v':
                verts.append([-1 * int(float(data[1])), -1 * int(float(data[2])), -1 * int(float(data[3]))])
            elif data[0] == 'f':
                v = []
                t = []
                n = []
                for i in range(3):
                    l = data[i + 1].split('/')
                    v.append(int(l[0]))
                    t.append(int(l[1]))
                    n.append(int(l[2]))
                ind.append([v, t, n])
            data = f.readline()
    for points in ind:
        v = []
        for i in points[0]:
            v.append(verts[i - 1])
        triangles.append(Triangle(v))
    return triangles

Get vertex and lines from obj file

class Matrix:
    def __init__(self, matrix):
        self.matrix = matrix
        self.height = len(matrix)
        self.width = len(matrix[0])

    def __add__(self, other):
        result = []
        for h in range(self.height):
            row = []
            for w in range(self.width):
                row.append(self.matrix[h][w] + other.matrix[h][w])
            result.append(row)
        return Matrix(result)

    def __sub__(self, other):
        result = []
        for h in range(self.height):
            row = []
            for w in range(self.width):
                row.append(self.matrix[h][w] - other.matrix[h][w])
            result.append(row)
        return Matrix(result)
    
    def __mul__(self, other):
        result = []
        for h in range(self.height):
            SUM = 0
            for w in range(self.width):
                SUM += self.matrix[h][w] * other.matrix[0][w]
            result.append(SUM)
        return Matrix([result])

The class Matrix is to make the addition, subtraction and multiplication of matrixes more easy

class Cam:
    def __init__(self):
        self.pos = Matrix([[0, 0, 0]])
        self.rot = [0, 0, 0]
    
    def getcoord(self, coord):
        m = Matrix([coord])
        data = m - self.pos
        F = Matrix([
            [math.cos(self.rot[2]), math.sin(self.rot[2]), 0],
            [-1 * math.sin(self.rot[2]), math.cos(self.rot[2]), 0],
            [0, 0, 1]
        ])
        S = Matrix([
            [math.cos(self.rot[1]), 0, -1 * math.sin(self.rot[1])],
            [0, 1, 0],
            [math.sin(self.rot[1]), 0, math.cos(self.rot[1])]
        ])
        T = Matrix([
            [1, 0, 0],
            [0, math.cos(self.rot[0]), math.sin(self.rot[0])],
            [0, -1 * math.sin(self.rot[0]), math.cos(self.rot[0])]
        ])
        data = F * data
        data = S * data
        data = T * data
        x = (width / 2) * data.matrix[0][0] / data.matrix[0][2]
        y = (height / 2) * data.matrix[0][1] / data.matrix[0][2]
        return [x, y]

    def events(self, event):
        if event.type == pygame.MOUSEMOTION:
            x, y = event.rel
            x /= 300
            y /= 300
            self.rot[0] -= y
            self.rot[1] += x
    
    def update(self, dt, key):
        s = dt * 2
        if key[pygame.K_LSHIFT]: self.pos.matrix[0][1] -= s
        if key[pygame.K_SPACE]: self.pos.matrix[0][1] += s

        x, y = s*math.sin(self.rot[1]), s*math.cos(self.rot[1])

        if key[pygame.K_w]: self.pos.matrix[0][0] -= x; self.pos.matrix[0][2] -= y
        if key[pygame.K_s]: self.pos.matrix[0][0] += x; self.pos.matrix[0][2] += y
        if key[pygame.K_a]: self.pos.matrix[0][0] += y; self.pos.matrix[0][2] -= x
        if key[pygame.K_d]: self.pos.matrix[0][0] -= y; self.pos.matrix[0][2] += x

Where the movement and rotation is controled, and where the projection matrix is used.

cam = Cam()

pygame.init()
screen = pygame.display.set_mode((width, height))
clock = pygame.time.Clock()

pygame.event.get()
pygame.mouse.get_rel()
pygame.mouse.set_visible(0)
pygame.event.set_grab(1)

Initialize

# tris = getobj("untitled.obj")
tris = [
    Triangle([[1, -1, 1], [-1, -1, -1], [-1, -1, 1]]),
    Triangle([[-1, -1, -1], [1, 1, -1], [-1, 1, -1]]),
    Triangle([[1, -1, -1], [1, 1, 1], [1, 1, -1]]),
    Triangle([[-1, 1, 1], [1, 1, -1], [1, 1, 1]]),
    Triangle([[-1, -1, 1], [-1, 1, -1], [-1, 1, 1]]),
    Triangle([[1, -1, 1], [-1, 1, 1], [1, 1, 1]]),
    Triangle([[1, -1, 1], [1, -1, -1], [-1, -1, -1]]),
    Triangle([[-1, -1, -1], [1, -1, -1], [1, 1, -1]]),
    Triangle([[1, -1, -1], [1, -1, 1], [1, 1, 1]]),
    Triangle([[-1, 1, 1], [-1, 1, -1], [1, 1, -1]]),
    Triangle([[-1, -1, 1], [-1, -1, -1], [-1, 1, -1]]),
    Triangle([[1, -1, 1], [-1, -1, 1], [-1, 1, 1]])
]

The data in the obj file I use.


while True:
    dt = clock.tick()/1000
    screen.fill((255, 255, 255))
    for event in pygame.event.get():
        if event.type == pygame.QUIT: pygame.quit(); sys.exit()
        cam.events(event)
    for tri in tris:
        coord1 = cam.getcoord(tri.verts[0])
        coord2 = cam.getcoord(tri.verts[1])
        coord3 = cam.getcoord(tri.verts[2])
        pygame.draw.line(screen, (0, 0, 0), (int(coord1[0]), int(coord1[1])), (int(coord2[0]), int(coord2[1])))
        pygame.draw.line(screen, (0, 0, 0), (int(coord1[0]), int(coord1[1])), (int(coord3[0]), int(coord3[1])))
        pygame.draw.line(screen, (0, 0, 0), (int(coord2[0]), int(coord2[1])), (int(coord3[0]), int(coord3[1])))
    pygame.display.flip()
    
    key = pygame.key.get_pressed()
    cam.update(dt, key)

Draw the points on screen.


Solution

  • Your code is fine. However, the origin of the pygame coordinate system (0, 0) is in the upper left corner of the window. You need to translate the geometry from the top left corner to the center of the window:

    x = (width / 2) * data.matrix[0][0] / data.matrix[0][2]
    y = (height / 2) * data.matrix[0][1] / data.matrix[0][2]

    x = (width / 2) + (width / 2) * data.matrix[0][0] / data.matrix[0][2]
    y = (height / 2)  + (height / 2) * data.matrix[0][1] / data.matrix[0][2]
    

    I suggest changing the starting position of the camera:

    class Cam:
        def __init__(self):
            self.pos = Matrix([[0, 0, -5]])
            self.rot = [0, 0, 0]
    

    I created the animation with:

    angle_y = 0
    while True:
        angle_y += 0.01
        cam.rot[1] = angle_y
        cam.pos = Matrix([[-math.sin(angle_y)*6, 0, -math.cos(angle_y)*6]])
    
        dt = clock.tick(100)/1000
        # [...]
    

    See also Pygame rotating cubes around axis and Does PyGame do 3d?.