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:
- Explain what a GPU buffer object is and why vertex data must be uploaded before the render loop
- Distinguish between GL_STATIC_DRAW (data set once) and GL_DYNAMIC_DRAW (data updated per frame)
- Implement the VertexBuffer class following the RAII pattern: generate in the constructor, delete in the destructor
- Explain why IndexBuffer stores m_Count and how glDrawElements uses it instead of glDrawArrays
- Use ASSERT to confirm that unsigned int and GLuint have the same size before uploading index data
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.
- Create
src/VertexBuffer.h. The skeleton below gives you the class shape with// TODO:comments. Fill in the member variable and function implementations. - Create
src/VertexBuffer.cpp. Include"VertexBuffer.h"and"Renderer.h"(forGlCall). - In the static-data constructor, call
glGenBuffers,glBindBuffer(GL_ARRAY_BUFFER, ...), thenglBufferData(GL_ARRAY_BUFFER, size, data, GL_STATIC_DRAW). Wrap all three inGlCall. - In the dynamic-data constructor, follow the same sequence but pass
nullptras the data pointer and useGL_DYNAMIC_DRAW. - In the destructor, call
glDeleteBuffers(1, &m_RendererID)insideGlCall. - Implement
Bind()asglBindBuffer(GL_ARRAY_BUFFER, m_RendererID)andUnbind()asglBindBuffer(GL_ARRAY_BUFFER, 0). - Implement
Update()withglBindBufferfollowed byglBufferSubData(GL_ARRAY_BUFFER, offset, size, data). - Add both files to your Visual Studio project and build. No visual change is expected yet.
#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().
- The skeleton below shows all the function stubs with
// TODO:markers. Replace each TODO with the corresponding OpenGL call. - Confirm the class compiles cleanly. The build output should show no errors.
#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.
- Create
src/IndexBuffer.husing the skeleton below. Note the two private members:m_RendererIDfor the GL handle andm_Countfor the number of indices. - Create
src/IndexBuffer.cpp. Include"IndexBuffer.h"and"Renderer.h". - In the constructor, initialise
m_Countin the member-initialiser list, then add the portability check:ASSERT(sizeof(unsigned int) == sizeof(GLuint)). - Call
glGenBuffers,glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ...), andglBufferData(GL_ELEMENT_ARRAY_BUFFER, count * sizeof(unsigned int), data, GL_STATIC_DRAW). - Implement
Bind()andUnbind()targetingGL_ELEMENT_ARRAY_BUFFERinstead ofGL_ARRAY_BUFFER. - Add both files to your project and rebuild. Confirm no errors.
#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.
- Fill in every
// TODO:in the skeleton below. - Build and confirm no errors.
- In your main render loop, replace the raw
glGenBufferscalls for your VBO and IBO with instances of these new classes. Verify the scene still renders correctly.
#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.