4. OpenGL 4 Vertex Array Objects (VAO)

Introduction

With the fixed-function/immediate-mode vertex path removed from our core-profile setup, we no longer have calls such as glBegin(GL_TRIANGLES), glVertex or glColor. One correction to the old version of this tutorial: glEnable(GL_TRIANGLES) was never a thing, and OpenGL absolutely still has glEnable state for features such as depth testing. What has changed here is how we feed vertex data to the pipeline.

We are going to use VBOs, which I introduced in one of the terrain tutorials, together with VAOs. A Vertex Buffer Object (VBO) stores our vertex data in an OpenGL buffer. A Vertex Array Object (VAO) stores the vertex-input configuration – which attribute locations are enabled, how to interpret the data, and which buffers those attribute pointers refer to. That means we can keep position, colour, normals and other per-vertex data in buffers and have the VAO remember how the shader should read them.

The big idea is that we upload our vertex data to buffer objects instead of resending every vertex one call at a time. The VAO then remembers the setup required to feed those buffers into the shader. This is the intended core-profile rendering path and avoids a lot of per-vertex API chatter, although I am not going to promise an “extreme” performance increase in every application – real performance depends on what the rest of your renderer is doing.

The best part is that this still isn’t too difficult to set up. Instead of a bunch of glVertex calls, we build an array, upload that array into a VBO, and while our VAO is bound we describe how attribute location 0 should read that VBO. Once that is set up, drawing the square becomes only a few calls.

Coding

While I understand you may not have done the terrain tutorial which introduces VBO’s, I’m going to be starting from the basics and I will still be explaining everything as I go. The best part is we don’t have to do too much to get the output for this tutorial, which is a square on the screen.

Open up opengl_3.h and inside of it we are going to create two variables and one method. The method will be take no parameters and will return nothing, but inside of it we will be creating our VAO and VBO to draw a square. In that mindset, I am going to call the method createSquare.

void createSquare(void); // Method for creating our squares Vertex Array Object

Our two variables are going to both be arrays of unsigned int’s and they are both going to be of length 1, so we have one VAO and one VBO. I am calling these vaoID and vboID and am using these as private variables in our class.

unsigned int vaoID[1]; // Our Vertex Array Object

unsigned int vboID[1]; // Our Vertex Buffer Object

Now that’s not so scary, let’s go and jump into opengl_3.cpp and start making use of this. Inside of the setupScene method I am adding the call to createSquare and next we will start creating this method. Keep in mind that I am creating the square and the VAO and VBO before I actually use them, otherwise we might run into issues with empty memory.

void OpenGLContext::setupScene(void) {
    ...

    createSquare(); // Create our square
}

And now to create our createSquare method. I have jumped down to the bottom of the opengl_3.cpp file, however you can place this anywhere in the file if you prefer to order your methods by name or something of the like. Here is what the empty method will initially look like:

/**
createSquare is used to create the Vertex Array Object which will hold our square. We will
be hard coding in the vertices for the square, which will be done in this method.
*/
void OpenGLContext::createSquare(void) {

}

Before we create our VAO and VBO, we need some data which describes the vertices for our shape. I am drawing a square made up of two triangles, so we need 6 vertices, 3 for each triangle. Then, because we have 6 vertices, each vertex is made up of 3 values, the x, y and z coordinates, giving us a total of 18 values we need to describe our square. These are going to be float values and stored in an array called vertices. As I am using a dynamic array, don’t forget to call the delete method afterwards to make sure we free up our memory again.

/**
createSquare is used to create the Vertex Array Object which will hold our square. We will
be hard coding in the vertices for the square, which will be done in this method.
*/

void OpenGLContext::createSquare(void) {
    float* vertices = new float[18];  // Vertices for our square

    delete [] vertices; // Delete our vertices from memory
}

Now it’s time to fill in some values for the vertices. I am going to place all the vertices at 0 on the z axis and I am going to give the square a size of 1 unit. That means I need to go up and to the left half a unit, and down and to the right half a unit as 0.5 + 0.5 = 1, giving us a length of 1 for the sides of the square. So the top left vertex is (-0.5, 0.5, 0.0) and the bottom right vertex is (0.5, -0.5, 0.0). Filling in all 18 values based on this, we get:

/**
createSquare is used to create the Vertex Array Object which will hold our square. We will
be hard coding in the vertices for the square, which will be done in this method.
*/
void OpenGLContext::createSquare(void) {
    float* vertices = new float[18];  // Vertices for our square

    vertices[0] = -0.5; vertices[1] = -0.5; vertices[2] = 0.0; // Bottom left corner
    vertices[3] = -0.5; vertices[4] = 0.5; vertices[5] = 0.0; // Top left corner
    vertices[6] = 0.5; vertices[7] = 0.5; vertices[8] = 0.0; // Top Right corner

    vertices[9] = 0.5; vertices[10] = -0.5; vertices[11] = 0.0; // Bottom right corner
    vertices[12] = -0.5; vertices[13] = -0.5; vertices[14] = 0.0; // Bottom left corner
    vertices[15] = 0.5; vertices[16] = 0.5; vertices[17] = 0.0; // Top Right corner

    delete [] vertices; // Delete our vertices from memory
}

So we’ve now filled in our vertices coordinates, we need to go and create a Vertex Array Object which is done with a call to glGenVertexArrays. We then need a call to glBindVertexArray to make the VAO active. Once the VAO is active, we need to call glGenBuffers to create our Vertex Buffer Object, and then we also need to bind it with glBindBuffer.

So it goes in this order:

  1. Generate Vertex Array Object
  2. Bind Vertex Array Object
  3. Generate Vertex Buffer Object
  4. Bind Vertex Buffer Object

Once we have done step 4, we need to fill our VBO with the vertex data. The final argument to glBufferData is a usage hint. In this case we use GL_STATIC_DRAW because we expect to upload the data and draw from it many times without changing it often. It is a hint to the driver rather than a different “type” of VBO. There are other usage hints for different update patterns as well.

Now, we call glBufferData to upload the float array to the VBO. Then, while both the VAO and VBO are bound, glVertexAttribPointer tells attribute location 0 how to read that buffer and glEnableVertexAttribArray(0) turns that attribute on. Finally we unbind the VAO. Notice that glEnableVertexAttribArray is enabling the attribute – the old comment saying “Disable” was simply wrong.

/**
createSquare is used to create the Vertex Array Object which will hold our square. We will
be hard coding in the vertices for the square, which will be done in this method.
*/
void OpenGLContext::createSquare(void) {

    ...

    vertices[15] = 0.5; vertices[16] = 0.5; vertices[17] = 0.0; // Top Right corner

    glGenVertexArrays(1, &vaoID[0]); // Create our Vertex Array Object
    glBindVertexArray(vaoID[0]); // Bind our Vertex Array Object so we can use it

    glGenBuffers(1, vboID); // Generate our Vertex Buffer Object
    glBindBuffer(GL_ARRAY_BUFFER, vboID[0]); // Bind our Vertex Buffer Object
    glBufferData(GL_ARRAY_BUFFER, 18 * sizeof(GLfloat), vertices, GL_STATIC_DRAW); // Set the size and data of our VBO and set it to STATIC_DRAW

    glVertexAttribPointer((GLuint)0, 3, GL_FLOAT, GL_FALSE, 0, 0); // Set up our vertex attributes pointer

    glEnableVertexAttribArray(0); // Enable vertex attribute location 0
    glBindVertexArray(0); // Unbind our Vertex Array Object
    glBindBuffer(GL_ARRAY_BUFFER, 0); // Unbind the array buffer

    delete [] vertices; // Delete our vertices from memory
}

What we have now is a VBO storing the actual vertex data for our square, with a VAO remembering how attribute location 0 should read that buffer. So far it’s been straight forward, so now we need to render this data to the screen. Go ahead and jump to our renderScene method and after we set our glUniformMatrix4vf variables for our shader from the previous tutorial, we need to add three lines of code to render. First off we bind our Vertex Array Object, then we issue the draw call, and finally we unbind the VAO. Like so:

glBindVertexArray(vaoID[0]); // Bind our Vertex Array Object

glDrawArrays(GL_TRIANGLES, 0, 6); // Draw our square

glBindVertexArray(0); // Unbind our Vertex Array Object

And well done, you can now create a square without immediate mode and with the vertex data stored in a buffer object. I am not going to claim this means “maximum framerates” – if only optimisation was that easy! – but it is the correct foundation for core-profile rendering.

If you want to play with the geometry, a GL_TRIANGLE_STRIP can describe this particular square with 4 vertices instead of repeating two vertices across 6 entries. In bigger meshes you will normally also look at indexed drawing so shared vertices do not have to be duplicated. In the next tutorial we will look at adding colour to our square.

Source & downloads

Questions or corrections? If something doesn’t work or you spot something I’ve got wrong, leave a comment below. These tutorials have been around a long time, so useful corrections are always welcome!

  • December 16, 2010
  • 43