Search code examples
pythonpython-3.xperformanceopenglpyglet

Why is my pyglet program slow while rendering 128 particles?


Why is my program slow while rendering 128 particles? I think that's not enough to get less than 30 fps.

All I do is rendering 128 particles and giving them some basic gravitation

on_draw function

 def on_draw(self, time=None):
    glClearColor(0.0, 0.0, 0.0, 1.0)
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)
    glLoadIdentity()

    self.particles.append(Particle())
    for particle in self.particles:
        particle.draw()

        if particle.is_dead:
            self.particles.remove(particle)

Particle class

class Particle:

    def __init__(self, **kwargs):
        self.acceleration = Vector2(0, 0.05)
        self.velocity = Vector2(random.uniform(-1, 1), random.uniform(-1, 0))
        self.position = Vector2()
        self.time_to_live = 255

        self.numpoints = 50

        self._vertices = []

        for i in range(self.numpoints):
            angle = math.radians(float(i) / self.numpoints * 360.0)
            x = 10 * math.cos(angle) + self.velocity[0] + 300
            y = 10 * math.sin(angle) + self.velocity[1] + 400
            self._vertices += [x, y]

    def update(self, time=None):
        self.velocity += self.acceleration
        self.position -= self.velocity
        self.time_to_live -= 2

    def draw(self):
        glEnable(GL_BLEND)
        glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)
        glPushMatrix()
        glTranslatef(self.position[0], self.position[1], 0)
        pyglet.graphics.draw(self.numpoints, GL_TRIANGLE_FAN, ('v2f', self._vertices), ('c4B', self.color))
        glPopMatrix()

        self.update()

    @property
    def is_dead(self):
        if self.time_to_live <= 0:
            return True
        return False

    @property
    def color(self):
        return tuple(color for i in range(self.numpoints) for color in (255, 255, 255,  self.time_to_live))

Solution

  • I'm not overly happy about using GL_TRIANGLE_FAN because it's caused a lot of odd shapes when using batched rendering. So consider moving over to GL_TRIANGLES instead and simply add all the points to the object rather than leaning on GL to close the shape for you.

    That way, you can easily move over to doing batched rendering:

    import pyglet
    from pyglet.gl import *
    from collections import OrderedDict
    from time import time, sleep
    from math import *
    from random import randint
    
    key = pyglet.window.key
    
    class CustomGroup(pyglet.graphics.Group):
        def set_state(self):
            #pyglet.gl.glLineWidth(5)
            #glPolygonMode(GL_FRONT_AND_BACK, GL_LINE)
            #glColor4f(1, 0, 0, 1) #FFFFFF
            #glLineWidth(1)
            #glEnable(texture.target)
            #glBindTexture(texture.target, texture.id)
            pass
    
        def unset_state(self):
            glLineWidth(1)
            #glDisable(texture.target)
    
    class Particle():
        def __init__(self, x, y, batch, particles):
            self.batch = batch
            self.particles = particles
            self.group = CustomGroup()
    
            self.add_point(x, y)
    
        def add_point(self, x, y):
    
            colors = ()#255,0,0
    
            sides = 50
            radius = 25
    
            deg = 360/sides
            points = ()#x, y # Starting point is x, y?
    
            prev = None
            for i in range(sides):
    
                n = ((deg*i)/180)*pi # Convert degrees to radians
                point = int(radius * cos(n)) + x, int(radius * sin(n)) + y
    
                if prev:
                    points += x, y
                    points += prev
                    points += point
                    colors += (255, i*int(255/sides), 0)*3 # Add a color pair for each point (r,g,b) * points[3 points added]
    
                prev = point
    
            points += x, y
            points += prev
            points += points[2:4]
            colors += (255, 0, 255)*3
    
            self.particles[len(self.particles)] = self.batch.add(int(len(points)/2), pyglet.gl.GL_TRIANGLES, self.group, ('v2i/stream', points), ('c3B', colors))
    
    class main(pyglet.window.Window):
        def __init__ (self, demo=False):
            super(main, self).__init__(800, 600, fullscreen = False, vsync = True)
            #print(self.context.config.sample_buffers)
            self.x, self.y = 0, 0
    
            self.sprites = OrderedDict()
            self.batches = OrderedDict()
            self.batches['default'] = pyglet.graphics.Batch()
            self.active_batch = 'default'
    
            for i in range(1000):
                self.sprites[len(self.sprites)] = Particle(randint(0, 800), randint(0, 600), self.batches[self.active_batch], self.sprites)
    
            self.alive = True
    
            self.fps = 0
            self.last_fps = time()
            self.fps_label = pyglet.text.Label(str(self.fps) + ' fps', font_size=12, x=3, y=self.height-15)
    
        def on_draw(self):
            self.render()
    
        def on_close(self):
            self.alive = 0
    
        def on_key_press(self, symbol, modifiers):
            if symbol == key.ESCAPE: # [ESC]
                self.alive = 0
    
        def render(self):
            self.clear()
            #self.bg.draw()
    
            self.batches[self.active_batch].draw()
    
            self.fps += 1
            if time()-self.last_fps > 1:
                self.fps_label.text = str(self.fps) + ' fps'
                self.fps = 0
                self.last_fps = time()
    
            self.fps_label.draw()
            self.flip()
    
        def run(self):
            while self.alive == 1:
                self.render()
    
                # -----------> This is key <----------
                # This is what replaces pyglet.app.run()
                # but is required for the GUI to not freeze
                #
                event = self.dispatch_events()
    
    if __name__ == '__main__':
        x = main(demo=True)
        x.run()
    

    Bare in mind, on my nVidia 1070 I managed to get roughly 35fps out of this code, which isn't mind blowing. But it is 1000 objects * sides, give or take.

    What I've changed is essentially this:

    self.batch.add(int(len(points)/2), pyglet.gl.GL_TRIANGLES, self.group, ('v2i/stream', points), ('c3B', colors))
    

    and in your draw loop, you'll do:

    self.batch.draw()
    

    Instead of calling Particle.draw() for each particle object.
    What this does is that it'll send all the objects to the graphics card in one gigantic batch rather than having to tell the graphics card what to render object by object.

    As @thokra pointed out, your code is more CPU intensive than GPU intensive.
    Hopefully this fixes it or gives you a few pointers.

    Most of this code is taking from a LAN project I did with a good friend of mine a while back:

    Because I didn't have all your code, mainly the main loop. I applied your problem to my own code and "solved " it by tweaking it a bit. Again, hope it helps and steal ideas from that github project if you need to. Happy new year!