Only had 1/2 day to work on PyOpenGL today (client needed work done this morning), so just played around with the Vertex Buffer Object (VBO) extension. In particular I added support for mapping data into the arrays and automatically deleting the arrays if the user forgets to do so. For those following along at home...
What are Vertex Buffer Objects?
As you will recall from PyOpenGL 101, there are two major approaches to speeding up PyOpenGL code. Both can be thought of as 'push the data as close to the hardware as possible'. The first approach is to use display lists, the second to use (large) arrays of contiguous data.
Display lists are mechanisms that let you run your client side (i.e. Python or C) code in a special mode where the GL records the set of operations performed. When you want to replay that sequence of events, you can tell the GL to replay the entire sequence with a single command.
The GL can optimize the operations within the display list if it knows how, but I don't think there's all that much optimization done on current hardware. That said, display lists can be wickedly fast for static models and let you use whatever exotic algorithms you want to generate the model. You can use low-level GL primitives (glNormal, glVertex, glColor, those present in OpenGL since 1.0) which are easy for new OpenGL programmers to understand.
The second major approach to speeding up OpenGL code is to make use of large arrays of homogenous data. This approach was introduced in OpenGL 1.1. It sets up Single Instruction Multiple Dispatch (SIMD) data-streaming operations to process your model from arrays of data-points to sets of graphics operations. It is especially effective in the most common case where your pieces of geometry are composed of extremely large numbers of elements which are all treated in approximately the same way (think higher-resolution modern models).
Particularly with PyOpenGL, the use of array-based data lets you avoid the cost of the large numbers of primitive OpenGL calls associated with creating large geometric objects in the "traditional" (OpenGL 1.1) form. A given PyOpenGL function call is likely 100 to 10,000 times slower than the equivalent C call, so defining geometry with 100s of thousands of data-points can be ridiculously slow using primitive OpenGL calls.
The SIMD approach amplifies the effect of each OpenGL operation. Array-using code is able to minimize the overhead of running in Python considerably. By pushing all of our data into a couple of large arrays we can reduce the Python overhead of rendering even extremely large scenes to a couple of function calls.
Interesting note; OpenGL-ES, the spec for lightweight mobile implementations of OpenGL is entirely array-centric. If you want to develop for the iPhone you better learn the array-based approach to OpenGL. It seems that with arrays we have solved all the problems of OpenGL (yay!).
In OpenGL there are two sets of code which are communicating, the client code (the code you write) and the GL. The GL is normally implemented as a mix of software (driver) and hardware, with modern cards often having extremely advanced hardware implementations that can churn through vast amounts of data and have almost as much memory as the host system.
Display lists, textures and rendering areas are all stored in the card's hardware memory whenever possible. Only if they exceed the memory available are they stored in the GL's "software" buffers. Whenever data is available only in the software buffers the GL has to transfer the data each time it is rendered across the graphics bus. Even though this bus is normally the fastest peripheral bus in your system and has special operations to help with the sharing and streaming of data (the acronym AGP referred to such a mechanism), transferring large amounts of data across it is going to limit your rendering speed.
This is why your cards are constantly growing more and more memory. You want to avoid copies across the graphics bus as much as possible. You want to make sure that as much of your data is over on the card as is possible. Polygon (vertex, colour, normal) data was not in OpenGL 1.1 times, however, normally stored on the card (other than in display lists).
In OpenGL 1.1 times, the bulk of the space for your world-models was in textures, so it makes sense that one of the first things to move to the GL's storage was textures. At the time, models would be a few thousand polygons at most, as that was about what cards could chew through in terms of textured polygons. Those same models, however, might have a couple of megabytes of textures to be applied.
There might have been a few hundred KB of data in given scene's polygonal data (and that only if full precision models were used). Compared to the bandwidth and memory consumed by large collections of textures, the polygonal data was just noise, so there was no reason to complicate the hardware by storing the polygonal data there.
As a result, with the OpenGL 1.1 (array-based) approach, you would transfer your few hundred KB of vertex data across the bus every time you wanted to render a model. This meant that you could modify your array-based data in memory as the next time you sent it the new data would be transferred. Compared to (static) display lists, this made array-based data the natural choice for frequently updated geometry.
Why Vertex Buffer Objects Now?
Graphics processors have advanced a long way since the days of OpenGL 1.1. Today's high-end implementations are capable of rendering meshes with around 10 million vertices. While they can process meshes with millions of vertices, with OpenGL 1.1 style arrays the (uncompressed, full-precision) data required to represent millions of vertices would be hundreds of MB, needing to be tranferred ~60 times per second across the graphics bus. That is, while graphics processors can process vast amounts of vertex data, they can't access it fast enough if we use traditional OpenGL 1.1 array storage.
The VBO is another example of moving data over to the graphics card as much as possible. In this case, we move the (now very large) vertex data to the card then tell the card to use the stored data instead of client-side vertex-sets when we want to render our geometry using array-based operations. This is the same basic operation we've done with textures and display lists. We store the data on the card with an ID for later reference and trigger its use when we would normally re-transfer the data.
However, recall that arrays have commonly been used for dynamic data. That is, we often want to change the data in a model. Consider for instance when we want to make our character "bend" in a game to take a bow. We want to remap (normally a portion of) the data with new locations, preferably without having to re-transfer the whole data-set (colours, normals, texture coordinates, the unchanged vertices).
To support this kind of usage, the VBO needs to provide a mechanism whereby the client code can (cheaply) update portions of certain VBOs (just the dynamic ones). VBOs handle this by allowing you to either "map" the current contents into memory (get a pointer to the data which you can manipulate as you wish) or by allowing you to transfer portions of the data to replace parts of the current data.
What VBOs Look Like
VBO code looks pretty much like regular OpenGL 1.1 array-using code. You build the same arrays (e.g. with Numpy), but instead of passing the arrays into PyOpenGL, you set up a VBO and load the data into it:
id = glGenBuffersARB(1)
glBufferDataARB(target, data, usage)
where target is basically one of vertex data or index data and usage is an enumerant telling the system how often you intend to update the buffer (static, dynamic or streaming) so that it can decide where/how to store the data.
When you want to use the data, you bind the buffer again and then pass 0/NULL to the OpenGL 1.1 array-drawing code as the data-pointer for your array.
glVertexPointerd( 0 )
glDrawElements( GL_LINE_LOOP, len(indices), GL_UNSIGNED_INT, indices )
unbinding the vbo after you've finished drawing your data.
If you want to update a range of your vertex data, you can use:
glBufferSubData(target, start, size, dataptr)
which maps a range of raw data (bytes) into your array in the GL. You can also map portions of the array data into your application's memory space by using:
Though to be honest I still haven't figured out what to do with this other than reading the value out, which might be useful for pixel data, but doesn't seem as useful for the vertex data.
What am I doing with VBOs in PyOpenGL?
At the moment I'm really just playing around with possible APIs to make an elegant, Pythonic API that integrates nicely with PyOpenGL. It makes the VBO look like a regular FormatHandler type, that is, a type of thing in which data is stored, similar to Numpy arrays or Ctypes arrays/pointers. You define the VBO by passing in a data-array, you update portions of it with slice assignment, you pass it as the argument to e.g. glVertexPointer. When you delete the VBO object the VBO is deleted on the back-end.
Originally (last week) I had the VBO automatically binding itself when you got its data-pointer, but that just seemed too "magic-y", now you have to manually bind your VBO by calling its "bind" method and unbind it with the unbind method.
I still need to do some work with frame buffer objects (textures to which you can render) to see if there's code that wants to be shared. I also need to see whether providing a mapping api is useful for something in PyOpenGL.
I've very much a newbie with VBO usage, so I'd love feedback from heavy VBO users on what you need in an API. The OpenGL.arrays.vbo module isn't likely to suit every project, but I'd like it to be a fairly generic mechanism that lets the beginning PyOpenGL developer use the feature without too much effort and with maximal benefit.
The current code is available in CVS.
Pingbacks are closed.