Search code examples
pythonpyglet

Sand Simulation Renders Slowly in Pyglet


I was following the latest coding challenge by The Coding Train. In his video, using p5.js, he builds a falling sand simulator, I decided to try the challenge using pyglet.

I did some tests, but I noticed that my program renders very slowly compared to his. I know I'm re-drawing the grid at every frame, but my code's logic is similar to The Coding Train's one and he doesn't experience the same issue.

I'm sure there's a better way to handle the sand falling, but since I just started learning pyglet I'm not sure how to proceed.

Here's the code:

import random
import math
# pyglet libraries
import pyglet
from pyglet import shapes
from pyglet.window import mouse

SCREEN_WIDTH = 400
SCREEN_HEIGHT = 400
SAND_SIZE = 10

def make2DArray(rows, cols):
    arr = [[0 for y in range(cols)] for x in range(rows)]
    for row in range(rows):
        for col in range(cols):
            arr[row][col] = 0
    return arr


window = pyglet.window.Window(SCREEN_WIDTH, SCREEN_HEIGHT)

rows = int(SCREEN_WIDTH / SAND_SIZE)
cols = int(SCREEN_HEIGHT / SAND_SIZE)
grid = make2DArray(rows, cols)
grid[30][20] = 1


@window.event
def on_mouse_drag(x, y, dx, dy, buttons, modifiers):
    row = math.floor(x / SAND_SIZE)
    col = math.floor(y / SAND_SIZE)
    grid[row][col] = 1


@window.event
def on_draw():
    global grid

    for row in range(rows):
        for col in range(cols):
            if grid[row][col] == 0:
                rectangle = shapes.Rectangle(row*10, col*10, 10, 10, color=(0, 0, 0))
            else:
                rectangle = shapes.Rectangle(row*10, col*10, 10, 10, color=(255, 255, 255))
            rectangle.draw()
    # Generating New State
    next_state_grid = make2DArray(rows, cols)
    for row in range(rows):
        for col in range(cols):
            current_state = grid[row][col]
            if current_state == 1:
                below = grid[row][col - 1]
                if below == 0 and col != 0:
                    next_state_grid[row][col - 1] = 1
                else:
                    next_state_grid[row][col] = 1
    grid = next_state_grid


pyglet.app.run()


Solution

  • Pyglet uses hardware acceleration (OpenGL) to draw its images. When you call rectangle.draw(), you're making a draw call to the GPU which is very slow. You're expected to reduce draw calls as much as possible as communication between the CPU and GPU is generally what takes time.

    The documentation explicitly discourages you from using this function and instead suggests using batches.

    Batches will collect everything you want to draw and then optimize the amount of draw calls. Here's the example from the documentation where they add all shapes to a batch and use batch.draw():

    import pyglet
    from pyglet import shapes
    
    window = pyglet.window.Window(960, 540)
    batch = pyglet.graphics.Batch()
    
    circle = shapes.Circle(700, 150, 100, color=(50, 225, 30), batch=batch)
    square = shapes.Rectangle(200, 200, 200, 200, color=(55, 55, 255), batch=batch)
    rectangle = shapes.Rectangle(250, 300, 400, 200, color=(255, 22, 20), batch=batch)
    rectangle.opacity = 128
    rectangle.rotation = 33
    line = shapes.Line(100, 100, 100, 200, width=19, batch=batch)
    line2 = shapes.Line(150, 150, 444, 111, width=4, color=(200, 20, 20), batch=batch)
    star = shapes.Star(800, 400, 60, 40, num_spikes=20, color=(255, 255, 0), batch=batch)
    
    @window.event
    def on_draw():
        window.clear()
        batch.draw()
    
    pyglet.app.run()