University of Wales Trinity Saint David
ACCA6021 - Graphics and Games Programming

Lab 10 - Vertex and Index Buffers

Overview

Building on the GlCall error system from Lab 09 (GlCall Error System), this lab wraps two of the most fundamental OpenGL objects in C++ classes. The Vertex Buffer Object (VBO) holds your vertex data on the GPU - positions, colours, UVs, normals. The Index Buffer Object (IBO) holds integer indices that tell the GPU which vertices to connect into triangles, allowing a four-vertex quad to be drawn with six indices instead of six duplicated vertices. Both follow the same RAII pattern introduced in the previous lab: the constructor calls glGenBuffers and uploads data, and the destructor calls glDeleteBuffers so GPU memory is released automatically when the object goes out of scope.

Learning Objectives

By the end of this lab you will be able to:

Concept

When you called glBufferData directly in main.cpp, the GPU-side memory management was entirely your responsibility. You had to remember the buffer ID in a raw unsigned int, call glDeleteBuffers manually before the program exited, and repeat the same four-line setup everywhere you needed a new buffer. A Vertex Buffer Object is simply a named region of GPU memory. glGenBuffers allocates it and returns an integer handle. glBindBuffer(GL_ARRAY_BUFFER, id) makes it the current target for vertex data. glBufferData uploads bytes from a CPU pointer to that region. The GL_STATIC_DRAW hint tells the driver to place the data in fast, read-only VRAM because you will not change it. GL_DYNAMIC_DRAW tells the driver to keep the buffer in a region that supports frequent rewrites, suited for particle systems or animated geometry.

An Index Buffer Object (also called an Element Buffer Object or EBO) works like a lookup table for vertices. A quad has four unique corners but needs six vertex references to draw two triangles. Without an IBO you would repeat two vertices and waste bandwidth. With an IBO you upload four vertices to the VBO and an index array { 0, 1, 2, 2, 3, 0 } to the IBO. The GPU reads the indices and fetches vertices in that order. glDrawElements replaces glDrawArrays and takes the number of indices as its second argument - which is why the IndexBuffer class stores m_Count. The portability check ASSERT(sizeof(unsigned int) == sizeof(GLuint)) confirms that the C++ type you are using to build the index array is the same width as what OpenGL expects. On all modern desktop platforms they match, but the assertion makes the assumption explicit and will break loudly if it ever fails.

Both classes include Renderer.h in their .cpp files so every OpenGL call is wrapped in GlCall. The header files do not include Renderer.h - only the implementations need it, and keeping includes out of headers avoids cascading recompilation. The constructor takes a raw const void* for vertex data rather than a typed pointer because vertices can hold floats, unsigned bytes, or packed structs - a void pointer accepts all of them without a cast at the call site.

Tasks

Task 1 - Implement the Vertex Buffer Class

Create src/VertexBuffer.h and src/VertexBuffer.cpp. The header declares the class; the implementation uploads data and wraps every OpenGL call in GlCall.

  1. Create src/VertexBuffer.h. The skeleton below gives you the class shape with // TODO: comments. Fill in the member variable and function implementations.
  2. Create src/VertexBuffer.cpp. Include "VertexBuffer.h" and "Renderer.h" (for GlCall).
  3. In the static-data constructor, call glGenBuffers, glBindBuffer(GL_ARRAY_BUFFER, ...), then glBufferData(GL_ARRAY_BUFFER, size, data, GL_STATIC_DRAW). Wrap all three in GlCall.
  4. In the dynamic-data constructor, follow the same sequence but pass nullptr as the data pointer and use GL_DYNAMIC_DRAW.
  5. In the destructor, call glDeleteBuffers(1, &m_RendererID) inside GlCall.
  6. Implement Bind() as glBindBuffer(GL_ARRAY_BUFFER, m_RendererID) and Unbind() as glBindBuffer(GL_ARRAY_BUFFER, 0).
  7. Implement Update() with glBindBuffer followed by glBufferSubData(GL_ARRAY_BUFFER, offset, size, data).
  8. Add both files to your Visual Studio project and build. No visual change is expected yet.
src/VertexBuffer.h
#pragma once

class VertexBuffer
{
private:
    // TODO: declare m_RendererID as an unsigned int to store the OpenGL buffer handle
public:
    // TODO: constructor for static data - takes a pointer to vertex data and its size in bytes
    VertexBuffer(const void* data, unsigned int size);

    // TODO: constructor for dynamic data - takes only the size; data uploaded later via Update()
    VertexBuffer(unsigned int size);

    // TODO: destructor - releases the GPU buffer
    ~VertexBuffer();

    // TODO: bind this buffer as GL_ARRAY_BUFFER
    void Bind() const;

    // TODO: unbind by binding 0 to GL_ARRAY_BUFFER
    void Unbind() const;

    // TODO: overwrite a region of a dynamic buffer using glBufferSubData
    void Update(const void* data, unsigned int size, unsigned int offset = 0) const;
};

Task 2 - Implement the Vertex Buffer .cpp

Implement src/VertexBuffer.cpp following the pattern from the header skeleton in Task 1. Every OpenGL call must be wrapped in GlCall().

  1. The skeleton below shows all the function stubs with // TODO: markers. Replace each TODO with the corresponding OpenGL call.
  2. Confirm the class compiles cleanly. The build output should show no errors.
src/VertexBuffer.cpp
#include "VertexBuffer.h"
#include "Renderer.h"   // provides GlCall

VertexBuffer::VertexBuffer(const void* data, unsigned int size)
{
    // TODO: glGenBuffers(1, &m_RendererID)
    // TODO: glBindBuffer(GL_ARRAY_BUFFER, m_RendererID)
    // TODO: glBufferData(GL_ARRAY_BUFFER, size, data, GL_STATIC_DRAW)
}

VertexBuffer::VertexBuffer(unsigned int size)
{
    // TODO: glGenBuffers(1, &m_RendererID)
    // TODO: glBindBuffer(GL_ARRAY_BUFFER, m_RendererID)
    // TODO: glBufferData(GL_ARRAY_BUFFER, size, nullptr, GL_DYNAMIC_DRAW)
}

VertexBuffer::~VertexBuffer()
{
    // TODO: glDeleteBuffers(1, &m_RendererID)
}

void VertexBuffer::Bind() const
{
    // TODO: glBindBuffer(GL_ARRAY_BUFFER, m_RendererID)
}

void VertexBuffer::Unbind() const
{
    // TODO: glBindBuffer(GL_ARRAY_BUFFER, 0)
}

void VertexBuffer::Update(const void* data, unsigned int size, unsigned int offset) const
{
    // TODO: glBindBuffer then glBufferSubData
}

Task 3 - Implement the Index Buffer Class

Create src/IndexBuffer.h and src/IndexBuffer.cpp. The IBO follows the same RAII pattern as the VBO but targets GL_ELEMENT_ARRAY_BUFFER and stores the index count.

  1. Create src/IndexBuffer.h using the skeleton below. Note the two private members: m_RendererID for the GL handle and m_Count for the number of indices.
  2. Create src/IndexBuffer.cpp. Include "IndexBuffer.h" and "Renderer.h".
  3. In the constructor, initialise m_Count in the member-initialiser list, then add the portability check: ASSERT(sizeof(unsigned int) == sizeof(GLuint)).
  4. Call glGenBuffers, glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ...), and glBufferData(GL_ELEMENT_ARRAY_BUFFER, count * sizeof(unsigned int), data, GL_STATIC_DRAW).
  5. Implement Bind() and Unbind() targeting GL_ELEMENT_ARRAY_BUFFER instead of GL_ARRAY_BUFFER.
  6. Add both files to your project and rebuild. Confirm no errors.
src/IndexBuffer.h
#pragma once

class IndexBuffer
{
private:
    // TODO: declare m_RendererID (unsigned int)
    // TODO: declare m_Count (unsigned int) - stores the number of indices
public:
    // TODO: constructor - takes a pointer to unsigned int array and the number of indices
    IndexBuffer(const unsigned int* data, unsigned int count);

    // TODO: destructor - release the GPU buffer
    ~IndexBuffer();

    void Bind() const;
    void Unbind() const;

    // TODO: return m_Count so Renderer::Draw can pass it to glDrawElements
    inline unsigned int GetCount() const { return m_Count; }
};

Task 4 - Implement the Index Buffer .cpp

Implement src/IndexBuffer.cpp from the skeleton. The key differences from VertexBuffer.cpp are the buffer target (GL_ELEMENT_ARRAY_BUFFER), the size calculation (count * sizeof(unsigned int)), and the portability assert.

  1. Fill in every // TODO: in the skeleton below.
  2. Build and confirm no errors.
  3. In your main render loop, replace the raw glGenBuffers calls for your VBO and IBO with instances of these new classes. Verify the scene still renders correctly.
src/IndexBuffer.cpp
#include "IndexBuffer.h"
#include "Renderer.h"

IndexBuffer::IndexBuffer(const unsigned int* data, unsigned int count)
    : m_Count(count)
{
    // TODO: ASSERT that sizeof(unsigned int) == sizeof(GLuint) for portability
    // TODO: glGenBuffers(1, &m_RendererID)
    // TODO: glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, m_RendererID)
    // TODO: glBufferData(GL_ELEMENT_ARRAY_BUFFER, count * sizeof(unsigned int), data, GL_STATIC_DRAW)
}

IndexBuffer::~IndexBuffer()
{
    // TODO: glDeleteBuffers(1, &m_RendererID)
}

void IndexBuffer::Bind() const
{
    // TODO: glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, m_RendererID)
}

void IndexBuffer::Unbind() const
{
    // TODO: glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0)
}

Solution

Try the tasks above before revealing the solution.