0

I am an amateur programmer, creating an RPG from scratch using Python 3, and I am handling graphics using Pygame.

At the moment, I have to render 4153 different polygons, and soon I will also have to add 9216 lines. My framerate for just the polygons is terrible, about 1 FPS, and I can't image what it will drop to once I add the lines.

The problem is that I also have to do quite a lot of calculations to even know where to place the polygons on the screen, before I even render them, because I'm using 3D graphics I coded myself, so that is probably another cause for the low framerate.

Code:

Yes, it is very messy...

class Obj:

def __init__(self, x, y, z, points, lines, surfaces):
    self.x = x
    self.y = y
    self.z = z
    self.points = points
    self.lines = lines
    self.surfaces = surfaces

#Mapper function
def drawObj(self, x, y, z, ha, va, screen, width, height):
    drawOrder = []
    #Draw points
    '''for point in self.points:
        n = [0, 0, 0]
    
        nx = point[0] + self.x - x
        nz = point[2] + self.z - z
        
        n[0] = nx*m.cos(va) - nz*m.sin(va) + x
        n[2] = nz*m.cos(va) + nx*m.sin(va) + z
        
        ny = point[1] + self.y - y
        nz = n[2] - z
        
        n[1] = ny*m.cos(ha) - nz*m.sin(ha) + y
        n[2] = nz*m.cos(ha) + ny*m.sin(ha) + z
        
        if n[2] > z - zoom:
            drawOrder.append([n[2], 'point', [m.floor((n[0] - x)*zoom/(n[2] - (z - zoom)) + width/2), m.floor((n[1] - y)*zoom/(n[2] - (z - zoom)) + height/2)], max(0, 255 - dist(n[0], n[1], n[2], X, Y, Z)*fog)])
    '''
    #Draw lines
    for line in self.lines:
        #Point 1
        n1 = [0, 0, 0]
        
        n1x = self.points[line[0]][0] + self.x - x
        n1z = self.points[line[0]][2] + self.z - z
        
        n1[0] = n1x*m.cos(va) - n1z*m.sin(va) + x
        n1[2] = n1z*m.cos(va) + n1x*m.sin(va) + z
        
        n1y = self.points[line[0]][1] + self.y - y
        n1z = n1[2] - z
        
        n1[1] = n1y*m.cos(ha) - n1z*m.sin(ha) + y
        n1[2] = n1z*m.cos(ha) + n1y*m.sin(ha) + z
        
        #Point 2
        n2 = [0, 0, 0]
        
        n2x = self.points[line[1]][0] + self.x - x
        n2z = self.points[line[1]][2] + self.z - z
        
        n2[0] = n2x*m.cos(va) - n2z*m.sin(va) + x
        n2[2] = n2z*m.cos(va) + n2x*m.sin(va) + z
        
        n2y = self.points[line[1]][1] + self.y - y
        n2z = n2[2] - z
        
        n2[1] = n2y*m.cos(ha) - n2z*m.sin(ha) + y
        n2[2] = n2z*m.cos(ha) + n2y*m.sin(ha) + z
        
        #Set coords for line
        l = [(n1[0] - x)*zoom/(n1[2] - (z - zoom)), (n1[1] - y)*zoom/(n1[2] - (z - zoom)), (n2[0] - x)*zoom/(n2[2] - (z - zoom)), (n2[1] - y)*zoom/(n2[2] - (z - zoom))]
        
        #Primitive rebound workaround
        '''if n1[2] <= z - zoom:
            l[0] = m.inf
            l[1] = m.inf
        
        if n2[2] <= z - zoom:
            l[2] = m.inf
            l[3] = m.inf'''
        
        drawOrder.append([(n1[2] + n2[2])/2, 'line', [m.floor(l[0] + width/2), m.floor(l[1] + height/2), m.floor(l[2] + width/2), m.floor(l[3] + height/2)], max(0, 255 - dist((n1[0] + n2[0])/2, (n1[1] + n2[1])/2, (n1[2] + n2[2])/2, X, Y, Z)*fog)])
    
    #Draw surfaces
    for surface in self.surfaces:
        f = []
        drawOrder.append([0, 'surface', [], []])
        for i in range(1,len(surface)):
            p = surface[i]
            n = [0, 0, 0]
            
            nx = self.points[p][0] + self.x - x
            nz = self.points[p][2] + self.z - z
            
            n[0] = nx*m.cos(va) - nz*m.sin(va) + x
            n[2] = nz*m.cos(va) + nx*m.sin(va) + z
            
            ny = self.points[p][1] + self.y - y
            nz = n[2] - z
            
            n[1] = ny*m.cos(ha) - nz*m.sin(ha) + y
            n[2] = nz*m.cos(ha) + ny*m.sin(ha) + z
            
            f.append([n[0],n[1],n[2]])
            
            if n[2] > z - zoom:
                drawOrder[len(drawOrder)-1][2].append([m.floor((n[0] - x)*zoom/(n[2] - (z - zoom)) + width/2), m.floor((n[1] - y)*zoom/(n[2] - (z - zoom)) + height/2)])
        
        ax = 0
        ay = 0
        az = 0
        for p in f:
            ax += p[0]
            ay += p[1]
            az += p[2]
        
        ax /= len(f)
        ay /= len(f)
        az /= len(f)
        drawOrder[len(drawOrder)-1][0] = az
        
        drawOrder[len(drawOrder)-1][3] = [surface[0][0], surface[0][1], surface[0][2], max(0, surface[0][3] - dist(ax,ay,az,x,y,z)*fog)]
    
    drawOrder.sort(key = lambda a: a[0], reverse = True)
    
    for drawTopic in drawOrder:
        if drawTopic[1] == 'point':
            '''color = (255, 255, 255, drawTopic[3])
            pyg.draw.circle(screen, color, (drawTopic[2][0], drawTopic[2][1]), 0)'''
            
        elif drawTopic[1] == 'line':
            color = (255, 255, 255, drawTopic[3])
            pyg.draw.line(screen, color, (drawTopic[2][0], drawTopic[2][1]), (drawTopic[2][2], drawTopic[2][3]))
            
        elif drawTopic[1] == 'surface':
            color = (drawTopic[3][0], drawTopic[3][1], drawTopic[3][2], drawTopic[3][3])
            pyg.draw.polygon(screen, color, drawTopic[2])

Any ideas?

Community
  • 1
  • 1
The Eye
  • 59
  • 12
  • Without seeing your code, it's hard to know what you're doing, but my guess is that you have a plain old Python list of lists of floats, and you're looping over them with a `for` loop and doing 3D math and then a PyGame call on every single one of them, right? – abarnert Jun 19 '18 at 04:15
  • If so, the best fix is probably to use [NumPy](http://www.numpy.org/), to store your vertices as an array of native floats, do your math as vectorized elementwise operations across the whole array, and pass the array to PyGame in one go (or a few goes) rather than 4153 separate calls. I don't know the details for how to do that last part, but I think PyGame has modules named things like `surfarray`, `vertarray`, etc. for taking NumPy arrays. – abarnert Jun 19 '18 at 04:17
  • Anyway, if you want a less vague answer than this, or any help beyond "here's some things, go search for and work though tutorials for them", you're going to need to give us a [mcve] that we can show you how to optimize. – abarnert Jun 19 '18 at 04:19
  • yeah, basically that. I just need a general sort of answer, but I can attach the code for the 3D rendering if you want. – The Eye Jun 19 '18 at 04:19
  • If you don't already know NumPy, your first step should be to go find and work through a basic NumPy tutorial, then one focused on 3D math, and then look into PyGame's interfaces with NumPy. There's a lot to learn, but it'll be worth it. Another alternative might be to use Numba to JIT-compile your code, possibly even to CUDA code that runs on the GPU, but I suspect that would be a lot harder to integrate with PyGame. Another option is using Cython (or C/C++/Rust/D/whatever) to write a C extension to do the 3D math, and maybe even raw OpenGL in place of PyGame/SDL. – abarnert Jun 19 '18 at 04:22
  • Finally: I don't know if PyGame works well with PyPy, but if it does, try just running your program in PyPy instead of the normal CPython interpreter. It might magically make everything 5-25x faster, which might be enough to get you to 30fps without changing anything. – abarnert Jun 19 '18 at 04:24
  • How would NumPy be used in this case? When the code is used in the program, the whole thing (`3Dobj.drawObj(args)`) is passed once for the `3Dobj`, which contains ALL the polygons/lines. – The Eye Jun 19 '18 at 04:26
  • Every list of lists of floats, like `lines`, becomes a 2D numpy array of floats. Then, inside of a loop over `for line in self.lines:`, you just do a vectorized operation on the whole array of lines at once. If you don't already have NumPy experience, it's really hard to explain in any more detail until you go work through some tutorials. – abarnert Jun 19 '18 at 04:36

0 Answers0