在 pygame 中为 3D 渲染器添加简单的透视图

问题描述

我在 pyGame 中创建了一个 3D 渲染器,但是我现在想添加透视图。我已经尝试了一段时间,但似乎无法弄清楚。

我读过最简单的透视形式是将 x 和 y 坐标乘以 z 坐标的倒数,以便 x 和 y 取决于 z 值。这意味着 x 和 y 距离应该随着 z 坐标的增加而减小,而 x 和 y 应该随着 z 的减小而增加。我设法让它稍微起作用,但是它似乎会积累,所以当我左右旋转盒子时,盒子的背面变得非常小,并且似乎积累了负比例,而不是在设置时保持恒定大小z 距离。

这是我的代码

线框.py:

class Wireframe:

    def __init__(self):
        self.nodes = np.zeros((0,4))
        self.edges = []

    def addNodes(self,node_array):

        ones_column = np.ones((len(node_array),1))
        ones_added = np.hstack((node_array,ones_column))
        self.nodes = np.vstack((self.nodes,ones_added))
        

    def addEdges(self,edgeList):
        self.edges += edgeList

    def outputNodes(self):
        print("\n --- Nodes ---")

        for i,(x,y,z,_) in enumerate(self.nodes):
            print(" %d: (%.2f,%.2f,%.2f)" % (i,node.x,node.y,node.z))

    def outputEdges(self):

        print("\n --- Edges ---")

        for i,(node1,node2) in enumerate(self.edges):
            print(" %d: %d -> %d" % (i,node1,node2))

    def translate(self,axis,d):
        if axis in ['x','y','z']:
            for node in self.nodes:
                setattr(node,getattr(node,axis) + d)

    def scale(self,centre_x,centre_y,scale):

        for node in self.nodes:
            node.x = centre_x + scale * (node.x - centre_x)
            node.y = centre_y + scale * (node.y - centre_y)
            node.z *= scale

    def findCentre(self):

        num_nodes = len(self.nodes)
        meanX = sum([node.x for node in self.nodes]) / num_nodes
        meanY = sum([node.y for node in self.nodes]) / num_nodes
        meanZ = sum([node.z for node in self.nodes]) / num_nodes

        return (meanX,meanY,meanZ)

    def rotateZ(self,centre,radians):
        cx,cy,cz = centre

        for node in self.nodes:
            x = node.x - cx
            y = node.y - cy
            d = math.hypot(y,x)
            theta = math.atan2(y,x) + radians
            node.x = cx + d * math.cos(theta)
            node.y = cy + d * math.sin(theta)

    def rotateX(self,cz = centre
        for node in self.nodes:
            y = node.y - cy
            z = node.z - cz
            d = math.hypot(y,z)
            theta = math.atan2(y,z) + radians
            node.z = cz + d * math.cos(theta)
            node.y = cy + d * math.sin(theta)

    def rotateY(self,cz = centre
        for node in self.nodes:
            x = node.x - cx
            z = node.z - cz
            d = math.hypot(x,z)
            theta = math.atan2(x,z) + radians

            node.z = cz + d * math.cos(theta)
            node.x = cx + d * math.sin(theta)

    def transform(self,matrix):
        self.nodes = np.dot(self.nodes,matrix)

    def transform_for_perspective(self):

        for node in self.nodes:
            print(node[0],node[1],node[2])
            if node[2] != 0:

                node[0] = node[0]*(1/(1-(node[2]*0.00005)))
                node[1] = node[1]*(1/(1-(node[2]*0.00005)))
                node[2] = node[2]*1

    def translationMatrix(self,dx=0,dy=0,dz=0):

        return np.array([[1,0],[0,1,[dx,dy,dz,1]])

    def scaleMatrix(self,sx=0,sy=0,sz=0):

        return np.array([[sx,sy,sz,1]])

    def rotateXMatrix(self,radians):

        c = np.cos(radians)
        s = np.sin(radians)

        return np.array([[1,c,-s,s,1]])

    def rotateYMatrix(self,radians):

        c = np.cos(radians)
        s = np.sin(radians)

        return np.array([[c,[-s,1]])

    def rotateZMatrix(self,[s,1]])

    def movCamera(self,tilt,pan):

        return np.array([[1,200],[pan,0]])

projectionViewer.py

from wireframe import *
import pygame
import numpy as np

class ProjectionViewer:

    ''' displays 3D Objects on a Pygame Screen '''

    def __init__(self,width,height):
        self.width = width
        self.height = height
        self.screen = pygame.display.set_mode((width,height))
        pygame.display.set_caption('Wireframe display')
        self.background = (10,10,50)


        self.wireframes = {}
        self.displayNodes = True
        self.displayEdges = True
        self.nodeColour = (255,255,255)
        self.edgeColour = (200,200,200)
        self.nodeRadius = 4

    def run(self):

        key_to_function = {
        pygame.K_LEFT: (lambda x: x.translateall([-10,0])),pygame.K_RIGHT:(lambda x: x.translateall([ 10,pygame.K_DOWN: (lambda x: x.translateall([0,pygame.K_UP:   (lambda x: x.translateall([0,-10,pygame.K_a: (lambda x: x.rotate_about_Center('Y',-0.08)),pygame.K_d: (lambda x: x.rotate_about_Center('Y',0.08)),pygame.K_w: (lambda x: x.rotate_about_Center('X',pygame.K_s: (lambda x: x.rotate_about_Center('X',pygame.K_EQUALS: (lambda x: x.scale_centre([1.25,1.25,1.25])),pygame.K_MINUS: (lambda x: x.scale_centre([0.8,0.8,0.8])),pygame.K_q: (lambda x: x.rotateall('X',0.1)),pygame.K_z: (lambda x: x.rotateall('Z',pygame.K_x: (lambda x: x.rotateall('Z',-0.1)),pygame.K_p: (lambda x: x.perspectiveMode()),pygame.K_t: (lambda x: x.translate_Camera())
        
        }


        running = True
        flag = False

        while running:

            keys = pygame.key.get_pressed()

            for event in pygame.event.get():
                if event.type == pygame.QUIT:
                    running = False

                
                
            if keys[pygame.K_LEFT]:
                key_to_function[pygame.K_LEFT](self)
            if keys[pygame.K_RIGHT]:
                key_to_function[pygame.K_RIGHT](self)
            if keys[pygame.K_DOWN]:
                key_to_function[pygame.K_DOWN](self)
            if keys[pygame.K_UP]:
                key_to_function[pygame.K_UP](self)
            if keys[pygame.K_EQUALS]:
                key_to_function[pygame.K_EQUALS](self)
            if keys[pygame.K_MINUS]:
                key_to_function[pygame.K_MINUS](self)
            if keys[pygame.K_LEFT]:
                key_to_function[pygame.K_LEFT](self)
            if keys[pygame.K_q]:
                key_to_function[pygame.K_q](self)
            if keys[pygame.K_w]:
                key_to_function[pygame.K_w](self)
            if keys[pygame.K_a]:
                key_to_function[pygame.K_a](self)
            if keys[pygame.K_s]:
                key_to_function[pygame.K_s](self)
            if keys[pygame.K_z]:
                key_to_function[pygame.K_z](self)
            if keys[pygame.K_x]:
                key_to_function[pygame.K_x](self)
            if keys[pygame.K_p]:
                key_to_function[pygame.K_p](self)
            if keys[pygame.K_t]:
                key_to_function[pygame.K_t](self)
            if keys[pygame.K_d]:
                key_to_function[pygame.K_d](self)

            self.display()
            pygame.display.flip()

    def addWireframe(self,name,wireframe):
        self.wireframes[name] = wireframe
        #translate to center
        wf = Wireframe()
        matrix = wf.translationMatrix(-self.width/2,-self.height/2,0)

        for wireframe in self.wireframes.values():
            wireframe.transform(matrix)

        

        wf = Wireframe()
        matrix = wf.translationMatrix(self.width,self.height,0)

        for wireframe in self.wireframes.values():
            wireframe.transform(matrix)


        

    def display(self):

        self.screen.fill(self.background)

        for wireframe in self.wireframes.values():
            if self.displayEdges:
                for n1,n2 in wireframe.edges:
                    pygame.draw.aaline(self.screen,self.edgeColour,wireframe.nodes[n1][:2],wireframe.nodes[n2][:2],1)

            wireframe.transform_for_perspective()

            if self.displayNodes:
                for node in wireframe.nodes:

                    pygame.draw.circle(self.screen,self.nodeColour,(int(node[0]),int(node[1])),self.nodeRadius,0)




    def translateall(self,vector):
        ''' Translate all wireframes along a given axis by d units '''
        wf = Wireframe()
        matrix = wf.translationMatrix(*vector)
        for wireframe in self.wireframes.values():
            wireframe.transform(matrix)

    def scaleAll(self,vector):
        wf = Wireframe()
        matrix = wf.scaleMatrix(*vector)

        for wireframe in self.wireframes.values():
            wireframe.transform(matrix)

    def rotateall(self,theta):

        wf = Wireframe()
        if axis == 'X':
            matrix = wf.rotateXMatrix(theta)
        elif axis == 'Y':
            matrix = wf.rotateYMatrix(theta)
        elif axis == 'Z':
            matrix = wf.rotateZMatrix(theta)

        for wireframe in self.wireframes.values():
            wireframe.transform(matrix)
            #wireframe.transform_for_perspective()

    def moveCameraX(self,x,y):

        wf = Wireframe()

        matrix = wf.movCamera(x,y)
        print("test")

        for wireframe in self.wireframes.values():
            wireframe.transform(matrix)

    def moveCameraZ(self,y):

        wf = Wireframe()

        matrix = wf.testMat((0,val))

        for wireframe in self.wireframes.values():
            wireframe.transform(matrix)

    def perspectiveMode(self):

        #First translate the centre of screen to 0,0

        wf = Wireframe()
        matrix = wf.translationMatrix(-self.width/2,0)

        for wireframe in self.wireframes.values():
            wireframe.transform(matrix)

        #perform the perspectivecorrection

        wf = Wireframe()
        matrix = wf.translationMatrix(-self.width/2,0)

        for wireframe in self.wireframes.values():
            matrix = wf.perspectiveCorrection(1.2)
            wireframe.transform(matrix)

        wf = Wireframe()
        matrix = wf.translationMatrix(self.width/2,self.height/2,0)

        for wireframe in self.wireframes.values():
            wireframe.transform(matrix)


    def rotate_about_Center(self,Axis,theta):

        #First translate Centre of screen to 0,0)

        for wireframe in self.wireframes.values():
            wireframe.transform(matrix)

        #Do Rotation
        wf = Wireframe()
        if Axis == 'X':
            matrix = wf.rotateXMatrix(theta)
        elif Axis == 'Y':
            matrix = wf.rotateYMatrix(theta)
        elif Axis == 'Z':
            matrix = wf.rotateZMatrix(theta)

        for wireframe in self.wireframes.values():
            wireframe.transform(matrix)
            

        
        

        #Translate back to centre of screen

        wf = Wireframe()
        matrix = wf.translationMatrix(self.width/2,0)

        for wireframe in self.wireframes.values():
            wireframe.transform(matrix)



        

        #Do perspective if needed

    def scale_centre(self,vector):

        #Transform center of screen to origin

        wf = Wireframe()
        matrix = wf.translationMatrix(-self.width/2,0)

        for wireframe in self.wireframes.values():
            wireframe.transform(matrix)

        #Scale the origin by vector

        wf = Wireframe()
        matrix = wf.scaleMatrix(*vector)

        for wireframe in self.wireframes.values():
            wireframe.transform(matrix)

        wf = Wireframe()
        matrix = wf.translationMatrix(self.width/2,0)

        for wireframe in self.wireframes.values():
            wireframe.transform(matrix)



    def add_perspective(self):

        for wireframe in self.wireframes.values():
            for node in wireframe.nodes:
                if node[2] != 0:


                    print("Point ----------")
                    print("x node",node[0])
                    print("y node",node[1])
                    print("z node",node[2])

                    node[0] = node[0] + (10/node[2])
                    node[1] = node[1] + (10/node[2])

ma​​in.py

from projectionViewer import ProjectionViewer 
import wireframe
import numpy as np

cube = wireframe.Wireframe()

cube_nodes = [(x,z) for x in (-100,100) for y in (-100,100) for z in (-100,100)]

print(cube_nodes)

cube.addNodes(np.array(cube_nodes))
cube.addEdges([(n,n + 4) for n in range(0,4)])
cube.addEdges([(n,n + 1) for n in range(0,8,2)])
cube.addEdges([(n,n + 2) for n in (0,4,5)])

pv = ProjectionViewer(1200,1000)

pv.addWireframe('cube',cube)



pv.run()

进行乘法运算的代码在线框文件和 transform_for_perspective() 函数中。

def transform_for_perspective(self):

        for node in self.nodes:
            print(node[0],node[2])
            if node[2] != 0:

                node[0] = node[0]*(1/(1-(node[2]*0.00005)))
                node[1] = node[1]*(1/(1-(node[2]*0.00005)))
                node[2] = node[2]*1

如果有人能告诉我我哪里出错了,并解释我需要按什么顺序调用透视矩阵,即旋转然后透视或透视然后旋转。

另外,因为 Pygame 从左上角的 (0,0) 开始,这意味着如果我想绕屏幕中心旋转,我必须平移屏幕中心,执行旋转矩阵,然后平移它回到中心。这对透视意味着什么?我是否必须将屏幕中心平移到左上角,然后执行透视矩阵,然后再将其平移回来?

任何帮助将不胜感激。

解决方法

您在 transform_for_perspective 中应用的转换只能应用一次。但是,您似乎在每一帧上都调用它,并且因为它将输出存储在同一个变量 (self.nodes) 中,所以它被多次应用。 考虑将该转换的输出保存在新字段(例如 self.perspective_nodes)中。

此外,转换对我不起作用,我尝试做一些变化并想出了这个:

class Wireframe:

    def __init__(self):
        self.nodes = np.zeros((0,4))
        self.perspective_nodes = None
        self.edges = []

    ....

    def transform_for_perspective(self,center):
        self.perspective_nodes = self.nodes.copy()
        for i in range(len(self.nodes)):
            node = self.nodes[i]
            p_node = self.perspective_nodes[i]
            print(node[0],node[1],node[2])
            if node[2] != 0:
                p_node[0] = center[0] + (node[0]-center[0])*250/(200-(node[2]))
                p_node[1] = center[1] + (node[1]-center[1])*250/(200-(node[2]))
                p_node[2] = node[2] * 1

您还需要在projectionViewer中修改显示:

    def display(self):

        self.screen.fill(self.background)

        for wireframe in self.wireframes.values():

            wireframe.transform_for_perspective((self.width/2,self.height/2))

            if self.displayNodes:
                for node in wireframe.perspective_nodes:

                    pygame.draw.circle(self.screen,self.nodeColour,(int(
                        node[0]),int(node[1])),self.nodeRadius,0)
            if self.displayEdges:
                for n1,n2 in wireframe.edges:
                    pygame.draw.aaline(
                        self.screen,self.edgeColour,wireframe.perspective_nodes[n1][:2],wireframe.perspective_nodes[n2][:2],1)