OpenGL – VBO, Shader, VAO

Tutorial with example

As posted in OpenGL-Then and Now, there has been a paradigm shift in rendering. This is a comprehensive look at the transition from “Then” (Immediate Mode, Fixed Functon Pipeline) to “Now” (Retained Mode, Programmable Pipeline). The simple example will incrementally transition from the legacy approach to the current paradigm.

Introduction
Example
Render
    Immediate using glBegin and glEnd
    Vertex Array to specify vertex data in client memory
    Vertex Buffer Object (VBO) to store vertex data in GPU memory
    Shader to define the program to execute on the GPU
    Shader with Vertex Buffer Object (VBO) to invoke the Programmable Pipeline
    Shader with Verrtex Array Object (VAO) an advanced concept of the Programmable Pipeline
Conclusion

Introduction

A short overview of the underlying concept will help understand the basis of the transition to modern OpenGL. Graphics Processing Unit (GPU) is hardware dedicated (and optimized) for graphics rendering. Modern OpenGL aims for better utilization of the GPU. So the transition is in these broad areas.

Operation Legacy OpenGL Modern OpenGL
Specifying vertex data with attributes like coordinates, colors, normals, textures, etc. Main/Client memory GPU memory (Vertex Buffer Objects)
Processing of the vertex data with hardware/software along the rendering pipeline Fixed functions User defined programs (Shaders) in the GPU


Example

The example draws lines and triangles. Different colors are used for each of the entities so that problems can be identified easily.

  • The same vertex data, coordinates and color attribute, is used for rendering in various methods.
  • The Cartesian coordinate system axes are drawn in RGB at the origin 0,0,0 (a common notation to represent X-axis in red, Y-axis in Green and Z-axis in Blue).
  • A pyramid with its base in grey and sides in shades of yellow/orange.
  • Key stroke commands are provided to rotate, pan and zoon. The list of commands are shown in the output windows.

Further, the example in order to focus on the concept keeps the necessary API calls in a sequence. It is NOT optimized for performance. The example code can be accessed on GitHub at https://github.com/cognitivewaves/OpenGL-Render.

Immediate

The simplest way to “experience” OpenGL is to draw in immediate mode using per vertex attribute specification between glBegin and glEnd.

static void drawImmediate()
{
    // Draw x-axis in red
    glColor3d(ac[0], ac[1], ac[2]);
    glBegin(GL_LINES);
        glVertex3f(av[0], av[1], av[2]);
        glVertex3f(av[3], av[4], av[5]);
    glEnd();

    // Draw y-axis in green
    glColor3d(ac[3], ac[4], ac[5]);
    glBegin(GL_LINES);
        glVertex3f(av[0], av[1], av[2]);
        glVertex3f(av[6], av[7], av[8]);
    glEnd();

    // Draw z-axis in blue
    glColor3d(ac[6], ac[7], ac[8]);
    glBegin(GL_LINES);
        glVertex3f(av[0], av[1], av[2]);
        glVertex3f(av[9], av[10], av[11]);
    glEnd();

    // Draw pyramid
    glBegin(GL_TRIANGLES);
        glColor3d(pc[0], pc[1], pc[2]);

        glVertex3f(pv[0], pv[1], pv[2]);       // 0
        glVertex3f(pv[3], pv[4], pv[5]);       // 1
        glVertex3f(pv[6], pv[7], pv[8]);       // 2

        glVertex3f(pv[6], pv[7],  pv[8]);      // 2
        glVertex3f(pv[9], pv[10], pv[11]);     // 3
        glVertex3f(pv[0], pv[1],  pv[2]);      // 0

        glColor3f(pc[3], pc[4], pc[5]);

        glVertex3f(pv[0],  pv[1],  pv[2]);     // 0
        glVertex3f(pv[9],  pv[10], pv[11]);    // 3
        glVertex3f(pv[12], pv[13], pv[14]);    // 4

        glColor3f(pc[6], pc[7], pc[8]);

        glVertex3f(pv[9],  pv[10], pv[11]);    // 3
        glVertex3f(pv[6],  pv[7],  pv[8]);     // 2
        glVertex3f(pv[12], pv[13], pv[14]);    // 4

        glColor3f(pc[9], pc[10], pc[11]);

        glVertex3f(pv[6],  pv[7],  pv[8]);     // 2
        glVertex3f(pv[3],  pv[4],  pv[5]);     // 1
        glVertex3f(pv[12], pv[13], pv[14]);    // 4

        glColor3f(pc[12],  pc[13], pc[14]);

        glVertex3f(pv[3],  pv[4],  pv[5]);     // 1
        glVertex3f(pv[0],  pv[1],  pv[2]);     // 0
        glVertex3f(pv[12], pv[13], pv[14]);    // 4
    glEnd();
}

Vertex Array

A slightly better way to specify vertex data in immediate mode is using Vertex Arrays (not to be confused with Vertex Array Objects discussed later). The vertex data is stored in array format in client memory. The respective attribute association is specified with glVertexPointer and glColorPointer. Data is transferred to the GPU in bulk for every frame.

static void drawVertexArray()
{
    glEnableClientState(GL_VERTEX_ARRAY);
    glEnableClientState(GL_COLOR_ARRAY);

    // Set axes data
    glVertexPointer(nVertexComponents, GL_FLOAT, 0, ave);
    glColorPointer(nColorComponents, GL_FLOAT, 0, ace);

    // Draw axes
    glDrawArrays(GL_LINES, 0, nLines*nVerticesPerLine);

    // Set pyramid data
    glVertexPointer(nVertexComponents, GL_FLOAT, 0, pve);
    glColorPointer(nColorComponents, GL_FLOAT, 0, pce);

    // Draw pyramid
    glDrawArrays(GL_TRIANGLES, 0, nFaces*nVerticesPerFace);

    glDisableClientState(GL_VERTEX_ARRAY);
    glDisableClientState(GL_COLOR_ARRAY);
}

Extensions

APIs for VBO, Shader, VAO etc. are dependent on OpenGL extensions which have to be loaded dynamically. Though it is a nifty concept, it is not like linking to a dynamic library. Pointers to the required functions have to be loaded explicitly. There are OpenGL Loading Libraries which abstracts (and hides) the loading mechanism. But in the example, it is done manually to get a feel for it. See glext.h and glextload.h in the source code.

Vertex Buffer Object (VBO)

An even better way is to store the vertex data directly on the GPU. These GPU memory pools are called Vertex Buffer Objects. This is achieved by setting a current buffer with glBindBuffer and copying the contents from client memory to the VBO with glBufferData. The GPU can then access the data directly and will save the cost of transferring from client memory to GPU memory every frame. The attribute association is again specified with glVertexPointer and glColorPointer.

⚠ Note the “reuse” of the APIs glVertexPointer and glColorPointer to specify the vertex attributes now copied in the VBOs. I say “reuse” because the 4th parameter pointer has a different meaning depending on the context. So in this case, the same APIs implicitly sets the vertex and color because a VBO is bound prior to the call.

If a non-zero named buffer object is bound to the GL_ARRAY_BUFFER target (see glBindBuffer) while a vertex array is specified, pointer is treated as a byte offset into the buffer object’s data store.

static void drawVertexBufferObject()
{
    LoadGLExtensions();

    vboIds = new GLuint[3];
    glGenBuffers(3, vboIds);

    glEnableClientState(GL_VERTEX_ARRAY);
    glEnableClientState(GL_COLOR_ARRAY);

    // Set axes data
    glBindBuffer(GL_ARRAY_BUFFER, vboIds[0]);  // coordinates
    glBufferData(GL_ARRAY_BUFFER, sizeof(ave), ave, GL_STATIC_DRAW);
    glVertexPointer(nCoordsComponents, GL_FLOAT, 0, 0);

    glBindBuffer(GL_ARRAY_BUFFER, vboIds[1]);  // color
    glBufferData(GL_ARRAY_BUFFER, sizeof(ace), ace, GL_STATIC_DRAW);
    glColorPointer(nColorComponents, GL_FLOAT, 0, 0);

    // Draw axes
    glDrawArrays(GL_LINES, 0, nLines*nVerticesPerLine);

    // Set pyramid data
    glBindBuffer(GL_ARRAY_BUFFER, vboIds[2]);  // coordinates
    glBufferData(GL_ARRAY_BUFFER, sizeof(pve), pve, GL_STATIC_DRAW);
    glVertexPointer(nCoordsComponents, GL_FLOAT, 0, 0);

    glBindBuffer(GL_ARRAY_BUFFER, vboIds[3]);  // color
    glBufferData(GL_ARRAY_BUFFER, sizeof(pce), pce, GL_STATIC_DRAW);
    glColorPointer(nColorComponents, GL_FLOAT, 0, 0);

    // Draw pyramid
    glDrawArrays(GL_TRIANGLES, 0, nFaces*nVerticesPerFace);

    glDisableClientState(GL_VERTEX_ARRAY);
    glDisableClientState(GL_COLOR_ARRAY);

    // Disable the VBO
    glBindBuffer(GL_ARRAY_BUFFER, 0);
}

Shader

Shaders are user defined programs/code written in GLSL (OpenGL Shading Language) that are executed on the GPU in the rendering pipeline. Very basic shaders are used as the focus is on the overall setup rather than GLSL.

  • Vertex Shader – applies the projection and model-view matrix to each of the vertices
  • Fragment Shader – applies the color attribute specified at the vertices
const char* vertex_shader =
    "attribute vec3 aCoords;"
    "attribute vec3 aColor;"
    "uniform mat4 umvMat;"
    "uniform mat4 upMat;"
    "varying vec3 vColor;"
    "void main () {"
        "gl_Position = upMat * umvMat * vec4(aCoords, 1.0);"
        "vColor = aColor;"
    "}";

const char* fragment_shader =
    "varying vec3 vColor;"
    "void main () {"
        "gl_FragColor = vec4 (vColor, 1.0);"
    "}";

These programs replace the fixed functions provided in legacy OpenGL. Since these programs run on the GPU, the source written in OpenGL Shading Language (GLSL) has to be first first loaded, then complied and linked on the GPU using appropriate API.

GLuint VERTEX_ATTR_COORDS = 1;
GLuint VERTEX_ATTR_COLOR = 2;

static void initShaders()
{
    GLuint vs = glCreateShader (GL_VERTEX_SHADER);
    glShaderSource (vs, 1, &vertex_shader, NULL);
    glCompileShader (vs);

    GLuint fs = glCreateShader (GL_FRAGMENT_SHADER);
    glShaderSource (fs, 1, &fragment_shader, NULL);
    glCompileShader (fs);

    program = glCreateProgram();
    glAttachShader (program, fs);
    glAttachShader (program, vs);

    glBindAttribLocation(program, VERTEX_ATTR_COORDS, "aCoords");
    glBindAttribLocation(program, VERTEX_ATTR_COLOR, "aColor");

    glLinkProgram (program);

    glUseProgram (program);
}

Shader with Vertex Buffer Object (VBO)

The shaders are loaded on the GPU, and are responsible for portions of the rendering pipeline. So it is necessary for the shader code to access the vertex data to process it. Since the shader is on the GPU, the vertex data too has to be on the GPU in VBOs. The vertex data is copied to the GPU Vertex Buffer Objects as before with glBindBuffer and glBufferData.

Then comes the most confusing part – associating the attributes (coordinates, color, etc.) in VBOs to be accessed by the shader variables. The magic happens in this one function glVertexAttribPointer which creates generic vertex attributes. It establishes a relation between the bound (current) VBO to the shader attribute variable associated (using glBindAttribLocation) with its index. See OpenGL Terminology Demystified for a detailed explanation.

static void drawShaderWithVertexBufferObject()
{
    LoadGLExtensions();

    initShaders();

    // Get the variables from the shader to which data will be passed
    GLint mvloc = glGetUniformLocation(program, "umvMat");
    GLint ploc = glGetUniformLocation(program, "upMat");
    GLint vertexAttribCoords = glGetAttribLocation(program, "aCoords");
    GLint vertexAttribColor = glGetAttribLocation(program, "aColor");
    
    // Pass the model-view matrix to the shader
    GLfloat mvMat[16]; 
    glGetFloatv(GL_MODELVIEW_MATRIX, mvMat); 
    glUniformMatrix4fv(mvloc, 1, false, mvMat);

    // Pass the projection matrix to the shader
    GLfloat pMat[16]; 
    glGetFloatv(GL_PROJECTION_MATRIX, pMat); 
    glUniformMatrix4fv(ploc, 1, false, pMat);

    vboIds = new GLuint[4];
    glGenBuffers(4, vboIds);

    // Set axes data
    glBindBuffer(GL_ARRAY_BUFFER, vboIds[0]);  // coordinates
    glBufferData(GL_ARRAY_BUFFER, sizeof(ave), ave, GL_STATIC_DRAW);
    glVertexAttribPointer(vertexAttribCoords, nCoordsComponents, GL_FLOAT, GL_FALSE, 0, 0);
    glEnableVertexAttribArray(vertexAttribCoords);

    glBindBuffer(GL_ARRAY_BUFFER, vboIds[1]);  // color
    glBufferData(GL_ARRAY_BUFFER, sizeof(ace), ace, GL_STATIC_DRAW);
    glVertexAttribPointer(vertexAttribColor, nColorComponents, GL_FLOAT, GL_FALSE, 0, 0);
    glEnableVertexAttribArray(vertexAttribColor);

    // Draw axes
    glDrawArrays(GL_LINES, 0, nLines*nVerticesPerLine);

    // Set pyramid data
    glBindBuffer(GL_ARRAY_BUFFER, vboIds[2]);  // coordinates
    glBufferData(GL_ARRAY_BUFFER, sizeof(pve), pve, GL_STATIC_DRAW);
    glVertexAttribPointer(vertexAttribCoords, nCoordsComponents, GL_FLOAT, GL_FALSE, 0, 0);
    glEnableVertexAttribArray(vertexAttribCoords);

    glBindBuffer(GL_ARRAY_BUFFER, vboIds[3]);  // color
    glBufferData(GL_ARRAY_BUFFER, sizeof(pce), pce, GL_STATIC_DRAW);
    glVertexAttribPointer(vertexAttribColor, nColorComponents, GL_FLOAT, GL_FALSE, 0, 0);
    glEnableVertexAttribArray(vertexAttribColor);

    // Draw pyramid
    glDrawArrays(GL_TRIANGLES, 0, nFaces*nVerticesPerFace);

    // Disable the VBO
    glBindBuffer(GL_ARRAY_BUFFER, 0);
}

Shader with Vertex Array Object (VAO)

VAOs provide a way to “pre-define” vertex data and its attributes. It is like creating “objects” which hold the required states to render. In this example, two VAOs are created, one for the axes and the other for the pyramid. The code defining a VAO is the same as rendering using VBOs. Instead of binding the buffer, copying data to the buffer and creating a generic vertex attribute during render, these steps are “defined” in a VAO.

static void defineVAO()
{
    vaoIds = new GLuint[2];
    glGenVertexArrays(2, vaoIds);

    vboIds = new GLuint[4];
    glGenBuffers(4, vboIds);

    GLint vertexAttribCoords = glGetAttribLocation(program, "aCoords");
    GLint vertexAttribColor = glGetAttribLocation(program, "aColor");

    // Bind VAO (set current) to define axes data
    glBindVertexArray(vaoIds[0]);

    glBindBuffer(GL_ARRAY_BUFFER, vboIds[0]);  // coordinates
    glBufferData(GL_ARRAY_BUFFER, sizeof(ave), ave, GL_STATIC_DRAW);
    glVertexAttribPointer(vertexAttribCoords, nCoordsComponents, GL_FLOAT, GL_FALSE, 0, 0);
    glEnableVertexAttribArray(vertexAttribCoords);

    glBindBuffer(GL_ARRAY_BUFFER, vboIds[1]);  // color
    glBufferData(GL_ARRAY_BUFFER, sizeof(ace), ace, GL_STATIC_DRAW);
    glVertexAttribPointer(vertexAttribColor, nColorComponents, GL_FLOAT, GL_FALSE, 0, 0);
    glEnableVertexAttribArray(vertexAttribColor);

    // Bind VAO (set current) to define pyramid data
    glBindVertexArray(vaoIds[1]);
    
    glBindBuffer(GL_ARRAY_BUFFER, vboIds[2]);  // coordinates
    glBufferData(GL_ARRAY_BUFFER, sizeof(pve), pve, GL_STATIC_DRAW);
    glVertexAttribPointer(vertexAttribCoords, nCoordsComponents, GL_FLOAT, GL_FALSE, 0, 0);
    glEnableVertexAttribArray(vertexAttribCoords);

    glBindBuffer(GL_ARRAY_BUFFER, vboIds[3]);  // color
    glBufferData(GL_ARRAY_BUFFER, sizeof(pce), pce, GL_STATIC_DRAW);
    glVertexAttribPointer(vertexAttribColor, nColorComponents, GL_FLOAT, GL_FALSE, 0, 0);
    glEnableVertexAttribArray(vertexAttribColor);

    // Disable VAO
    glBindVertexArray(0);
}

During render, the VAO(s) are simply bound (made current) for the GPU to access the information about the “pre-defined” VBOs and vertex attributes.

static void drawShaderWithVertexArrayObject()
{
    LoadGLExtensions();

    initShaders();
    defineVAO();

    // Get the variables from the shader to which data will be passed
    GLint mvloc = glGetUniformLocation(program, "umvMat");
    GLint ploc = glGetUniformLocation(program, "upMat");

    // Pass the model-view matrix to the shader
    GLfloat mvMat[16]; 
    glGetFloatv(GL_MODELVIEW_MATRIX, mvMat); 
    glUniformMatrix4fv(mvloc, 1, false, mvMat);

    // Pass the projection matrix to the shader
    GLfloat pMat[16]; 
    glGetFloatv(GL_PROJECTION_MATRIX, pMat); 
    glUniformMatrix4fv(ploc, 1, false, pMat);

    // Enable VAO to set axes data
    glBindVertexArray(vaoIds[0]);
    
    // Draw axes
    glDrawArrays(GL_LINES, 0, nLines*nVerticesPerLine);

    // Enable VAO to set pyramid data
    glBindVertexArray(vaoIds[1]);

    // Draw pyramid
    glDrawArrays(GL_TRIANGLES, 0, nFaces*nVerticesPerFace);

    // Disable VAO
    glBindVertexArray(0);
}

Conclusion

Modern OpenGL is powerful but can be intimidating until you understand the nitty gritty details. Hope that the document is useful. Feel free to comment if you need any clarifications or further information.