In the previous article about heightmaps, we mentioned the use of Vertex Buffer Objects to speed up drawing.
Being part of the OpenGL standard since version 3.2 buffers are a powerful mechanism to let you store vertex data, texture data, shader data and so on. The key to their speed is that they are stored in GPU memory, which provides fast and efficient access. Before the standardization of the buffers all these data had to reside in system memory making complex scenes and effects slow. We had to rely on extensions that were not always available.
In this article we will discuss the buffer objects as they are used in the HEIGHTMAP example where we store vertex data to speed the drawing of the landscape.
Buffers are powerful mechanism that allows you to store vertex, pixel, texture data, inputs for shader execution, or the output of different shader stages. They are stored in GPU memory resulting in high execution speed. Rendering an object no longer requires sending its geometry to the GPU, a process that is very slow.
Creating and using them is fairly easy. The first thing we have to do is allocate the memory needed. Then we fill it with the data (vertex ccordinates, normals, texture coordinates etc.). Finally in order to draw we tell OpenGL which buffer to use and what it contains.
Here is a step by step description of the steps mentioned above.
1. Create and fill the buffer with data
Call the 'glGenBuffers' passing the variable that will represent the buffer in our program.
Then call 'glBindBuffer' passing it the same variable to tell OpenGL that this is the buffer to use.
Now that we have creted and locked the buffer we can start 'loading' it with data.
When done we call 'glBindBuffer' with NULL for the variable to unlock the buffer
Here we should note that what 'glGenBuffers' does is to bind a variable with a buffer id.
The buffer is actually created upon its first use, in our case when we start loading it.
Here is the code that creates and loads vertex, normals and texture coordinates buffers.
void clf_mesh::build_buffers() { // Generate And Bind The Vertex Buffer glGenBuffers( 1, &m_nVBOVertices ); // Get A Valid Name glBindBuffer( GL_ARRAY_BUFFER, m_nVBOVertices ); // Bind The Buffer // load The Data DWORD len = m_nVertexCount*sizeof(clf_vector3D); // allocate a buffer for the vertices and the normals, filled with seros glBufferData( GL_ARRAY_BUFFER, len*2, 0, GL_STATIC_DRAW ); // (vertices and normals) // copy the vertices in the first half glBufferSubData(GL_ARRAY_BUFFER, 0, len, m_pVertices); // and the normals in the rest of the buffer glBufferSubData(GL_ARRAY_BUFFER, len, len, m_pNormals); glBindBuffer( GL_ARRAY_BUFFER, 0 ); // Release The Buffer if (m_nTextureId > 0) { // Generate And Bind The Texture Coordinate Buffer glGenBuffers( 1, &m_nVBOTexCoords ); // Get A Valid Name glBindBuffer( GL_ARRAY_BUFFER, m_nVBOTexCoords ); // Bind The Buffer // load The Data glBufferData( GL_ARRAY_BUFFER, m_nVertexCount*sizeof(clf_point2D), m_pTexCoords, GL_STATIC_DRAW ); glBindBuffer( GL_ARRAY_BUFFER, 0 ); // Release The Buffer } }
Up until now we managed to create the buffers and store the geometry and any other data. Now we will proceed with rendering this information, or in plain english draw our scene.
When we want to draw using the data stored in the buffer, we follow a path similar to the one above.
First we tell OpenGL that we want to use buffers by calling 'glEnableClientState' function. Then we call 'glBindBuffer' to tell the system which buffer to use. The next step is to tell the system what ti find in that buffer (coordinates, normals, texture etc.) and at which offset. When we are done setting up we call 'glDrawArrays' to draw the scene. Finally we release the buffers by calling the 'glDisableClientState' function.
And here is the code that uses the buffers to draw the terrain based on the heightmap.
void clf_mesh::render() { glEnable(GL_TEXTURE_2D); glEnableClientState( GL_VERTEX_ARRAY ); // Enable Vertex Arrays glEnableClientState(GL_NORMAL_ARRAY); if (m_nTextureId > 0) glEnableClientState( GL_TEXTURE_COORD_ARRAY ); // Enable Texture Coord Arrays // Set Pointers To Our Data glBindBuffer( GL_ARRAY_BUFFER, m_nVBOVertices ); glNormalPointer(GL_FLOAT, sizeof(clf_vector3D), (void*)(m_nVertexCount*sizeof(clf_vector3D))); glVertexPointer( 3, GL_FLOAT, sizeof(clf_vector3D), (char *) NULL ); // Set The Vertex Pointer To The Vertex Buffer if (m_nTextureId > 0) { glBindBuffer( GL_ARRAY_BUFFER, m_nVBOTexCoords ); glTexCoordPointer( 2, GL_FLOAT, sizeof(clf_point2D), (char *) NULL ); // Set The TexCoord Pointer To The TexCoord Buffer glBindTexture( GL_TEXTURE_2D, m_nTextureId ); // Bind The Texture } // render glDrawArrays( GL_TRIANGLES, 0, m_nVertexCount ); // Draw All Of The Triangles At Once // Disable Pointers glDisableClientState( GL_VERTEX_ARRAY ); // Disable Vertex Arrays glDisableClientState(GL_NORMAL_ARRAY); // disable normal arrays if (m_nTextureId > 0) glDisableClientState( GL_TEXTURE_COORD_ARRAY ); // Disable Texture Coord Arrays }
Some other techniques use frame buffer objects (objects similar to the vertex buffer objects). Those are buffers in the video memory used to do background drawing for effects like drawing on textures or motion blur. In future articles we will cover these techniques of special efects.
Finally you can download the complete sample from here