关于投影矩阵的问题以及为什么它显示的图片有时会变形

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

我目前正在研究 3d 渲染,我正在尝试在没有任何附加库的情况下完成它(pygame、math、sys 除外)。我做了很多研究,但仍然不太了解数学(我正在使用 Methods on wikipedia). It did output the right coordinate, but it somtimes has an severely deform. Here is what the result look like。我不太了解矩阵,所以我很难调试。我真的可以使用一些帮助来了解它为什么变形或只是矩阵如何工作,非常感谢!
这是我的代码:

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

class三角形是为了让代码更容易阅读

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

从obj文件获取顶点和线

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])

class矩阵是为了让矩阵的加减乘乘更简单

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

控制移动旋转的地方,用到投影矩阵的地方

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)

初始化

# 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]])
]

我用的obj文件里的数据


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)

在屏幕上绘制点。

您的矩阵乘法定义不正确。我已经按照呈现的样式重写了它,尽管我会评论说有更多的 pythonic 方法可以做到这一点:

def __mul__(self, other):
    if self.width != other.height:
        raise ValueError("incompatible matrices")

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

你的代码没问题。但是pygame坐标系(0, 0)的原点在window的左上角。您需要将几何图形从左上角平移到 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]

我建议改变相机的起始位置:

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

我用以下方法创建了动画:

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
    # [...]

另见 and Does PyGame do 3d?