pythonopenglnumpypyopenglvertex-buffer

pyopengl - dynamically updating values in a vertex buffer object


I'd like to create polygons with draggable vertices in PyOpenGL. Having read around a bit, VBOs seemed like a sensible way to achieve this.

Having never used VBOs before, I'm having trouble figuring out how to dynamically update them - ideally I'd like to just modify elements of a numpy array of vertices, then propagate only the elements that changed up to the GPU. I had assumed that the OpenGL.arrays.vbo.VBO wrapper did this automagically with its copy_data() method, but it seems not.

Here's a silly example:

from OpenGL import GL as gl
from OpenGL import GLUT as glut
from OpenGL.arrays import vbo
import numpy as np

class VBOJiggle(object):

    def __init__(self,nvert=100,jiggliness=0.01):
        self.nvert = nvert
        self.jiggliness = jiggliness

        verts = 2*np.random.rand(nvert,2) - 1
        self.verts = np.require(verts,np.float32,'F')
        self.vbo = vbo.VBO(self.verts)

    def draw(self):

        gl.glClearColor(0,0,0,0)
        gl.glClear(gl.GL_COLOR_BUFFER_BIT)

        gl.glEnableClientState(gl.GL_VERTEX_ARRAY)

        self.vbo.bind()

        gl.glVertexPointer(2,gl.GL_FLOAT,0,self.vbo)
        gl.glColor(0,1,0,1)
        gl.glDrawArrays(gl.GL_LINE_LOOP,0,self.vbo.data.shape[0])

        gl.glDisableClientState(gl.GL_VERTEX_ARRAY)

        self.vbo.unbind()

        self.jiggle()

        glut.glutSwapBuffers()

    def jiggle(self):

        # jiggle half of the vertices around randomly
        delta = (np.random.rand(self.nvert//2,2) - 0.5)*self.jiggliness
        self.verts[:self.nvert:2] += delta

        # the data attribute of the vbo is the same as the numpy array
        # of vertices
        assert self.verts is self.vbo.data

        # # Approach 1:
        # # it seems like this ought to work, but it doesn't - all the
        # # vertices remain static even though the vbo's data gets updated
        # self.vbo.copy_data()

        # Approach 2:
        # this works, but it seems unnecessary to copy the whole array
        # up to the GPU, particularly if the array is large and I have
        # modified only a small subset of vertices
        self.vbo.set_array(self.verts)

if __name__ == '__main__':
    glut.glutInit()
    glut.glutInitDisplayMode( glut.GLUT_DOUBLE | glut.GLUT_RGB )
    glut.glutInitWindowSize( 250, 250 )
    glut.glutInitWindowPosition( 100, 100 )
    glut.glutCreateWindow( None )

    demo = VBOJiggle()
    glut.glutDisplayFunc( demo.draw )
    glut.glutIdleFunc( demo.draw )

    glut.glutMainLoop()

Solution

  • To completely answer this question, I have to mention the OpenGL buffer update first.

    The OpenGL instruction glBufferData creates and initializes a buffer object's data store. An existing data store of an buffer object is completely destroyed and a new data store (possibly with a different size) is created. If a data pointer is passed to the function, then the data store is completely initialized by the data. The size of the buffer and the size of the provided data is assumed to be equal.

    glBufferSubData updates the entire data or a subset of the data of an existing data store. The data store is assumed to be created before, by glBufferData. No data store is destroyed or created.

    Of course, technically glBufferData can always be use instead of glBufferSubData, but glBufferSubData will perform much better, because the expensive buffer creation (allocation) is eliminated.

    Using

    self.vbo.set_array(self.verts)
    

    is a bad idea, because as seen in the implementation (PyOpenGL/OpenGL/arrays/vbo.py), this method creates a completely new buffer, with a possibly new size and will force the recreation of the buffer object's data store (because of self.copied = False).

    If the buffer was created before, then self.vbo.copy_data() will update the data by glBufferSubData (see if self.copied: in copy_data). To make this work the buffer has to be the currently bound buffer (self.vbo.bind()). Further a copy information has to be set (VBO.copy_segments). The copy information is stated by the item setter (VBO.__setitem__).

    This means, in "Approach 1" you would have to do something like the following:

    self.vbo[:] = self.verts
    self.vbo.bind()
    self.vbo.copy_data()
    

    Since OpenGL.arrays.vbo is nothing more than a wrapper for the OpenGL buffer instructions, I would prefer to use glBufferSubData directly, which will perform best in cases like this:

    # Approach 3:
    # Direct buffer update by `glBufferSubData`
    self.vbo.bind()
    self.vbo.implementation.glBufferSubData(self.vbo.target, 0, self.vbo.data)
    

    With this approach even subsets of the data store can be updated. Note the 2nd parameter of glBufferSubData is a byte offset to the buffer objects data store. Further there is an overloaded implementation, which can process a buffer size and a direct data pointer.