5.3 - A Primer on Buffer Objects

A buffer object is a contiguous block of memory in the GPU that can be accessed very quickly by shader programs executing on the GPU. Buffer objects store the data that shader programs need for rendering. The contents of a buffer object is always an 1-dimensional array. For our purposes we will consider a buffer object to always be homogeneous - that is, every value will have the same data type. (You can mix data types with some tricky JavaScript code, but let’s keep things simple.)

Buffer objects provide the data for attribute variables in vertex shader programs. Remember that WebGL is a restricted subset of OpenGL, and WebGL only allows attribute variables to be of type float, vec2, vec3, vec4, mat2, mat3, and mat4. These are all floating point values. Therefore, all buffer objects that you create will be arrays of floating point values.

JavaScript is not a strongly typed language and it does not distinguish between different types of numbers. Most programming languages have shorts, ints, floats, and doubles. JavaScript’s has only one data type for numeric values: number. JavaScript was modified to deal with binary data values by adding “typed array” objects. For WebGL, all of your buffer objects will contain Float32Array arrays.

// Floating point arrays.
var f32 = new Float32Array(size); // Fractional values with 7 digits of accuracy

There are two ways to put data into a “typed array”:

These two options are demonstrated in the following code:

// Create an array containing 6 floats. Notice the brackets around the array data.
var my_array = new Float32Array( [1.0, 2.0, 3.0, -1.0, -2.0, -3.0] );

// Create an array to hold 4 floating point numbers.
var an_array = new Float32Array(4);
an_array[0] = 12.0;
an_array[1] =  5.0;
an_array[2] = 37.0;
an_array[3] = 18.3;

Creating and Initializing Buffer Objects

Note that buffer objects reside in the GPU, but they are created, managed, and deleted using the WebGL API from JavaScript code. Here is a typical sequence of commands to create a buffer object and fill it with data.

//-----------------------------------------------------------------------
function createAndFillBufferObject(gl, data) {
  var buffer_id;

  // Create a buffer object
  buffer_id = gl.createBuffer();
  if (!buffer_id) {
    out.displayError('Failed to create the buffer object for ' + model_name);
    return null;
  }

  // Make the buffer object the active buffer.
  gl.bindBuffer(gl.ARRAY_BUFFER, buffer_id);

  // Upload the data for this buffer object to the GPU.
  gl.bufferData(gl.ARRAY_BUFFER, data, gl.STATIC_DRAW);

  return buffer_id;
}

Please note the following about this code:

  • Creating an object buffer does nothing more than reserve a new ID for a new buffer.
  • You will typically have many object buffers and only one of them is the “active buffer”. When you issue commands on buffer objects you are always manipulating the “active buffer”. The bindBuffer function simply changes the “active buffer” to the specific buffer using the buffer_id.
  • The bufferData function copies data from your JavaScript program into the GPU’s buffer object. If there was already data in the buffer object then its current contents is deleted and the new data is added.
  • The main error you will receive when copying data to the GPU is OUT_OF_MEMORY. The code above should check for gl errors by calling gl.getError(), but we will worry about catching errors later.

Shaders, Buffers, and the Graphics Pipeline

Shader programs are sometimes difficult to “wrap your brain around” because the relationship between the graphics pipeline and a shader program is not obvious and sometimes never explained. Let’s try to write some pseudocode that describes how the graphics pipeline performs rendering.

Each time your JavaScript program calls gl.drawArrays(mode, start, count), ‘count’ number of vertices are sent through the graphics pipeline. Your vertex shader program is called once for each vertex in an array of vertices that is stored in a buffer object. Inside the graphics pipeline, hidden from you, is a algorithm that is doing this:

for (j = start; j < count; j += 1) {
  call vertex_shader(vertex_buffer[j]);
}

Vertex and fragment shaders need more than just location data if they are going to create complex graphic images. Such information includes color, normal vector, texture coordinates, etc.. Because the graphics pipeline is optimized for speed, the other data has to be organized in arrays in the same order as the vertex data. If each vertex has additional attributes, the above pseudocode becomes something like this:

for (j = start; j < count; j += 1) {
  call vertex_shader(vertex_buffer[j], color_buffer[j], normal_vector_buffer[j], ...);
}

This is an important basic principle of WebGL rendering. All data must be organized on a “per vertex” basis because of the way the pipeline works. This means that in some cases your data must be duplicated in arrays multiple times to “match up” with the vertex data. This can be very inefficient for memory usage, but it makes rendering really fast. To illustrate this principle, suppose you want to render a triangle using a particular color. You must create an array that stores the color of each individual vertex, even though all three vertices have the same color. The code belows shows an array of 9 values that represents 3 vertices. If the color of the vertices is coming from an array in a buffer object, the color has to be stored in three times to “match up” with the vertex data. In the example below, the color ‘red’ is stored three times.

var triangle_vertices = [0,0,0, 1,6,2, 3,4,1];
var triangle_color    = [1,0,0, 1,0,0, 1,0,0]

Glossary

buffer object
a contiguous block of memory in the GPU that stores rendering data for a model. For WebGL, a buffer object is always a 1D array of floats.
vertex object buffer
a buffer object that contains vertices. It is sometimes abbreviated as VOB.
Float32Array
a JavaScript data type that creates an array of floating point values.
Next Section - 5.4 - A Simple Model