Question

continuing on from my explorations of the basics of OpenGL (see this question), I'm trying to figure out the basic principles of drawing a scene with OpenGL.

I am trying to render a simple cube repeated n times in every direction.

My method appears to yield terrible performance : 1000 cubes brings performance below 50fps (on a QuadroFX 1800, roughly a GeForce 9600GT).

My method for drawing these cubes is as follows:

done once:

  • set up a vertex buffer and array buffer containing my cube vertices in model space
  • set up an array buffer indexing the cube for drawing as 12 triangles

done for each frame:

  • update uniform values used by the vertex shader to move all cubes at once

done for each cube, for each frame:

  • update uniform values used by the vertex shader to move each cube to its position
  • call glDrawElements to draw the positioned cube

Is this a sane method ? If not, how does one go about something like this ? I'm guessing I need to minimize calls to glUniform, glDrawElements, or both, but I'm not sure how to do that.


Full code for my little test : (depends on gletools and pyglet)

I'm aware that my init code (at least) is really ugly; I'm concerned with the rendering code for each frame right now, I'll move to something a little less insane for the creation of the vertex buffers and such later on.

import pyglet
from pyglet.gl import *
from pyglet.window import key
from numpy import deg2rad, tan
from gletools import ShaderProgram, FragmentShader, VertexShader, GeometryShader

vertexData = [-0.5, -0.5, -0.5, 1.0,
              -0.5, 0.5, -0.5, 1.0,
              0.5, -0.5, -0.5, 1.0,
              0.5, 0.5, -0.5, 1.0,
              -0.5, -0.5, 0.5, 1.0,
              -0.5, 0.5, 0.5, 1.0,
              0.5, -0.5, 0.5, 1.0,
              0.5, 0.5, 0.5, 1.0]

elementArray = [2, 1, 0, 1, 2, 3,## back face
                4, 7, 6, 4, 5, 7,## front face
                1, 3, 5, 3, 7, 5,## top face
                2, 0, 4, 2, 4, 6,## bottom face
                1, 5, 4, 0, 1, 4,## left face
                6, 7, 3, 6, 3, 2]## right face

def toGLArray(input):
    return (GLfloat*len(input))(*input)

def toGLushortArray(input):
    return (GLushort*len(input))(*input)

def initPerspectiveMatrix(aspectRatio = 1.0, fov = 45):
    frustumScale = 1.0 / tan(deg2rad(fov) / 2.0)
    fzNear = 0.5
    fzFar = 300.0
    perspectiveMatrix = [frustumScale*aspectRatio, 0.0         , 0.0                            , 0.0 ,
                         0.0                     , frustumScale, 0.0                            , 0.0 ,
                         0.0                     , 0.0         , (fzFar+fzNear)/(fzNear-fzFar)  , -1.0,
                         0.0                     , 0.0         , (2*fzFar*fzNear)/(fzNear-fzFar), 0.0 ]
    return perspectiveMatrix

class ModelObject(object):
    vbo = GLuint()
    vao = GLuint()
    eao = GLuint()
    initDone = False

    verticesPool = []
    indexPool = []

    def __init__(self, vertices, indexing):
        super(ModelObject, self).__init__()
        if not ModelObject.initDone:
            glGenVertexArrays(1, ModelObject.vao)
            glGenBuffers(1, ModelObject.vbo)
            glGenBuffers(1, ModelObject.eao)
            glBindVertexArray(ModelObject.vao)
            initDone = True
        self.numIndices = len(indexing)
        self.offsetIntoVerticesPool = len(ModelObject.verticesPool)
        ModelObject.verticesPool.extend(vertices)
        self.offsetIntoElementArray = len(ModelObject.indexPool)
        ModelObject.indexPool.extend(indexing)

        glBindBuffer(GL_ARRAY_BUFFER, ModelObject.vbo)
        glEnableVertexAttribArray(0) #position
        glVertexAttribPointer(0, 4, GL_FLOAT, GL_FALSE, 0, 0)
        glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ModelObject.eao)
        glBufferData(GL_ARRAY_BUFFER, len(ModelObject.verticesPool)*4, toGLArray(ModelObject.verticesPool), GL_STREAM_DRAW)
        glBufferData(GL_ELEMENT_ARRAY_BUFFER, len(ModelObject.indexPool)*2, toGLushortArray(ModelObject.indexPool), GL_STREAM_DRAW)

    def draw(self):
        glDrawElements(GL_TRIANGLES, self.numIndices, GL_UNSIGNED_SHORT, self.offsetIntoElementArray)


class PositionedObject(object):
    def __init__(self, mesh, pos, objOffsetUf):
        super(PositionedObject, self).__init__()
        self.mesh = mesh
        self.pos = pos
        self.objOffsetUf = objOffsetUf

    def draw(self):
        glUniform3f(self.objOffsetUf, self.pos[0], self.pos[1], self.pos[2])
        self.mesh.draw()



w = 800
h = 600
AR = float(h)/float(w)
window = pyglet.window.Window(width=w, height=h, vsync=False)
window.set_exclusive_mouse(True)
pyglet.clock.set_fps_limit(None)

## input
forward = [False]
left = [False]
back = [False]
right = [False]
up = [False]
down = [False]
inputs = {key.Z: forward, key.Q: left, key.S: back, key.D: right,
          key.UP: forward, key.LEFT: left, key.DOWN: back, key.RIGHT: right,
          key.PAGEUP: up, key.PAGEDOWN: down}

## camera
camX = 0.0
camY = 0.0
camZ = -1.0

def simulate(delta):
    global camZ, camX, camY
    scale = 10.0
    move = scale*delta
    if forward[0]:
        camZ += move
    if back[0]:
        camZ += -move
    if left[0]:
        camX += move
    if right[0]:
        camX += -move
    if up[0]:
        camY += move
    if down[0]:
        camY += -move
pyglet.clock.schedule(simulate)

@window.event
def on_key_press(symbol, modifiers):
    global forward, back, left, right, up, down
    if symbol in inputs.keys():
        inputs[symbol][0] = True

@window.event
def on_key_release(symbol, modifiers):
    global forward, back, left, right, up, down
    if symbol in inputs.keys():
        inputs[symbol][0] = False


## uniforms for shaders
camOffsetUf = GLuint()
objOffsetUf = GLuint()
perspectiveMatrixUf = GLuint()
camRotationUf = GLuint()

program = ShaderProgram(
    VertexShader('''
    #version 330
    layout(location = 0) in vec4 objCoord;
    uniform vec3 objOffset;
    uniform vec3 cameraOffset;
    uniform mat4 perspMx;
    void main()
    {
        mat4 translateCamera = mat4(1.0f, 0.0f, 0.0f, 0.0f,
                                    0.0f, 1.0f, 0.0f, 0.0f,
                                    0.0f, 0.0f, 1.0f, 0.0f,
                                    cameraOffset.x, cameraOffset.y, cameraOffset.z, 1.0f);
        mat4 translateObject = mat4(1.0f, 0.0f, 0.0f, 0.0f,
                                    0.0f, 1.0f, 0.0f, 0.0f,
                                    0.0f, 0.0f, 1.0f, 0.0f,
                                    objOffset.x, objOffset.y, objOffset.z, 1.0f);

        vec4 modelCoord = objCoord;
        vec4 positionedModel = translateObject*modelCoord;
        vec4 cameraPos = translateCamera*positionedModel;

        gl_Position = perspMx * cameraPos;
    }'''),
    FragmentShader('''
    #version 330
    out vec4 outputColor;
    const vec4 fillColor = vec4(1.0f, 1.0f, 1.0f, 1.0f);

    void main()
    {
        outputColor = fillColor;
    }''')
)


shapes = []
def init():
    global camOffsetUf, objOffsetUf
    with program:
        camOffsetUf = glGetUniformLocation(program.id, "cameraOffset")
        objOffsetUf = glGetUniformLocation(program.id, "objOffset")
        perspectiveMatrixUf = glGetUniformLocation(program.id, "perspMx")
        glUniformMatrix4fv(perspectiveMatrixUf, 1, GL_FALSE, toGLArray(initPerspectiveMatrix(AR)))

        obj = ModelObject(vertexData, elementArray)
        nb = 20
        for i in range(nb):
            for j in range(nb):
                for k in range(nb):
                    shapes.append(PositionedObject(obj, (float(i*2), float(j*2), float(k*2)), objOffsetUf))

        glEnable(GL_CULL_FACE)
        glCullFace(GL_BACK)
        glFrontFace(GL_CW)
        glEnable(GL_DEPTH_TEST)
        glDepthMask(GL_TRUE)
        glDepthFunc(GL_LEQUAL)
        glDepthRange(0.0, 1.0)
        glClearDepth(1.0)

def update(dt):
    print pyglet.clock.get_fps()
pyglet.clock.schedule_interval(update, 1.0)

@window.event
def on_draw():
    with program:
        pyglet.clock.tick()
        glClear(GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT)
        glUniform3f(camOffsetUf, camX, camY, camZ)

        for shape in shapes:
            shape.draw()


init()
pyglet.app.run()
Was it helpful?

Solution

Basically, as you're looping through data once per frame, you're probably starting to hit Python's performance limits. It's probable that if you rewrote your code in, say, C you'd have much better performance.

Anyway, to increase performance in OpenGL, independently of the language, you have to limit the number of draw calls (calls to glDrawElements), to limit the CPU usage and improve the communication between CPU and GPU.

Depending of your target, you have multiple options to speed up your test :

  • if all your cubes are to remain static, you could combine their geometries into a single VBO, by pretransforming the vertices, and issue a single draw call for all your cubes.

  • if all your cubes are to be animated independently, you could use hardware instancing (if your hardware allows it), or pseudo-hardware instancing, you can find some pointers here. Using these techniques, you basically can draw multiple cubes with a single draw call, it's then up to the shader to fetch the cube position according its primitive id.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top