OpenGL Vertex Buffer Object (VBO)

Related Topics: Vertex Array, Display List, Pixel Buffer Object
Download: vbo.zip, vboCube.zip, vboCubeTex.zip, vboCubeShader.zip

Update: Vertex buffer object extension is promoted as a core feature of OpenGL version 2.1, and removed ARB suffix from APIs.

Overview

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().

glGenBuffers()

glGenBuffers() creates buffer objects and returns the identifiers of the buffer objects. It requires 2 parameters: the first one is the number of buffer objects to create, and the second parameter is the address of a GLuint variable or array to store a single ID or multiple IDs.


void glGenBuffers(GLsizei n, GLuint* ids)

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 ID.


void glBindBuffer(GLenum target, GLuint id)

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.


void glBufferData(GLenum target, GLsizei size, const void* data, GLenum usage)

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. The third parameter is the pointer to the array of source data. If data is NULL pointer, 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 be 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()


void glBufferSubData(GLenum target, GLint offset, GLsizei size, void* data)

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()


void glDeleteBuffers(GLsizei n, const GLuint* ids)

You can delete a single VBO or multiple VBOs with glDeleteBuffers() 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 you can delete the memory allocation for vertex array 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
glGenBuffers(1, &vboId);

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

// upload data to VBO
glBufferData(GL_ARRAY_BUFFER, dataSize, vertices, GL_STATIC_DRAW);

// it is safe to delete after copying data to VBO
delete [] vertices;
...

// delete VBO when program terminated
glDeleteBuffers(1, &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().

Binding the buffer object with 0 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.


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

glEnableClientState(GL_VERTEX_ARRAY);             // activate vertex position array
glEnableClientState(GL_NORMAL_ARRAY);             // activate vertex normal array
glEnableClientState(GL_TEXTURE_COORD_ARRAY);      // activate texture coord array

// do same as vertex array except pointer
glVertexPointer(3, GL_FLOAT, stride, offset1);    // last param is offset, not ptr
glNormalPointer(GL_FLOAT, stride, offset2);
glTexCoordPointer(2, GL_FLOAT, stride, offset3);

// draw 6 faces using offset of index array
glDrawElements(GL_TRIANGLES, 36, GL_UNSIGNED_BYTE, 0);

glDisableClientState(GL_VERTEX_ARRAY);            // deactivate vertex position array
glDisableClientState(GL_NORMAL_ARRAY);            // deactivate vertex normal array
glDisableClientState(GL_TEXTURE_COORD_ARRAY);     // deactivate vertex tex coord array

// bind with 0, so, switch back to normal pointer operation
glBindBuffer(GL_ARRAY_BUFFER, 0);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);

OpenGL version 2.0 added glVertexAttribPointer(), glEnableVertexAttribArray() and glDisableVertexAttribArray() functions to specify generic vertex attributes. Therefore, you can specify all vertex attributes; position, normal, colour and texture coordinate, using single API.


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

glEnableVertexAttribArray(attribVertex);          // activate vertex position array
glEnableVertexAttribArray(attribNormal);          // activate vertex normal array
glEnableVertexAttribArray(attribTexCoord);        // activate texture coords array

// set vertex arrays with generic API
glVertexAttribPointer(attribVertex,   3, GL_FLOAT, false, stride, offset1);
glVertexAttribPointer(attribNormal,   3, GL_FLOAT, false, stride, offset2);
glVertexAttribPointer(attribTexCoord, 2, GL_FLOAT, false, stride, offset3);

// draw 6 faces using offset of index array
glDrawElements(GL_TRIANGLES, 36, GL_UNSIGNED_BYTE, 0);

glDisableVertexAttribArray(attribVertex);         // deactivate vertex position
glDisableVertexAttribArray(attribNormal);         // deactivate vertex normal
glDisableVertexAttribArray(attribTexCoord);       // deactivate texture coords

// bind with 0, so, switch back to normal pointer operation
glBindBuffer(GL_ARRAY_BUFFER, 0);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);

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.


void* glMapBuffer(GLenum target, GLenum access)

If OpenGL is able to map the buffer object into client's address space, glMapBuffer() returns the pointer to the buffer. Otherwise it returns NULL.

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 glBufferData() with NULL pointer, 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()


GLboolean glUnmapBuffer(GLenum target)

After modifying the data of VBO, it must be unmapped the buffer object from the client's memory. glUnmapBuffer() returns GL_TRUE if success. When it returns GL_FALSE, 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);
float* ptr = (float*)glMapBuffer(GL_ARRAY_BUFFER, GL_WRITE_ONLY);

// if the pointer is valid(mapped), update VBO
if(ptr)
{
    updateMyVBO(ptr, ...);             // modify buffer data
    glUnmapBuffer(GL_ARRAY_BUFFER);    // unmap it after use
}

// you can draw the updated VBO
...

Example: Drawing a Cube

drawing a cube with glDrawElements()
drawing a cube with texture

This example is to draw a unit cube with glDrawElements().
Download: vboCube.zip, vboCubeTex.zip

A unit cube can be defined with the following arrays; vertices, normals and colors. A cube has 6 faces and 4 vertices per face, so the number of elements in each array is 24 (6 sides × 4 vertices).


// unit cube      
// A cube has 6 sides and each side has 4 vertices, therefore, the total number
// of vertices is 24 (6 sides * 4 verts), and 72 floats in the vertex array
// since each vertex has 3 components (x,y,z) (= 24 * 3)
//    v6----- v5  
//   /|      /|   
//  v1------v0|   
//  | |     | |   
//  | v7----|-v4  
//  |/      |/    
//  v2------v3    

// vertex position array
GLfloat vertices[]  = {
     .5f, .5f, .5f,  -.5f, .5f, .5f,  -.5f,-.5f, .5f,  .5f,-.5f, .5f, // v0,v1,v2,v3 (front)
     .5f, .5f, .5f,   .5f,-.5f, .5f,   .5f,-.5f,-.5f,  .5f, .5f,-.5f, // v0,v3,v4,v5 (right)
     .5f, .5f, .5f,   .5f, .5f,-.5f,  -.5f, .5f,-.5f, -.5f, .5f, .5f, // v0,v5,v6,v1 (top)
    -.5f, .5f, .5f,  -.5f, .5f,-.5f,  -.5f,-.5f,-.5f, -.5f,-.5f, .5f, // v1,v6,v7,v2 (left)
    -.5f,-.5f,-.5f,   .5f,-.5f,-.5f,   .5f,-.5f, .5f, -.5f,-.5f, .5f, // v7,v4,v3,v2 (bottom)
     .5f,-.5f,-.5f,  -.5f,-.5f,-.5f,  -.5f, .5f,-.5f,  .5f, .5f,-.5f  // v4,v7,v6,v5 (back)
};

// normal array
GLfloat normals[] = {
     0, 0, 1,   0, 0, 1,   0, 0, 1,   0, 0, 1,  // v0,v1,v2,v3 (front)
     1, 0, 0,   1, 0, 0,   1, 0, 0,   1, 0, 0,  // v0,v3,v4,v5 (right)
     0, 1, 0,   0, 1, 0,   0, 1, 0,   0, 1, 0,  // v0,v5,v6,v1 (top)
    -1, 0, 0,  -1, 0, 0,  -1, 0, 0,  -1, 0, 0,  // v1,v6,v7,v2 (left)
     0,-1, 0,   0,-1, 0,   0,-1, 0,   0,-1, 0,  // v7,v4,v3,v2 (bottom)
     0, 0,-1,   0, 0,-1,   0, 0,-1,   0, 0,-1   // v4,v7,v6,v5 (back)
};

// colour array
GLfloat colors[] = {
     1, 1, 1,   1, 1, 0,   1, 0, 0,   1, 0, 1,  // v0,v1,v2,v3 (front)
     1, 1, 1,   1, 0, 1,   0, 0, 1,   0, 1, 1,  // v0,v3,v4,v5 (right)
     1, 1, 1,   0, 1, 1,   0, 1, 0,   1, 1, 0,  // v0,v5,v6,v1 (top)
     1, 1, 0,   0, 1, 0,   0, 0, 0,   1, 0, 0,  // v1,v6,v7,v2 (left)
     0, 0, 0,   0, 0, 1,   1, 0, 1,   1, 0, 0,  // v7,v4,v3,v2 (bottom)
     0, 0, 1,   0, 0, 0,   0, 1, 0,   0, 1, 1   // v4,v7,v6,v5 (back)
};

// texture coord array
GLfloat texCoords[] = {
    1, 0,   0, 0,   0, 1,   1, 1,               // v0,v1,v2,v3 (front)
    0, 0,   0, 1,   1, 1,   1, 0,               // v0,v3,v4,v5 (right)
    1, 1,   1, 0,   0, 0,   0, 1,               // v0,v5,v6,v1 (top)
    1, 0,   0, 0,   0, 1,   1, 1,               // v1,v6,v7,v2 (left)
    0, 1,   1, 1,   1, 0,   0, 0,               // v7,v4,v3,v2 (bottom)
    0, 1,   1, 1,   1, 0,   0, 0                // v4,v7,v6,v5 (back)
};

// index array for glDrawElements()
// A cube requires 36 indices = 6 sides * 2 tris * 3 verts
GLuint indices[] = {
     0, 1, 2,   2, 3, 0,    // v0-v1-v2, v2-v3-v0 (front)
     4, 5, 6,   6, 7, 4,    // v0-v3-v4, v4-v5-v0 (right)
     8, 9,10,  10,11, 8,    // v0-v5-v6, v6-v1-v0 (top)
    12,13,14,  14,15,12,    // v1-v6-v7, v7-v2-v1 (left)
    16,17,18,  18,19,16,    // v7-v4-v3, v3-v2-v7 (bottom)
    20,21,22,  22,23,20     // v4-v7-v6, v6-v5-v4 (back)
};

After the vertex attribute arrays and index array was defined, they can be copied into VBOs. One VBO is used to store all vertex attribute arrays one after the other by using glBufferSubData() with the array size and offsets, for example, [VVV...NNN...CCC...TTT...]. The second VBO is for the index data only.


// create VBOs
glGenBuffers(1, &vboId);    // for vertex buffer
glGenBuffers(1, &iboId);    // for index buffer

size_t vSize = sizeof vertices;
size_t nSize = sizeof normals;
size_t cSize = sizeof colors;
size_t tSize = sizeof texCoords;

// bind VBO and allocate space first
glBindBuffer(GL_ARRAY_BUFFER, vboId);
glBufferData(GL_ARRAY_BUFFER, vSize+nSize+cSize+tSize, 0, GL_STATIC_DRAW);

// copy vertex attribs data to VBO
glBufferSubData(GL_ARRAY_BUFFER, 0,                 vSize, vertices);  // copy verts at offset 0
glBufferSubData(GL_ARRAY_BUFFER, vSize,             nSize, normals);   // copy norms after verts
glBufferSubData(GL_ARRAY_BUFFER, vSize+nSize,       cSize, colors);    // copy cols after norms
glBufferSubData(GL_ARRAY_BUFFER, vSize+nSize+cSize, tSize, texCoords); // copy texs after cols

// unbind VBO
glBindBuffer(GL_ARRAY_BUFFER, 0);

// copy index data to VBO
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, iboId);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);

Drawing VBO using OpenGL fixed pipeline is almost identical to Vertex Array. The only difference is to specify the memory offsets where the data are stored, instead of the pointers to the arrays. For OpenGL programmable pipeline using vertex/fragment shaders, please refer to the next example, Drawing a Cube with Shader.


// bind VBOs before drawing
glBindBuffer(GL_ARRAY_BUFFER, vboId);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, iboId);

// enable vertex arrays
glEnableClientState(GL_VERTEX_ARRAY);
glEnableClientState(GL_NORMAL_ARRAY);
glEnableClientState(GL_COLOR_ARRAY);
glEnableClientState(GL_TEXTURE_COORD_ARRAY);

size_t nOffset = sizeof vertices;
size_t cOffset = nOffset + sizeof normals;
size_t tOffset = cOffset + sizeof colors;

// specify vertex arrays with their offsets
glVertexPointer(3, GL_FLOAT, 0, (void*)0);
glNormalPointer(GL_FLOAT, 0, (void*)nOffset);
glColorPointer(3, GL_FLOAT, 0, (void*)cOffset);
glTexCoordPointer(2, GL_FLOAT, 0, (void*)tOffset);

// finally draw a cube with glDrawElements()
glDrawElements(GL_TRIANGLES,            // primitive type
               36,                      // # of indices
               GL_UNSIGNED_INT,         // data type
               (void*)0);               // offset to indices

// disable vertex arrays
glDisableClientState(GL_VERTEX_ARRAY);
glDisableClientState(GL_NORMAL_ARRAY);
glDisableClientState(GL_COLOR_ARRAY);
glDisableClientState(GL_TEXTURE_COORD_ARRAY);

// unbind VBOs
glBindBuffer(GL_ARRAY_BUFFER, 0);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);

Example: Drawing a Cube with Shader

drawing a cube with GLSL shader

Download: vboCubeShader.zip (with GLFW, 2023-05-12)

This example is to draw a cube with GLSL shader and GLFW framework. Creating and storing data to VBOs is the same as Vertex Array mode. The only difference in GLSL mode is specifying the offset of the vertex buffer data to draw VBOs. OpenGL v2.0 introduces a new generic function, glVertexAttribPointer() to specify the offset for any vertex attribute types, instead of using type-specific functions; glVertexPointer(), glNormalPointer(), glColorPointer(), etc.

OpenGL v2.1 adds VAO (Vertex Array Object) extension to encapsulate these vertex attributes into a VAO only once during initialization of VBOs and to quickly switch between vertex array attributes without calling glVertexAttribPointer(), glNormalPointer(), glColorPointer() every frame. And, VAO is promoted as a core feature in OpenGL version 3.0.


// bind VAO and GLSL program
glUseProgram(progId);
glBindTexture(GL_TEXTURE_2D, texId);
glBindVertexArray(vaoId);

// draw VBO
glDrawElements(GL_TRIANGLES,            // primitive type
               36,                      // # of indices
               GL_UNSIGNED_INT,         // data type
               (void*)0);               // offset to indices

// unbind
glBindVertexArray(0);
glBindTexture(GL_TEXTURE_2D, 0);
glUseProgram(0);

Example: Updating Vertex Data using glMapBuffer()

Example of VBO with memory mapping

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 the vertex positions and normals, and the other stores the index array only.

Download: vbo.zip (Updated: 2018-08-15).


// bind VBO
glBindBuffer(GL_ARRAY_BUFFER, vboId);

// map the current VBO into client's memory,
// so, this application can directly access VBO
// Note that glMapBuffer() causes sync issue.
// If GPU is working with this buffer, glMapBufferARB() will wait(stall)
// for GPU to finish its job.
float* ptr = (float*)glMapBuffer(GL_ARRAY_BUFFER, GL_READ_WRITE);
if(ptr)
{
    // wobble vertex in and out along normal
    updateVertices(ptr, srcVertices, teapotNormals, vertexCount, timer.getElapsedTime());

    // release pointer to mapped buffer after use
    glUnmapBuffer(GL_ARRAY_BUFFER);
}

// unbind VBO
glBindBuffer(GL_ARRAY_BUFFER, 0);

Every example includes Code::Blocks project file for Windows, and the makefiles (Makefile.linux and Makefile.mac) for linux system and macOS in src folder, so you can build an executable on your system, for example:


> make -f Makefile.linux
> make -f Makefile.mac

←Back
 
Hide Comments
comments powered by Disqus