I’ve been reading everywhere that VBOs are great. Since I’m having a little downtime not knowing how to carry my project forward, I thought I’d have a look into it. What lured me in is the promise of a dramatic performance increase.

Coding recipe

1. Call the code below, or similar, once (not in the rendering loop) for each array you wish to buffer

// this example illustrates how to load a vertex array buffer.
// only small variations for loading color arrays
// and element (indexed triangle) arrays.
// see the rest of the article for details.
// create a handle that you will use to refer to the buffer when talking to gl
GLuint handle;
glGenBuffers(1,&handle);
// let's load our buffer data...
// size of your buffer, here, 3 times the number of vertices,
// multiplied by your format size, for example sizeof(GL_FLOAT).
int dataByteSize = ...
float* data = malloc(dataByteSize)
// need to populate your buffer with vertex data!
...
// this loads 'data' into graphic memory referred by 'handle'
glBindBuffer(GL_ARRAY_BUFFER,handle);
// Typically you want to use static draw. This implies you will use the buffer
// over and over, in other words you won't modify geometry afterwards.
glBufferData( GL_ARRAY_BUFFER, dataSize, data, GL_STATIC_DRAW);
// clear the binding after loading your array, otherwise you will get crashes
glBindBuffer(GL_ARRAY_BUFFER,0);


2. Put that in your rendering loop to pass the buffer instead of a regular pointer

// this example illustrates how to 'pass pointers' with
// 'pass array buffers'
// note: this example only makes sense if you can do basic gl rendering.
glBindBuffer(GL_ARRAY_BUFFER,handleToColorBuffer);
glVertexPointer(4,GL_FLOAT,0,0 /*replaced the pointer reference by zero*/ );
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER,handleToPerFaceVertexIndices);
glDrawElements(... , 0 /*replaced the pointer reference by zero*/ );

What are VBOs?

Apparently VBO means vertex buffer object. GL-ES allows defining buffers into which we load our vertex (but also color, normal and face indexing) data. Color, normal and coordinate arrays are passed as ‘just array buffers’. Indexed faces are passed using element array buffers.

The promise is that by using buffer objects, we give to OpenGL an opportunity to move the data ‘somewhere fast to access’. I guess that would be graphic memory, if anything, but who knows? Anyway that’s a hardware sensitive topic.

Creating Buffers

First you define a handle variable. Buffers are indexed, so we need handles.

GLuint handle;
glGenBuffers(1,&handle);

This code generates just one handle. That’s what the number ’1′ is saying. We’re passing by reference so if we wanted, we could have allocated several handles within the same memory chunk. I’m not really sure why you’d ever want to do that but… (hey, I didn’t say it wasn’t marginally more efficient, right?)

Next you make the handle ‘current’, or in other words, ‘bind the buffer’. We tell gl that instead of addressing memory using pointers, we will address the buffer referred to by the handle, or, if loading data into the buffer, a buffer will be created for us and referred to by the handle.

glBindBuffer(GL_ARRAY_BUFFER,handle);

There are two ‘channels’ for addressing, one (GL_ARRAY_BUFFER) relates to functions like glColorPointer, glVertexPointer, glNormalPointer. The other refers mainly to glDrawElements (pass GL_ELEMENT_ARRAY_BUFFER).
In this case I’m planning on loading coordinates in the buffer, so I use GL_ARRAY_BUFFER. To load vertex indices (as used by glDrawElements), use GL_ELEMENT_ARRAY_BUFFER.

Next we pass a pointer to the memory block containing our data:

// data is pointing at our vertex coordinates. Something like float* data = malloc(…)
// dataSize is 3 times the number of vertices, multiplied by your format size, e.g sizeof(GL_FLOAT).
glBufferData( GL_ARRAY_BUFFER, dataSize, data, GL_STATIC_DRAW)

GL_STATIC_DRAW is telling gl that we will never change the data pointed at, so it’s safe to copy the data somewhere else (it’s a hint. Check the doc for less useful hints, because if you assert that the data is dynamic, it will be much harder for gl to optimize and your buffer may be a little cosmetic).

Now that we’ve loaded our data into the buffer referred by ‘handle’, it may be a good idea to reset the buffer binding, otherwise we may crash later on (see caveats, below).

glBindBuffer(GL_ARRAY_BUFFER,0);

Rendering from buffers

It is easy to convert gl drawing code using memory pointers to code using buffers. All we need to do is bind the buffer we want to use, then replace whatever pointer we used to use by zero (1).

glBindBuffer(GL_ARRAY_BUFFER,handle);
glVertexPointer(3,GL_FLOAT,0,0 /*replaced the pointer reference by zero*/ );

So we first have to bind the buffer once again, then we call glVertexPointer as we used to, but instead of passing our memory pointer, we pass zero to indicate that we want to read from the first entry in the buffer.

And we do (a) exactly the same thing for color buffers and (b) quite the same thing for element buffers, viz.:

glBindBuffer(GL_ARRAY_BUFFER,handleToColorBuffer);
glVertexPointer(4,GL_FLOAT,0,0 /*replaced the pointer reference by zero*/ );

glBindBuffer(GL_ELEMENT_ARRAY_BUFFER,handleToPerFaceVertexIndices);
glDrawElements(… , 0 /*replaced the pointer reference by zero*/ );

Errors and caveats

  1. Passing GL_ARRAY_BUFFER instead of GL_ELEMENT_ARRAY_BUFFER (or vice versa).
  2. Not calling glBindBuffer(xxx,0) before running an Open-GL call that doesn’t use VBOs.
    Why is that a problem? Because glBindBuffer reinterprets the pointer you pass to glVertextPointer, glColorPointer and so forth…
  3. So if you pass a regular pointer to any of these functions afterwards, your pointer is an offset inside the current buffer, not a memory pointer. Bang.
  4. A typical caveat related to (2) may be when you generate buffers while rendering. Since I have a mix of artwork and procedural geometry, that’s exactly what I do, and I don’t want/cannot use VBOs everywhere. So I bound my buffer for loading them, then moved on to rendering something else, and had a nice crash.
    Either way failing to reset the bindings after assigning them, then calling glDrawElements, will consistently mess your app, and maybe the next 3D app you run afterwards
  5. Not releasing buffer memory. Following  the above examples, we’d call glDeleteBuffers(1,&handle) to release the buffer referred from ‘handle’. Whatever memory is used by buffers is, in my humble experience, quite limited. It would appear that, short of making it a science, a memmove error signals memory shortage when running an iPod touch.

Notwithstanding, I kind of tried three approaches to redirecting my calls to regular memory after using buffers:

  • Call glBindbuffer(xxx,0) whenever you want to draw from client memory (as in, you don’t want to use a buffer, just pass a regular pointer)
  • Call glBindBuffer(xxx,0) as the last thing you do after rendering buffered data.
  • Somehow avoid resetting the binding too often. For example, maybe you want to loop over elements drawn using regular pointers, then you call glBindBuffer(xxx,0) just once (twice if you are binding both the array and element array buffers). Maybe it increases performance – What’s for sure is that this approach is error prone, and may give you pain.

Performance

I guess I’m testing with the wrong hardware. On an iPod Touch 8GB bought at the end of last year, I detected no performance increase. Nothing at all, zero!
Now, don’t expect VBOs to be useless. It’s either me, or the device i’m using. VBOs are good for us.

Notes

(1) Why zero? Actually, instead of using the pointer as a memory pointer, glBindBuffer tells gl that the pointer is ‘x machine units’ starting from the beginning of the graphic memory addressed by the handle. So if somebody knows what machine units are (maybe a float if it’s an array of GL_FLOAT) they might want to use that to draw only portions of the buffer they passed.
You may be tempted to pass null or nil (and find bad literature to inspire you). That’s kind of obfuscating the matter. You don’t pass zero to indicate no pointer, or a null pointer. Zero is really just an offset.