PyOpenGL --Feasible for sprite heavy 2D game ?(yes!) VBOs and Vertex arrays

Started by
30 comments, last by moonpirate 13 years, 10 months ago
Hi,

Also, once you become familiar with OpenGL programming, you'll enjoy shaders. I've written a PyOpenGL particle system that does 1,048,576 (10242) textured quads at 30Hz (on my laptop, no less). The geometry is static, but the shader does all of the transformations in hardware. Moving more computation from Python/PyOpenGL to the graphics card will make your programs faster.

Also, PyOpenGL is going to be a lot slower than OpenGL (not as slow as your original problem) simply due to the way it works internally. You can get much better speed by using the OpenGL.raw sublibrary, at the expense of complexity, and some readability.

-G

[size="1"]And a Unix user said rm -rf *.* and all was null and void...|There's no place like 127.0.0.1|The Application "Programmer" has unexpectedly quit. An error of type A.M. has occurred.
[size="2"]

Advertisement
Quote:
Using SFML instead of the PyGame + OpenGL combo will make it easy to do what you want at a good FPS. Of course, this means that you're not learning OpenGL, but it does mean that you get your project under way.


Quote:
You might want to take a look at rabbyt: http://matthewmarshall.org/projects/rabbyt/

I considered using SFML, but I could never get it's python binding running, and the support for the binding isn't great. I also considered rabbyt but I knew I would want some more advanced effects so I figured I would just buckle down and learn openGL (also could never find rabbyt for python 2.5 on windows).

Quote:
You can get much better speed by using the OpenGL.raw sublibrary


How does this work exactly? I can import OpenGL.raw but I'm not sure what it's capable of. I havent had any luck on google looking for info about this. A point in the right direction would be greatly appreciated.

Quote:
In that case, though, you should still be batching your sprites by texture, and only binding a new texture between batches.


Yea I figured it would work something like this. Luckily in an STG there are a huge number of sprites which will use the same texture.


Thanks for all the replies so far guys, this has been extremely helpful.
Hi,
Quote:
Quote:You can get much better speed by using the OpenGL.raw sublibrary
How does this work exactly? I can import OpenGL.raw but I'm not sure what it's capable of. I havent had any luck on google looking for info about this. A point in the right direction would be greatly appreciated.
You use it almost like you would normally. To import:
from OpenGL.raw.GL import *from OpenGL.raw.GLU import *#instead offrom OpenGL.GL import *from OpenGL.GLU import *
Most of your main code will be the same, though some things are different (texture loading requires extra arguments, for instance). I think there's some ctypes requirements. Anyway, I'm not too up on this; the PyOpenGL mailing list can help.

Just thought of this: you can also disable error checking (this is usually about a 2x speedup):
#first two lines of your main:import OpenGLOpenGL.ERROR_CHECKING=False#OR, later (you'll need to do "from OpenGL import error" too)error.ErrorChecker.registerChecker(None)

-G

[size="1"]And a Unix user said rm -rf *.* and all was null and void...|There's no place like 127.0.0.1|The Application "Programmer" has unexpectedly quit. An error of type A.M. has occurred.
[size="2"]

Finally I have implemented a VBO for my sprites (at least for a single batch), and the performance results are extremely promising. I am however having trouble getting the vertices to draw correctly. I am 90% sure it is a problem with drawing the vertex array, and not the VBO itself. I am probably doing something wrong in the draw function, and I'm not entirely sure what's even going on =P. Any help is greatly appreciated. Here's what it looks like (the sprite displays correctly, it just doesn't tile as expected) http://i442.photobucket.com/albums/qq150/m00npirate/whatitlookslike-1.png?t=1274717655

here's a rotated version.. seems they are clumping oddly?
http://s442.photobucket.com/albums/qq150/m00npirate/?action=view&current=trippy.flv

What I feel like is the problem portion:
#note this is a snippet and won't run on its own#thanks to Jeff Molofee and Brian Leair for the python/openGL tutorial on VBOs#from which this was adapteddef init():    ...    glInitVertexBufferObjectARB() #binds buffer function namesdef make_VBO():    global VBOVerts, Vertices    scale = 4.0    Vertices = []    for x in range(45):        for y in range(45):            #I thought this would tile a bunch of point sprites            Vertices.append([x/3.0 - 7,y/3.0 - 4])    Vertices = Numeric.array(Vertices) #Store them in a higher performance array    VBOVerts = int(glGenBuffersARB(1))    glBindBufferARB(GL_ARRAY_BUFFER_ARB, VBOVerts);    glBufferDataARB(GL_ARRAY_BUFFER_ARB, Vertices, GL_DYNAMIC_DRAW_ARB);def load_image():    TextureImage = pygame.image.load('small.png')    TextureString = pygame.image.tostring(TextureImage, "RGBA", 1)    sizeX = TextureImage.get_width()    sizeY = TextureImage.get_height()    TextureID = glGenTextures(1)    glBindTexture(GL_TEXTURE_2D, TextureID)    glTexImage2D( GL_TEXTURE_2D, 0, GL_RGBA, sizeX, sizeY, 0,                      GL_RGBA, GL_UNSIGNED_BYTE, TextureString)    glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST)    glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST)def draw():    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)      glLoadIdentity()     glEnableClientState(GL_VERTEX_ARRAY)    glBindBufferARB(GL_ARRAY_BUFFER_ARB, VBOVerts)    glBufferDataARB(GL_ARRAY_BUFFER_ARB, Vertices, GL_DYNAMIC_DRAW_ARB)    glVertexPointer(3, GL_FLOAT, 0, None)    glDrawArrays(GL_POINTS, 0, len(Vertices))        glDisableClientState(GL_VERTEX_ARRAY)// Disable Vertex Arrays    pygame.display.flip()def main():    pygame.init()    surface = pygame.display.set_mode((640,480), OPENGL|DOUBLEBUF)    resize((640,480))    init()    load_image()    make_VBO()    while True:        event = pygame.event.poll()        if event.type == QUIT or (event.type == KEYDOWN and event.key == K_ESCAPE):            break        draw()


All the code:
import os, pygame, pygame.image, timeimport OpenGL, sys, traceback, mathOpenGL.ERROR_CHECKING = FalseOpenGL.ERROR_LOGGING = Falsefrom OpenGL.GL import *from OpenGL.GLU import *from OpenGL.GL.ARB.vertex_buffer_object import *from pygame.locals import *import rectangleimport numpy as Numeric#import psyco#psyco.full()class Admin:    def __init__(self):        self.textures = [0,0]        self.buffers = [0,0]        self.sprites = []        self.timeline = 0        self.clock = pygame.time.Clock()        admin = Admin()class Sprite:    def __init__(self, filepath, x, y, admin):        self.x = x        self.y = y        self.admin = admin        self.imageSurface = pygame.image.load(filepath)        self.string = pygame.image.tostring(self.imageSurface, "RGBA", 1)        self.width = self.imageSurface.get_width()        self.height = self.imageSurface.get_height()        #will be used for collisions, ignore this:        self.rect = rectangle.Rectangle(x,y,self.width, self.height)        self.location = None        self.create_Sprite()        self.load_Sprite()    def create_Sprite(self):        self.location = len(self.admin.textures)        self.admin.textures.append(self.location)    def load_Sprite(self):        glBindTexture(GL_TEXTURE_2D, self.admin.textures[self.location-1])        glTexImage2D( GL_TEXTURE_2D, 0, GL_RGBA, self.width, self.height, 0,                      GL_RGBA, GL_UNSIGNED_BYTE, self.string);        glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST)        glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST)    def draw(self):        glBindTexture(GL_TEXTURE_2D,self.admin.textures[self.location-1])        glLoadIdentity()        glTranslatef(self.x,self.y,0)        glBegin(GL_QUADS)        glTexCoord2f(0.0, 0.0); glVertex3f(-0.2, -0.2,  0.)        glTexCoord2f(1.0, 0.0); glVertex3f(0.2, -0.2,  0.)        glTexCoord2f(1.0, 1.0); glVertex3f(0.2,  0.2,  0.)        glTexCoord2f(0.0, 1.0); glVertex3f(-0.2,  0.2,  0.)        glEnd();    def draw(self):        #glBindTexture(GL_TEXTURE_2D,self.admin.textures[self.location-1])        glLoadIdentity()        glTranslatef(self.x,self.y,0)        glBegin(GL_POINTS);        glVertex2f(0.0, 0.0)        glEnd();        def resize((width, height)):    glViewport(0,0,width, height)    glMatrixMode(GL_PROJECTION)    glLoadIdentity()    gluOrtho2D(-6.4,6.4,-4.8,4.8)    glMatrixMode(GL_MODELVIEW)    glLoadIdentity()def init():    global index    glEnable(GL_TEXTURE_2D)    admin.sprites.append(Sprite('star.png',-1,-1, admin))    for x in range(45):        for y in range(45):               admin.sprites.append(Sprite('small.png',x/3.0-7,y/3.0-4, admin))    glShadeModel(GL_SMOOTH)    glClearColor(0.0, 0.0, 0.0, 0.0)    glClearDepth(1.0)    glEnable(GL_DEPTH_TEST)    glDepthFunc(GL_LEQUAL)    glEnable(GL_BLEND)    glEnable(GL_ALPHA_TEST)    glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)    glAlphaFunc(GL_GREATER, 0.9)    glEnable(GL_POINT_SPRITE_ARB)    glTexEnvi(GL_POINT_SPRITE_ARB, GL_COORD_REPLACE, GL_TRUE);    glPointParameteri(GL_POINT_SPRITE_COORD_ORIGIN, GL_LOWER_LEFT)    glPointSize(8.0)    glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST)    glInitVertexBufferObjectARB()def make_VBO():    global VBOVerts, Vertices    scale = 4.0    Vertices = []    for x in range(45):        for y in range(45):            Vertices.append([x/3.0 - 7,y/3.0 - 4])    Vertices = Numeric.array(Vertices)    VBOVerts = int(glGenBuffersARB(1))    glBindBufferARB(GL_ARRAY_BUFFER_ARB, VBOVerts);    glBufferDataARB(GL_ARRAY_BUFFER_ARB, Vertices, GL_DYNAMIC_DRAW_ARB);    def load_image():    TextureImage = pygame.image.load('small.png')    TextureString = pygame.image.tostring(TextureImage, "RGBA", 1)    sizeX = TextureImage.get_width()    sizeY = TextureImage.get_height()    TextureID = glGenTextures(1)    glBindTexture(GL_TEXTURE_2D, TextureID)    glTexImage2D( GL_TEXTURE_2D, 0, GL_RGBA, sizeX, sizeY, 0,                      GL_RGBA, GL_UNSIGNED_BYTE, TextureString);    glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST)    glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST)                                     def draw():    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)       glLoadIdentity ()    glEnableClientState(GL_VERTEX_ARRAY)				    glBindBufferARB(GL_ARRAY_BUFFER_ARB, VBOVerts)    glBufferDataARB(GL_ARRAY_BUFFER_ARB, Vertices, GL_DYNAMIC_DRAW_ARB)    glVertexPointer(3, GL_FLOAT, 0, None)    glDrawArrays(GL_POINTS, 0, len(Vertices) )        glDisableClientState(GL_VERTEX_ARRAY)    pygame.display.flip()def main():    video_flags = OPENGL|DOUBLEBUF    pygame.init()    surface = pygame.display.set_mode((640,480), video_flags)    resize((640,480))    init()    load_image()    make_VBO()        while True:        event = pygame.event.poll()        if event.type == QUIT or (event.type == KEYDOWN and event.key == K_ESCAPE):            break        t = time.clock()        draw()        t = time.clock() - t        admin.timeline += 1        if admin.timeline % 30 == 0:            print 1.0/t, 'Hz'try:    main()except:    traceback.print_exc()    raw_input("Please enjoy your error, then press enter to exit.")    pygame.quit()    sys.exit()


[Edited by - moonpirate on May 24, 2010 2:34:46 PM]
I just wanted to pop in and say that I got my problem sorted out. It turns out that, for whatever reason, OpenGL is very slow on my computer unless I generate mipmaps for my textures. The bare-bones NeHe tutorials weren't doing this, which means that my test scripts based on them also weren't. Fortunately it turns out that generating mipmaps is pretty straightforward; I just replaced
GL.glTexImage2D(GL.GL_TEXTURE_2D, 0, GL.GL_RGBA,                 surface.get_width(), surface.get_height(),                 0, GL.GL_RGBA, GL.GL_UNSIGNED_BYTE, textureData)
with
GLU.gluBuild2DMipmaps(GL.GL_TEXTURE_2D, GL.GL_RGBA, surface.get_width(),        surface.get_height(), GL.GL_RGBA, GL.GL_UNSIGNED_BYTE, textureData)
and
GL.glTexParameterf(GL.GL_TEXTURE_2D, GL.GL_TEXTURE_MIN_FILTER, GL.GL_NEAREST)
with
GL.glTexParameterf(GL.GL_TEXTURE_2D, GL.GL_TEXTURE_MIN_FILTER, GL.GL_NEAREST_MIPMAP_NEAREST)

Ultimately I'm going to want mipmaps anyway so that I can zoom out my map cleanly, so this doesn't exactly hurt me any. Thanks, and sorry for hijacking your thread, Moonpirate. Good luck!
Jetblade: an open-source 2D platforming game in the style of Metroid and Castlevania, with procedurally-generated levels
Quote:Original post by moonpirate
I am however having trouble getting the vertices to draw correctly.

Hmm... it looks like you're defining the vertex buffer with vec2s, but when you glVertexPointer it, you specify 3 coordinates per vertex. Additionally, try doing glMapBuffer right after you upload the data, and read it back out to make sure it hasn't been munged in any way, either by PyOpenGL or by Numeric.

Incidentally, Numeric is really really really old. Most people use Numpy for high-performance bags of numbers in Python now.
Solved my problem, see the bottom of this post!

Quote:Ultimately I'm going to want mipmaps anyway so that I can zoom out my map cleanly, so this doesn't exactly hurt me any. Thanks, and sorry for hijacking your thread, Moonpirate. Good luck!

No problem, thanks for sharing your solution!
Quote:Also, once you become familiar with OpenGL programming, you'll enjoy shaders...

Yikes! I will definitely look into them once I have my basic sprite rendering up and running.
Quote:you can also disable error checking

Yea I already have done this, seems silly though since I still get plenty of errors!
Quote:Incidentally, Numeric is really really really old. Most people use Numpy for high-performance bags of numbers in Python now.

It's actually numpy, the code I was using was just really old and did import numpy as Numeric for some reason.


EDIT/UPDATE: I just read something on the now depreciated vertex arrays, and I tried implementing one and... well it works perfectly. So something must definitely be going wrong in the buffering. I have seen that glGenBuffers takes 2 arguments, but in PyOpenGL it will only accept one. Perhaps this is the issue?

UPDATE2: FIXED! Change
Vertices = numpy.array(Vertices)

to

Vertices = numpy.array(Vertices, 'f')

the end =]

[Edited by - moonpirate on May 25, 2010 5:44:08 PM]
Nice. What's your final framerate?
Quote:Original post by Sneftel
Nice. What's your final framerate?


Without updating the vertices every frame (static draw) I can draw 2500 8x8 pixel sprites at ~1400 Hz. Updating the vertices is quite taxing because of python's sluggishness, but I can still pull off ~300 Hz (and my goal was 2000 sprites at 240Hz).

Optimally I would grab the sprites' coordinates from python and update the array in a C module, then pass the array back to python. But would this actually increase the speed? Would passing the data back and forth be just as slow as updating the array in python? (I can take this elsewhere since it's hardly an openGL question!)

[Edited by - moonpirate on May 25, 2010 9:17:16 PM]
Quote:Original post by moonpirate
But would this actually increase the speed?
It might. You could get similar speed-ups from using numpy in a vectorized fashion to update the coordinates. This would be ideal for something like each particle having a position, a velocity, and a die time... you'd use vectorized array operations to move the particles, and then a take operation to filter out the dead ones.

This topic is closed to new replies.

Advertisement