简体   繁体   English

在 pygame 中向 3D 渲染器添加简单透视

[英]Adding simple perspective to a 3D renderer in pygame

I have created a 3D renderer in pyGame however I would now like to add perspective.我在 pyGame 中创建了一个 3D 渲染器,但是我现在想添加透视图。 I have been trying for a while now and can't seem to figure it out.我已经尝试了一段时间,似乎无法弄清楚。

I have read that the most simplest form of perspective is to multiply the x and y coordinates by the inverse of the z coordinate so that the x and y are dependant on the z value.我读过最简单的透视形式是将 x 和 y 坐标乘以 z 坐标的倒数,以便 x 和 y 取决于 z 值。 This means that the x and y distances should decrease with the z coordinate increasing and x and y should increase with z decreasing.这意味着 x 和 y 距离应该随着 z 坐标的增加而减小,并且 x 和 y 应该随着 z 的减小而增加。 I have managed to get this to work slightly however it seems to accumulate so when I spin the box left and right the back of the box gets very small and seems to accumulate a negative scale instead of remaining at a constant size with being at the set z distance.我已经设法让它稍微工作,但它似乎会累积,所以当我左右旋转盒子时,盒子的背面变得非常小,并且似乎累积了一个负比例,而不是在设置时保持恒定大小z 距离。

Here is my code:这是我的代码:

wireframe.py:线框.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, axis, 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, centre, radians):
        cx, cy, 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, centre, radians):
        cx, cy, 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,0],
                         [0,1,0,0],
                         [0,0,1,0],
                         [dx,dy,dz,1]])

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

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

    def rotateXMatrix(self, radians):

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

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

    def rotateYMatrix(self, radians):

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

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

    def rotateZMatrix(self, radians):

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

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

    def movCamera(self, tilt, pan):

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

projectionViewer.py投影查看器.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, 0])),
        pygame.K_RIGHT:(lambda x: x.translateAll([ 10, 0, 0])),
        pygame.K_DOWN: (lambda x: x.translateAll([0,  10, 0])),
        pygame.K_UP:   (lambda x: x.translateAll([0, -10, 0])),

        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', -0.08)),
        pygame.K_s: (lambda x: x.rotate_about_Center('X', 0.08)),


        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', 0.1)),
        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, axis, 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,x,y):

        wf = Wireframe()

        matrix = wf.testMat((0,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,-self.height/2,0)

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

        #perform the perspectivecorrection

        wf = Wireframe()
        matrix = wf.translationMatrix(-self.width/2,-self.height/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

        wf = Wireframe()
        matrix = wf.translationMatrix(-self.width/2,-self.height/2,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,self.height/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,-self.height/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,self.height/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])

main .py主要的.py

from projectionViewer import ProjectionViewer 
import wireframe
import numpy as np

cube = wireframe.Wireframe()

cube_nodes = [(x, y, 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, 1, 4, 5)])

pv = ProjectionViewer(1200, 1000)

pv.addWireframe('cube', cube)



pv.run()

The code that does the multiplying is in the wireframe file and the transform_for_perspective() function.进行乘法运算的代码位于线框文件和 transform_for_perspective() function 中。

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

If anyone could tell me where I am going wrong and explain in which order I need to call the perspective matrix, ie rotation then perspective or perspective and then rotation.如果有人能告诉我哪里出错并解释我需要按什么顺序调用透视矩阵,即旋转然后透视或透视然后旋转。

Also, Because Pygame starts at (0,0) in the top left corner this means that if I want to rotate about the centre of the screen I have to translate the centre of the screen, perform the rotation matrix and then translate it back to the centre.另外,因为 Pygame 从左上角的 (0,0) 开始,这意味着如果我想围绕屏幕中心旋转,我必须平移屏幕中心,执行旋转矩阵,然后将其转换回中心。 What does this mean for perspective?这对透视意味着什么? do I have to translate the centre of the screen to the top left and then perform the perspective matrix and then translate it back again?我是否必须将屏幕的中心翻译到左上角,然后执行透视矩阵,然后再将其翻译回来?

Any help would be much appreciated.任何帮助将非常感激。

The transformation that you are applying in transform_for_perspective should only be applied once.您在 transform_for_perspective 中应用的transform_for_perspective应该只应用一次。 However, it seems that you are calling it on every frame, and as it stores the output in the same variable ( self.nodes ) it is applied many times.但是,您似乎在每一帧都调用它,并且由于它将 output 存储在同一个变量( self.nodes )中,因此它被应用了很多次。 Consider saving the output of that transformation in a new field (such as self.perspective_nodes ).考虑将该转换的 output 保存在新字段中(例如self.perspective_nodes )。

Also, the transformation was not working for me, I tried to do some variations and came up with this:此外,转换对我不起作用,我尝试做一些变化并想出了这个:

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

You also need to modify display in projectionViewer:您还需要修改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)

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM