←Back
The original version of this document for the C/C++ language is available there →

OpenGL Vertex Buffer Object (VBO) in OCaml

Related Topics: Vertex Array,
Download: vboSimple.tgz

GL_ARB_vertex_buffer_object extension is intended to enhance the performance of OpenGL by providing the benefits of vertex array and display list, while avoiding downsides of their implementations. Vertex buffer object (VBO) allows vertex array data to be stored in high-performance graphics memory on the server side and promotes efficient data transfer. If the buffer object is used to store pixel data, it is called Pixel Buffer Object (PBO).

Using vertex array can reduce the number of function calls and redundant usage of the shared vertices. However, the disadvantage of vertex array is that vertex array functions are in the client state and the data in the arrays must be re-sent to the server each time when it is referenced.

On the other hand, display list is server side function, so it does not suffer from overhead of data transfer. But, once a display list is compiled, the data in the display list cannot be modified.

Vertex buffer object (VBO) creates "buffer objects" for vertex attributes in high-performance memory on the server side and provides same access functions to reference the arrays, which are used in vertex arrays, such as glVertexPointer(), glNormalPointer(), glTexCoordPointer(), etc.

The memory manager in vertex buffer object will put the buffer objects into the best place of memory based on user's hints: "target" and "usage" mode. Therefore, the memory manager can optimize the buffers by balancing between 3 kinds of memory: system, AGP and video memory.

Unlike display lists, the data in vertex buffer object can be read and updated by mapping the buffer into client's memory space.

Another important advantage of VBO is sharing the buffer objects with many clients, like display lists and textures. Since VBO is on the server's side, multiple clients will be able to access the same buffer with the corresponding identifier.

Creating VBO

Creating a VBO requires 3 steps;

  1. Generate a new buffer object with glGenBuffers.
  2. Bind the buffer object with glBindBuffer.
  3. Copy vertex data to the buffer object with glBufferData.

These functions reside in the module named VBO of glMLite.

glGenBuffer(s)

glGenBuffers creates buffer objects and returns the identifiers of the buffer objects. It requires 1 parameter: the number of buffer objects to create, and it returns a single VBO ID or multiple VBO IDs. If you want to create only one VBO, you can use glGenBuffer().

val glGenBuffers : n:int -> vbo_id array
val glGenBuffer : unit -> vbo_id

glBindBuffer

Once the buffer object has been created, we need to hook the buffer object with the corresponding ID before using the buffer object. glBindBuffer takes 2 parameters: target and vbo_id.

val glBindBuffer : target:buffer_object_target -> vbo:vbo_id -> unit

Target is a hint to tell VBO whether this buffer object will store vertex array data or index array data: GL_ARRAY_BUFFER, or GL_ELEMENT_ARRAY_BUFFER. Any vertex attributes, such as vertex coordinates, texture coordinates, normals and color component arrays should use GL_ARRAY_BUFFER. Index array which is used for glDraw[Range]Elements() should be tied with GL_ELEMENT_ARRAY_BUFFER. Note that this target flag assists VBO to decide the most efficient locations of buffer objects, for example, some systems may prefer indices in AGP or system memory, and vertices in video memory.

Once glBindBuffer is first called, VBO initializes the buffer with a zero-sized memory buffer and set the initial VBO states, such as usage and access properties.

glBufferData

You can copy the data into the buffer object with glBufferData when the buffer has been initialized.

val glBufferData : target:buffer_object_target -> size:int -> data:('a, 'b, Bigarray.c_layout) Bigarray.Array1.t -> usage:vbo_usage_pattern -> unit

Again, the first parameter, target would be GL_ARRAY_BUFFER or GL_ELEMENT_ARRAY_BUFFER. Size is the number of bytes of data to transfer, you can use the function ba_sizeof to calculate and provide this value. The third parameter is the big array containing the source data. If you use glBufferDataNull instead, then no data parameter is provided and then VBO reserves only memory space with the given data size. The last parameter, "usage" flag is another performance hint for VBO to provide how the buffer object is going to be used: static, dynamic or stream, and read, copy or draw.

VBO specifies 9 enumerated values for usage flags:

GL_STATIC_DRAW
GL_STATIC_READ
GL_STATIC_COPY
GL_DYNAMIC_DRAW
GL_DYNAMIC_READ
GL_DYNAMIC_COPY
GL_STREAM_DRAW
GL_STREAM_READ
GL_STREAM_COPY

"Static" means the data in VBO will not be changed (specified once and used many times), "dynamic" means the data will be changed frequently (specified and used repeatedly), and "stream" means the data will be changed every frame (specified once and used once). "Draw" means the data will be sent to GPU in order to draw (application to GL), "read" means the data will be read by the client's application (GL to application), and "copy" means the data will be used both drawing and reading (GL to GL).

Note that only draw token is useful for VBO, and copy and read token will become meaningful only for pixel/frame buffer object (PBO or FBO).

VBO memory manager will choose the best memory places for the buffer object based on these usage flags, for example, GL_STATIC_DRAW and GL_STREAM_DRAW may use video memory, and GL_DYNAMIC_DRAW may use AGP memory. Any _READ_ related buffers would be fine in system or AGP memory because the data should be easy to access.

glBufferSubData

val glBufferSubData: target:buffer_object_target -> offset:int -> size:int -> data:('a, 'b, Bigarray.c_layout) Bigarray.Array1.t -> unit

Like glBufferData, glBufferSubData is used to copy data into VBO, but it only replaces a range of data into the existing buffer, starting from the given offset. (The total size of the buffer must be set by glBufferData before using glBufferSubData.)

glDeleteBuffers

val glDeleteBuffers : vbos:vbo_id array -> unit val glDeleteBuffer : vbo:vbo_id -> unit

You can delete a single VBO or multiple VBOs with glDeleteBuffer(s) if they are not used anymore. After a buffer object is deleted, its contents will be lost.

The following code is an example of creating a single VBO for vertex coordinates. Notice that the big array containing the datas can be garbage collected in your application after you copy data into VBO.

GLuint vboId;                              (* ID of VBO *)
GLfloat* vertices = new GLfloat[vCount*3]; (* create vertex array *)
...

(* generate a new VBO and get the associated ID *)
let vboId = glGenBuffer() in

(* bind VBO in order to use *)
glBindBuffer GL_ARRAY_BUFFER vboId;

(* upload data to VBO *)
let dataSize = ba_sizeof vertices in
glBufferData GL_ARRAY_BUFFER dataSize vertices GL_STATIC_DRAW;

(* it is safe let ocaml garbage collect after copying data to VBO *)
Gc.full_major();
...

(* delete VBO when program terminated *)
glDeleteBuffer vboId;

Drawing VBO

Because VBO sits on top of the existing vertex array implementation, rendering VBO is almost same as using vertex array. Only difference is that the pointer to the vertex array is now as an offset into a currently bound buffer object. Therefore, no additional APIs are required to draw a VBO except glBindBuffer.

// bind VBOs for vertex array and index array
glBindBuffer GL_ARRAY_BUFFER vboId1;         (* for vertex coordinates *)
glBindBuffer GL_ELEMENT_ARRAY_BUFFER vboId2; (* for indices *)

(* do same as vertex array except pointer *)
glEnableClientState GL_VERTEX_ARRAY;         (* activate vertex coords array *)
glVertexPointer 3 GL_FLOAT 0 0;              (* last param is offset, not ptr *)

(* draw 6 quads using offset of index array *)
glDrawElements GL_QUADS 24 GL_UNSIGNED_BYTE 0;

glDisableClientState GL_VERTEX_ARRAY;        (* deactivate vertex array *)

(* unbind, so switch back to normal pointer operation *)
glUnbindBuffer GL_ARRAY_BUFFER;
glUnbindBuffer GL_ELEMENT_ARRAY_BUFFER;

Unbinding the buffer object switchs off VBO operation. It is a good idea to turn VBO off after use, so normal vertex array operations with absolute pointers will be re-activated.

Updating VBO

The advantage of VBO over display list is the client can read and modify the buffer object data, but display list cannot. The simplest method of updating VBO is copying again new data into the bound VBO with glBufferData or glBufferSubData. For this case, your application should have a valid vertex array all the time in your application. That means that you must always have 2 copies of vertex data: one in your application and the other in VBO.

The other way to modify buffer object is to map the buffer object into client's memory, and the client can update data with the pointer to the mapped buffer. The following describes how to map VBO into client's memory and how to access the mapped data.

glMapBuffer

VBO provides glMapBuffer in order to map the buffer object into client's memory.

val glMapBuffer : target:buffer_object_target -> access:access_policy -> ('a, 'b, Bigarray.c_layout) Bigarray.Array1.t

If OpenGL is able to map the buffer object into client's address space, glMapBuffer returns the big array to the buffer. Otherwise it raises (Failure "glMapBuffer");

The first parameter, target is mentioned earlier at glBindBuffer and the second parameter, access flag specifies what to do with the mapped data: read, write or both.

GL_READ_ONLY
GL_WRITE_ONLY
GL_READ_WRITE

Note that glMapBuffer causes a synchronizing issue. If GPU is still working with the buffer object, glMapBuffer will not return until GPU finishes its job with the corresponding buffer object.

To avoid waiting (idle), you can call first glBufferDataNull, then call glMapBuffer. In this case, the previous data will be discarded and glMapBuffer returns a new allocated pointer immediately even if GPU is still working with the previous data.

However, this method is valid only if you want to update entire data set because you discard the previous data. If you want to change only portion of data or to read data, you better not release the previous data.

glUnmapBuffer

val glUnmapBuffer : target:buffer_object_target -> unit

After modifying the data of VBO, it must be unmapped the buffer object from the client's memory. glUnmapBuffer just returns unit if success. When it raises (Failure "glUnmapBuffer"), the contents of VBO become corrupted while the buffer was mapped. The corruption results from screen resolution change or window system specific events. In this case, the data must be resubmitted.

Here is a sample code to modify VBO with mapping method.

(* bind then map the VBO *)
glBindBuffer GL_ARRAY_BUFFER vboId;
begin try
  let ba = glMapBuffer GL_ARRAY_BUFFER GL_WRITE_ONLY in

  (* the pointer is valid(mapped), update VBO *)
  updateMyVBO ba ...;              (* modify buffer data *)
  glUnmapBuffer GL_ARRAY_BUFFER;   (* unmap it after use *)
with Failure "glMapBuffer" ->
  (* mapping failed *)
end;

(* you can draw the updated VBO *)
...

Example

Example of VBO
This demo application makes a VBO wobbling in and out along normals. It maps a VBO and updates its vertices every frame with the pointer to the mapped buffer. You can compare the performace with a traditional vertex array implementation.

It uses 2 vertex buffers; one for both vertex coords and normals, and the other stores index array only.

Download the source and binary: vboSimple.tgz.

vboSimple is a very simple example to draw a cube using VBO and Vertex Array. You can easily see what is common and what is different between VBO and VA.