How do I speed up this pyglet rendering?

Viewed 224

Below is the code for rendering. I am only rendering around 300 blocks yet it is very slow

async def render(self):
    time_start = time.time()
    self.batch = pyglet.graphics.Batch()
    for block in self.blocks_to_be_rendered:
        self.draw_block(block)
    print("Time to render 1 chunk " + str(time_start-time.time()))


def Create_chunk(cx,cz):
    thr = threading.Thread(target=_Create_chunk,args=(cx,cz))
    thr.start()
    thr.join()
    asyncio.run(Chunks[cx][cz].render())
def draw_block(self, block):

    position = (block.x,block.y,block.z)
    if block._type == "grass":
        block = Grass
    else:
        print("HOLD ON A SEC")
        block = Grass
    vertices = get_vertices(*position)
    faces = ("left", "right", "top", "bottom", "front", "back")
    for vertex, face in zip(vertices, faces):
        self.batch.add(
            4,
            gl.GL_QUADS,
            block.textures[face],
            ("v3f/static", vertex),
            ("t2f/static", (0, 0, 1, 0, 1, 1, 0, 1)),

I tried to run the "render" as an asynchronous function because rendering each chunk takes 0.3 seconds!!!!! This means that the fps is close to 0

Any help with optimization/critical flaws would be greatly appreciated!!

EDIT: This code may not make sense as I spliced it from the main code as it would be very long, but here it is just in case:

import pyglet
from pyglet.gl import *
from pyglet.window import key
from pyglet import gl, window
import math
import numpy as np
import random
import noise
import threading
import asyncio
from multiprocessing import Pool
import time
#AVOID FROM KEEPING GL FUNCTIONS IN CLASS BLOCK AND CLASS CHUNK BECAUSE THESE WILL BE EXECUTED FROM A DIFFERENT THREAD
def get_tex(file):
    tex = pyglet.image.load(file).get_texture()
    glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST)
    glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST)
    return pyglet.graphics.TextureGroup(tex)


def make_textures(textures):
    result = {}
    base = "imgs/"
    faces = ("left", "right", "top", "bottom", "front", "back")
    for face in faces:
        file_name = textures.get(face, textures.get("side"))
        result[face] = get_tex(base + file_name)
    return result

def get_vertices(x, y, z):
    dx, dy, dz = x + 1, y + 1, z + 1
    return [
        (x, y, z, x, y, dz, x, dy, dz, x, dy, z),  # side
        (dx, y, dz, dx, y, z, dx, dy, z, dx, dy, dz),  # side
        (x, dy, dz, dx, dy, dz, dx, dy, z, x, dy, z),  # top
        (x, y, z, dx, y, z, dx, y, dz, x, y, dz),  # bottom
        (dx, y, z, x, y, z, x, dy, z, dx, dy, z),  # side
        (x, y, dz, dx, y, dz, dx, dy, dz, x, dy, dz),  # side
    ]
class Grass:
    textures = make_textures({"side": "grass_side.png", "top": "grass_top.png", "bottom": "dirt.png"})

class Block():
    def __init__(self,x,y,z):
        #for testing sake _type = dirt
        self._type = "grass"
        self.x = x
        self.y = y
        self.z = z
        self.width = 1
        self.height = 1
        self.length = 1


np.seed = random.randint(1,10000)
Chunks = {-1:{},0:{},1:{}}
class Chunk():
    def prepare_for_rendering(self):
        #Im too lazy to do actual faces so i will only render the nesecary blocks
        #MUST BE DONE IN A DIFFERENT THREAD OR FPS = -1
        time_start = time.time()
        self.blocks_to_be_rendered = []
        for x in range(0,16):
            for z in range(0,16):
                #Gives us an unspecified length dictionary
                for key in self.Chunk_blocks[x][z].keys():
                    #key is an integer
                    #block is Chunk_blocks[x][z][key]
                    try:
                        if self.Chunk_blocks[x-1][z][key] and self.Chunk_blocks[x+1][z][key] and self.Chunk_blocks[x][z+1][key] and self.Chunk_blocks[x][z-1][key] and self.Chunk_blocks[x][z][key+1] and self.Chunk_blocks[x][z][key-1]:
                            pass
                    except:
                        self.blocks_to_be_rendered.append(self.Chunk_blocks[x][z][key])
        print(time_start-time.time())
    def draw_block(self, block):

        position = (block.x,block.y,block.z)
        if block._type == "grass":
            block = Grass
        else:
            print("HOLD ON A SEC")
            block = Grass
        vertices = get_vertices(*position)
        faces = ("left", "right", "top", "bottom", "front", "back")
        for vertex, face in zip(vertices, faces):
            self.batch.add(
                4,
                gl.GL_QUADS,
                block.textures[face],
                ("v3f/static", vertex),
                ("t2f/static", (0, 0, 1, 0, 1, 1, 0, 1)),
            )
    def __init__(self,Cx,Cz):
    #C means chunk x and chunk y aka top left
        self.Chunk_blocks = {}
        self.batch = pyglet.graphics.Batch()
        self.blocks_to_be_rendered = []
        x =0
        z=0
        for x in range(0,16):
            Chunk_row ={}
            for z in range(0,16):
                top_y = round(noise.pnoise2((Cx*16+x)/16,(Cz*16+z)/16)*10)
                Chunk_collumn = {}
                Chunk_collumn[top_y] = Block(Cx*16+x,top_y,Cz*16+z)
                #print(top_y)
                for y in range(-top_y,30):
                    Chunk_collumn[top_y-y-1] = Block(Cx*16+x,top_y-y-1,Cz*16+z)
                Chunk_row[z]= Chunk_collumn
            self.Chunk_blocks[x] = Chunk_row
        thr = threading.Thread(target=self.prepare_for_rendering)
        thr.start()
    async def render(self):
        time_start = time.time()
        self.batch = pyglet.graphics.Batch()
        for block in self.blocks_to_be_rendered:
            self.draw_block(block)
        print("Time to render 1 chunk " + str(time_start-time.time()))


def Create_chunk(cx,cz):
    thr = threading.Thread(target=_Create_chunk,args=(cx,cz))
    thr.start()
    thr.join()
    asyncio.run(Chunks[cx][cz].render())
def _Create_chunk(cx,cz):
    time_start = time.time()
    if cx in Chunks.keys():
        Chunks[cx][cz] = Chunk(cx,cz)
    else:
        Chunks[cx] = {cz:Chunk(cx,cz)}
    print(time_start-time.time())


#THIS IS WHERE THE PYGLET/GL SEGMENT STARTS
#I AM TRYING TO SPERATE THIS SO WE CAN MUTLI TRHEAD
#WITHOUT ACCIDENTILY CALLING GL FUNCTIONS IN A SEPERATE THREAD OTHER THEN THE MAIN THREAD
#

Create_chunk(0,0)

class Player:
    def __init__(self, position=(8, 0, 8), rotation=(0, 0)):
        self.position = position
        self.rotation = rotation
        self.strafe = [0, 0, 0]  # forward, up, left

    def mouse_motion(self, dx, dy):
        x, y = self.rotation
        sensitivity = 0.15
        x += dx * sensitivity
        y += dy * sensitivity
        y = max(-90, min(90, y))
        self.rotation = x % 360, y

    def update(self, dt):
        motion_vector = self.get_motion_vector()
        speed = dt * 5
        self.position = [x + y * speed for x, y in zip(self.position, motion_vector)]

    def get_motion_vector(self):
        x, y, z = self.strafe
        if x or z:
            strafe = math.degrees(math.atan2(x, z))
            yaw = self.rotation[0]
            x_angle = math.radians(yaw + strafe)
            x = math.cos(x_angle)
            z = math.sin(x_angle)
        return x, y, z


def draw_camera(position, rotation):
    yaw, pitch = rotation
    gl.glRotatef(yaw, 0, 1, 0)
    gl.glRotatef(-pitch, math.cos(math.radians(yaw)), 0, math.sin(math.radians(yaw)))
    x, y, z = position
    gl.glTranslatef(-x, -y, -z)


def set_3d(width, height):
    gl.glEnable(gl.GL_DEPTH_TEST)
    gl.glMatrixMode(gl.GL_PROJECTION)
    gl.glLoadIdentity()
    gl.gluPerspective(65, width / height, 0.1, 60)
    gl.glMatrixMode(gl.GL_MODELVIEW)
    gl.glLoadIdentity()


class Window(pyglet.window.Window):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.player = Player()
        self.set_exclusive_mouse(True)
        pyglet.clock.schedule(self.update)

    def on_mouse_motion(self, x, y, dx, dy):
        self.player.mouse_motion(dx, dy)

    def on_key_press(self, symbol, modifiers):
        if symbol == window.key.ESCAPE:
            self.close()
        speed = 1
        if symbol == window.key.W:
            self.player.strafe[0] = -speed
        elif symbol == window.key.S:
            self.player.strafe[0] = speed
        elif symbol == window.key.A:
            self.player.strafe[2] = -speed
        elif symbol == window.key.D:
            self.player.strafe[2] = speed
        elif symbol == window.key.SPACE:
            self.player.strafe[1] = speed
        elif symbol == window.key.LSHIFT:
            self.player.strafe[1] = -speed

    def on_key_release(self, symbol, modifiers):
        if symbol == window.key.W:
            self.player.strafe[0] = 0
        elif symbol == window.key.S:
            self.player.strafe[0] = 0
        elif symbol == window.key.A:
            self.player.strafe[2] = 0
        elif symbol == window.key.D:
            self.player.strafe[2] = 0
        elif symbol == window.key.SPACE:
            self.player.strafe[1] = 0
        elif symbol == window.key.LSHIFT:
            self.player.strafe[1] = 0
                
    def update(self, dt):
        self.player.update(dt)
        chunk_x_player = round(self.player.position[0]/16)
        chunk_z_player = round(self.player.position[2]/16)
        for x in range(-2,2):
            for z in range(-2,2):
                try:
                    if Chunks[chunk_x_player+x][chunk_z_player+z]:
                        pass
                except:
                    Create_chunk(chunk_x_player+x,chunk_z_player+z)

    def on_draw(self):
        self.clear()
        time_start = time.time()
        set_3d(*self.get_size())
        draw_camera(self.player.position, self.player.rotation)
        #for block in self.model.blocks_to_be_rendered:
        #    self.model.draw_block(block)
        chunk_x_player = round(self.player.position[0]/16)
        chunk_z_player = round(self.player.position[2]/16)
        for x in range(-2,2):
            for z in range(-2,2):
                try:
                    Chunks[chunk_x_player+x][chunk_z_player+z].batch.draw()
                except:
                    print("ERROR")
                    pass
        print(time_start-time.time())


if __name__ == "__main__":
    Window(width=800, height=480, resizable=True)
    gl.glClearColor(0.5, 0.7, 1, 1)  # sky color
    pyglet.app.run()
1 Answers

This is by far not a complete answer, but it's to long for a comment and might contain some useful information. I would recommend that you try to avoid threads, and with that asyncio. Mainly because it can be tricky to get it right, and both asyncio and pyglet have their own main event loops. You can create your own event loops, so it's not impossible - but perhaps for this case it's trying to solve something that could be solved with less code - not more.

You're also replacing your batch (i think) every render iteration, or at least every render call which I assume is as often as you can.

class Chunk():
    def __init__(self,Cx,Cz):
        self.batch = pyglet.graphics.Batch()
    def render(self):
        self.batch = pyglet.graphics.Batch()

You should also create your self.blocks_to_be_rendered in the __init__ function, and let it run its course. Unless you're comfortable creating your material/object loaders and create a loading screen, or pre-prepp them and load them in quicker - skip it and just leave the creation parts in the __init__ part. That way you avoid threads and you make sure you don't have do do this every render:

    def render(self):
        for block in self.blocks_to_be_rendered:
            self.draw_block(block)

Which translates to:

    def render(self):
        # Delete the entire old batch
        self.batch = pyglet.graphics.Batch()
    
        # Wait for thread lock in case there is one
        # because the resouces is being created out of main thread
        for block in self.blocks_to_be_rendered:
            position = (block.x,block.y,block.z)
            if block._type == "grass":
                block = Grass
            else:
                print("HOLD ON A SEC")
                block = Grass

            x, y, z = position
            dx, dy, dz = x + 1, y + 1, z + 1
            vertices = [
                (x, y, z, x, y, dz, x, dy, dz, x, dy, z),  # side
                (dx, y, dz, dx, y, z, dx, dy, z, dx, dy, dz),  # side
                (x, dy, dz, dx, dy, dz, dx, dy, z, x, dy, z),  # top
                (x, y, z, dx, y, z, dx, y, dz, x, y, dz),  # bottom
                (dx, y, z, x, y, z, x, dy, z, dx, dy, z),  # side
                (x, y, dz, dx, y, dz, dx, dy, dz, x, dy, dz),  # side
            ]

            faces = ("left", "right", "top", "bottom", "front", "back")
            for vertex, face in zip(vertices, faces):
                # Recreating the batch objects again, one by one
                self.batch.add(
                    4,
                    gl.GL_QUADS,
                    block.textures[face],
                    ("v3f/static", vertex),
                    ("t2f/static", (0, 0, 1, 0, 1, 1, 0, 1)),
                )
        print("Time to render 1 chunk " + str(time_start-time.time()))

(I expanded your code, so you would understand the massive amount of calls, loops and logic that you got going on in your render logic. Every clock cycle counts when it comes to graphic rendering)

Instead, your entire render logic could be boiled down to:

    def render(self):
        self.batch.draw()

Assuming you create the batch in the __init__ function instead.

Related