How to use Vertex Array Objects with OpenGL

If you are confused about Vertex Array Objects or have forgotten how to use them, you are in the right place. In this post I will explain all the code needed to load and render a simple mesh using Vertex Array Object (or VAO for short).

First you have to be aware that VAOs is an abstraction, meaning that it encapsulate a functionality to ease development with the OpenGL API. In this case it contains a series of OpenGL buffers configured in such a way to provide the GPU with all the necessary vertex data to render a 3D model. For completeness I have to mention that this feature is available since OpenGL version 3.0 and it is much faster than previous alternatives like glBegin() and glEnd().

To create a VAO, you have to use the function glGenVertexArrays(GLsize n,GLuint *Arrays), where "n" is the amount of VAOs to create and "Arrays" is an array of integers to store the objects. Like most objects in Opengl, VAOs are represented as integers and have to be "bound" first to be modified or used.

In the next snippet of code I create and bind one VAO:

int VAO = 0;
glGenVertexArrays(1,&VAO);
glBindVertexArray(VAO);//to bind it

Now that our VAO is created and bound we can start start populating it with OpenGL stuff.

First we are going to set the buffer that contains all vertex attributes.

int buffer = 0;
glGenBuffers(1,&buffer);
glBindBuffer(GL_ARRAY_BUFFER,VAO);

Here I create and bind our buffer that will contain the vertex data. Note that the VAO is still bound, meaning that every buffer I bind will be contained on the previously bound VAO.

Now that our buffer is created, we now can fill it with vertex data, to do this we need to:

  • Enable the vertex attribute arrays.
  • Specify the "pointer" to the data, that is, the arrangement of the data so Opengl can find each specific vertex data attribute.
  • Provide the buffer with all the vertex data.

To enable the attribute arrays we first need to know which different types of data we are going to use, for this example we are going to use position, normal, and uv coordinates. So if we set the location in our vertex array like as 0 for position, 1 for normal and 2 for uv we get:

glEnableVertexAttribArray(0);
glEnableVertexAttribArray(1);
glEnableVertexAttribArray(2);

Then we are going to put all vertex data in one big buffer and interleave it, by interleave I meaning that each type of data are not going to be next to other of the same type, for example, instead of having a buffer with {position, position, position, normal, normal, normal ,uv, uv, uv}, we are going to have a buffer with {position, normal, uv, position, normal, uv, position, normal, uv}. This way is theoreticaly faster for the gpu to read the data from its memory. To do this we use the old and useful c struct like this:

struct VertexData{
     float position[3];
     float normal[3];
     float uv[2];
};

So if we need to draw a triangle we just create an array of the struct "VertexData":

VertexData vertexData[3];

vertexData[0].position = {-0.5f,-0.5f,0.0f};
vertexData[1].position = {0.5f,-0.5f,0.0f};
vertexData[2].position = {0.0f,0.5,0.0f};

vertexData[0].normal= {0.0f,0.0f,1.0f};
vertexData[1].normal= {0.0f,0.0f,1.0f};
vertexData[2].normal= {0.0f,0.0f,1.0f};

vertexData[0].uv= {0.0f,0.0f};
vertexData[1].uv= {1.0f,0.0f};
vertexData[2].uv= {0.5f,1.0f};

Now that we have our buffer of data we can provide it to OpenGL like this:

glBufferData(GL_ARRAY_BUFFER, sizeof(vertexData), vertexData, GL_STATIC_DRAW);

Where:

  • The first argument is the type of buffer
  • The second argument is the amount of data in bytes of the whole buffer (in this case sizeof() do this for us for regular c arrays)
  • The third argument is the pointer to the first byte of data, in this case can be "vertexData" or "&vertexData[0]"
  • and the fourth argument is a hint on the usage of the buffer for OpenGL to optimize accordingly

Finally we need to tell OpenGL the arrangement of our buffer like this:

glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, sizeof(VertexData), (GLvoid*)offsetof(VertexData, position));
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, sizeof(VertexData), (GLvoid*)offsetof(VertexData, normal));
glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, sizeof(VertexData), (GLvoid*)offsetof(VertexData, uv));

Where:

  • The first argument is the location corresponding to each attribute
  • The second is how many components for each attribute, in this case for position and normal are three because we are working in 3d and uv is two because we are using 2d textures(like most of the time)
  • The third parameter is the type of data of the attribute, in this case we are using floats
  • The fourth if true tells OpenGL to normalize the data when using integers, in this case we are using floats son we put "GL_FALSE"
  • The fifth is the stride of the attribute, meaning the amount of bytes between each set of data of the same type, look closely that it says VertexData instead of vertexData, meaning we are providing the size in bytes of an instance of the struct VertexData
  • And the sixth is the offset of the attribute inside each instance of the struct

At this point our VAO is already set so we can unbind it:

glBindVertexArray(0);

All code written before this can be done only once for each VAO, and for each frame we do this:

glUseProgram(glslProgram);

//for each uniform
glUniformX(uniform0);
glUniformX(uniform1);
...
glUniformX(uniformN);

//for each texture
glActiveTexture(GL_TEXTURE0 + 0);
glBindTexture(GL_TEXTURE_2D, textureBinding0);
glActiveTexture(GL_TEXTURE0 + 1);
glBindTexture(GL_TEXTURE_2D, textureBinding1);
...
glActiveTexture(GL_TEXTURE0 + n);
glBindTexture(GL_TEXTURE_2D, textureBindingN);

glBindVertexArray(VAO);
glDrawArrays(GL_TRIANGLES,0,3);

And that is it. I hope that with this explanation you get a more thorough understanding of the mechanics of VAO and a goto place to refresh your knowledge every time you have to implement one.

Good luck and happy coding!

Comments