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()
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.
来源:https://stackoverflow.com/questions/4672748/opengl-basics-calling-gldrawelements-once-per-object