Overview
Over the last five labs you built six abstraction classes. This final lab in the series has two goals. First, you complete the Renderer class by adding the Draw() methods that were deferred in Lab 09 (GlCall Error System) because they reference VertexArray, IndexBuffer, and Shader - classes that did not exist at that point. Second, you refactor the Lab 08 monolithic main.cpp by replacing every raw OpenGL block with instantiations of those six classes. When the refactor is complete, main.cpp shrinks from over 100 lines of GL boilerplate to a concise, readable scene setup, and RAII eliminates the entire manual cleanup section at the end.
Learning Objectives
By the end of this lab you will be able to:
- Explain why Renderer::Draw was deferred to this lab and which header includes it now requires
- Implement Renderer::Draw by binding the shader, VAO, and IBO in the correct order before calling glDrawElements
- Replace every raw glGenBuffers, glBufferData, and glVertexAttribPointer call in main.cpp with the corresponding class instances
- Explain why the manual glDeleteBuffers, glDeleteTextures, and glDeleteProgram calls in the Lab 08 cleanup section can be removed entirely
- Identify the one line that links the sampler2D uniform to a texture unit and explain why it only needs to be called once outside the render loop
Concept
The Renderer class was introduced in Lab 09 (GlCall Error System) with only its Clear() methods. Adding Draw() was deferred because its signature references VertexArray, IndexBuffer, and Shader - and Renderer.h cannot include headers that do not exist yet. Now that all three classes are built, Renderer.h can include them at the top and declare the Draw overloads. The implementation in Renderer.cpp binds the three objects in the required order - shader first, then VAO, then IBO - and calls glDrawElements with the index count from ib.GetCount(). Centralising this sequence in one method means that every test scene in the project can call renderer.Draw(vao, ibo, shader) without repeating the bind sequence.
The refactoring task demonstrates the value of RAII concretely. The Lab 08 main.cpp had a multi-line cleanup block at the end: glDeleteTextures, glDeleteBuffers (twice), glDeleteVertexArrays, glDeleteProgram. In the refactored version, all of that code disappears. When the local variables vao, vbo, ibo, shader, and tex go out of scope at the end of main, their destructors call the corresponding glDelete* functions automatically, in reverse declaration order. There is nothing to forget, nothing to leak, and nothing to call manually.
One subtle point in the refactored main.cpp is that shader.setUniform1i("u_Texture", 0) is called once before the render loop, not inside it. This call sets the GLSL sampler2D uniform to texture unit 0. Because the sampler unit does not change between frames, repeating the call every frame is redundant. The texture itself changes frame to frame only if you are animating it - here it does not, so tex.Bind(0) inside the loop is all that is needed. The rule is: set uniform values only as often as they actually change.
Tasks
Task 1 - Complete the Renderer Header
Open src/Renderer.h from Lab 09 (GlCall Error System) and add the includes and Draw method declarations that were deferred until now.
- Open
src/Renderer.h. At the top, after#include <GL/glew.h>, add includes for the three classes thatDrawdepends on:#include "VertexArray.h" #include "IndexBuffer.h" #include "Shader.h" - Inside the
Rendererclass, add the twoDrawdeclarations:
The second overload is for when the shader is already bound externally (for example, when the same shader is shared across multiple draw calls).void Draw(const VertexArray& va, const IndexBuffer& ib, const Shader& shader) const; void Draw(const VertexArray& va, const IndexBuffer& ib) const; - Remove the
#include <iostream>and#include <string>that were added in Lab 09 if they are not needed in the header - they belong in the.cppfile. The reference block below shows the complete final header.
#pragma once
#include <GL/glew.h>
#include "VertexArray.h"
#include "IndexBuffer.h"
#include "Shader.h"
// Halt execution if condition is false
#define ASSERT(x) if (!(x)) __debugbreak();
// Wrap any OpenGL call: clear the error queue before, check it after
#define GlCall(x) glClearError();\
x;\
ASSERT(glLogCall(#x, __FILE__, __LINE__))
void glClearError();
bool glLogCall(const char* function, const char* file, int line);
class Renderer {
public:
// Bind shader, VAO, and IBO then issue a single glDrawElements call
void Draw(const VertexArray& va, const IndexBuffer& ib, const Shader& shader) const;
// Draw without binding a shader (for when the shader is bound externally)
void Draw(const VertexArray& va, const IndexBuffer& ib) const;
void Clear() const;
void Clear(std::string colour) const;
void ClearColour_Black() const;
void ClearColour_White() const;
};
Task 2 - Implement Renderer::Draw
Open src/Renderer.cpp and add the two Draw implementations.
- Add the first overload. Bind the shader, then the VAO, then the IBO. Call
glDrawElementswithGL_TRIANGLES,ib.GetCount(),GL_UNSIGNED_INT, andnullptr(the IBO is already bound, so no offset pointer is needed):void Renderer::Draw(const VertexArray& va, const IndexBuffer& ib, const Shader& shader) const { shader.Bind(); va.Bind(); ib.Bind(); GlCall(glDrawElements(GL_TRIANGLES, ib.GetCount(), GL_UNSIGNED_INT, nullptr)); } - Add the second overload (no shader parameter - bind VAO and IBO only):
void Renderer::Draw(const VertexArray& va, const IndexBuffer& ib) const { va.Bind(); ib.Bind(); GlCall(glDrawElements(GL_TRIANGLES, ib.GetCount(), GL_UNSIGNED_INT, nullptr)); } - Build the project. There should be no new errors at this point.
#include "Renderer.h"
#include <iostream>
#include <algorithm>
void glClearError()
{
while (glGetError() != GL_NO_ERROR);
}
bool glLogCall(const char* function, const char* file, int line)
{
while (GLenum error = glGetError())
{
std::cout << "[OpenGL Error]( " << error << " ) "
<< function << " File: " << file << " Line:" << line << std::endl;
return false;
}
return true;
}
void Renderer::Draw(const VertexArray& va, const IndexBuffer& ib, const Shader& shader) const
{
shader.Bind();
va.Bind();
ib.Bind();
GlCall(glDrawElements(GL_TRIANGLES, ib.GetCount(), GL_UNSIGNED_INT, nullptr));
}
void Renderer::Draw(const VertexArray& va, const IndexBuffer& ib) const
{
va.Bind();
ib.Bind();
GlCall(glDrawElements(GL_TRIANGLES, ib.GetCount(), GL_UNSIGNED_INT, nullptr));
}
void Renderer::Clear() const
{
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
}
void Renderer::Clear(std::string colour) const
{
std::transform(colour.begin(), colour.end(), colour.begin(), ::towlower);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
if (colour == "black") GlCall(glClearColor(0, 0, 0, 1));
if (colour == "white") GlCall(glClearColor(1, 1, 1, 1));
if (colour == "red") GlCall(glClearColor(1, 0, 0, 1));
if (colour == "green") GlCall(glClearColor(0, 1, 0, 1));
if (colour == "blue") GlCall(glClearColor(0, 0, 1, 1));
if (colour == "yellow") GlCall(glClearColor(1, 1, 0, 1));
if (colour == "cyan") GlCall(glClearColor(0, 1, 1, 1));
if (colour == "magenta") GlCall(glClearColor(1, 0, 1, 1));
}
void Renderer::ClearColour_Black() const
{
GlCall(glClearColor(0, 0, 0, 1));
}
void Renderer::ClearColour_White() const
{
GlCall(glClearColor(1, 1, 1, 1));
}
Task 3 - Refactor main.cpp
The skeleton below is the Lab 08 monolithic main.cpp with // TODO: comments marking every section to replace. Work through it top to bottom.
- Replace the
#includeblock at the top. Remove all raw OpenGL and stb_image headers. Add includes for each abstraction class:"Renderer.h","VertexBuffer.h","VertexBufferLayout.h","VertexArray.h","IndexBuffer.h","Shader.h","Texture.h". Keep GLFW, GLM, and iostream. - Delete the standalone
parseShaderfunction,compileShaderfunction, andcreateProgramfunction - theShaderclass handles all of that. - Inside
main(), replace the raw VAO/VBO/IBO setup with:VertexArray vao; VertexBuffer vbo(vertices, sizeof(vertices)); VertexBufferLayout layout; layout.Push<float>(3); // position layout.Push<float>(2); // UV coordinates vao.AddBuffer(vbo, layout); IndexBuffer ibo(indices, 6); - Replace the shader creation block with
Shader shader("res/shaders/transform.shader");. - Replace the texture setup block with
Texture tex("res/Textures/1.png");. - Add
Renderer renderer;. Before the render loop, bind the shader and set the sampler uniform once:shader.Bind(); shader.setUniform1i("u_Texture", 0);. - Inside the render loop, replace the clear calls with
renderer.Clear();. - Replace the raw uniform, bind, and draw calls with:
tex.Bind(0); shader.setUniformMat4f("u_MVP", proj * view * model); renderer.Draw(vao, ibo, shader);. - Delete the entire cleanup block at the end of
main()(glDeleteTextures,glDeleteBuffers, etc.). RAII handles all cleanup automatically. - Build and run. The output must be identical to the Lab 08 result - a textured quad rotating around its Y axis with perspective/orthographic toggle on P.
// Lab 08 solution (starting point for this lab)
// Goal: replace every raw OpenGL block below with the abstraction classes from Labs 09-13
#include <GL/glew.h>
#include <GLFW/glfw3.h>
#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include <glm/gtc/type_ptr.hpp>
#include <iostream>
#include <fstream>
#include <sstream>
#include <string>
#include "vendor/stb_image.h"
struct ShaderSource { std::string vertex; std::string fragment; };
ShaderSource parseShader(const std::string& filepath) {
std::ifstream file(filepath);
std::stringstream ss[2];
int type = -1;
std::string line;
while (std::getline(file, line)) {
if (line.find("#shader vertex") != std::string::npos) type = 0;
else if (line.find("#shader fragment") != std::string::npos) type = 1;
else if (type == 0 || type == 1) ss[type] << line << '\n';
}
return { ss[0].str(), ss[1].str() };
}
static unsigned int compileShader(unsigned int type, const std::string& source) {
unsigned int id = glCreateShader(type);
const char* src = source.c_str();
glShaderSource(id, 1, &src, nullptr);
glCompileShader(id);
int result;
glGetShaderiv(id, GL_COMPILE_STATUS, &result);
if (!result) {
int len; glGetShaderiv(id, GL_INFO_LOG_LENGTH, &len);
char* msg = new char[len];
glGetShaderInfoLog(id, len, &len, msg);
std::cerr << "Shader compile error: " << msg << "\n";
delete[] msg; glDeleteShader(id); return 0;
}
return id;
}
unsigned int createProgram(const std::string& vertSrc, const std::string& fragSrc) {
unsigned int program = glCreateProgram();
unsigned int vs = compileShader(GL_VERTEX_SHADER, vertSrc);
unsigned int fs = compileShader(GL_FRAGMENT_SHADER, fragSrc);
glAttachShader(program, vs); glAttachShader(program, fs);
glLinkProgram(program);
glDeleteShader(vs); glDeleteShader(fs);
return program;
}
int main() {
if (!glfwInit()) { std::cerr << "Failed to initialise GLFW\n"; return -1; }
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 4);
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
GLFWwindow* window = glfwCreateWindow(800, 600, "Projections", nullptr, nullptr);
if (!window) { glfwTerminate(); return -1; }
glfwMakeContextCurrent(window);
glewExperimental = GL_TRUE;
if (glewInit() != GLEW_OK) { std::cerr << "Failed to initialise GLEW\n"; glfwTerminate(); return -1; }
glfwSwapInterval(1);
glEnable(GL_DEPTH_TEST);
glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
float vertices[] = {
-0.5f, -0.5f, 0.0f, 0.0f, 0.0f,
0.5f, -0.5f, 0.0f, 1.0f, 0.0f,
0.5f, 0.5f, 0.0f, 1.0f, 1.0f,
-0.5f, 0.5f, 0.0f, 0.0f, 1.0f
};
unsigned int indices[] = { 0, 1, 2, 2, 3, 0 };
// TODO: replace this raw VAO/VBO/IBO block with VertexArray, VertexBuffer,
// VertexBufferLayout (Push<float>(3), Push<float>(2)), and IndexBuffer
unsigned int vao, vbo, ibo;
glGenVertexArrays(1, &vao);
glBindVertexArray(vao);
glGenBuffers(1, &vbo);
glBindBuffer(GL_ARRAY_BUFFER, vbo);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
glEnableVertexAttribArray(0);
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (void*)0);
glEnableVertexAttribArray(1);
glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (void*)(3 * sizeof(float)));
glGenBuffers(1, &ibo);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ibo);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);
glBindVertexArray(0);
// TODO: replace parseShader + createProgram with Shader shader("res/shaders/transform.shader")
ShaderSource src = parseShader("res/shaders/transform.shader");
unsigned int shader = createProgram(src.vertex, src.fragment);
// TODO: replace raw texture setup with Texture texture("res/Textures/1.png")
stbi_set_flip_vertically_on_load(1);
int imgWidth, imgHeight, imgChannels;
unsigned char* imgData = stbi_load("res/Textures/1.png", &imgWidth, &imgHeight, &imgChannels, 4);
unsigned int texture;
glGenTextures(1, &texture);
glBindTexture(GL_TEXTURE_2D, texture);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, imgWidth, imgHeight, 0, GL_RGBA, GL_UNSIGNED_BYTE, imgData);
glBindTexture(GL_TEXTURE_2D, 0);
if (imgData) stbi_image_free(imgData);
// TODO: create a Renderer renderer
bool useOrtho = false;
bool pWasDown = false;
while (!glfwWindowShouldClose(window)) {
// TODO: replace glClearColor + glClear with renderer.Clear()
glClearColor(0.15f, 0.15f, 0.15f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
bool pDown = glfwGetKey(window, GLFW_KEY_P) == GLFW_PRESS;
if (pDown && !pWasDown) {
useOrtho = !useOrtho;
std::cout << (useOrtho ? "Orthographic\n" : "Perspective\n");
}
pWasDown = pDown;
float angle = (float)glfwGetTime();
glm::mat4 model = glm::rotate(glm::mat4(1.0f), angle, glm::vec3(0.0f, 1.0f, 0.0f));
glm::mat4 view = glm::lookAt(glm::vec3(0, 0, 3), glm::vec3(0, 0, 0), glm::vec3(0, 1, 0));
glm::mat4 proj = useOrtho
? glm::ortho(-1.33f, 1.33f, -1.0f, 1.0f, 0.1f, 100.0f)
: glm::perspective(glm::radians(45.0f), 800.0f / 600.0f, 0.1f, 100.0f);
glm::mat4 mvp = proj * view * model;
// TODO: replace raw uniform upload with shader.setUniform1i / shader.setUniformMat4f
// TODO: replace glActiveTexture + glBindTexture with texture.Bind(0)
// TODO: replace glBindVertexArray + glDrawElements with renderer.Draw(vao, ibo, shader)
glUseProgram(shader);
glUniformMatrix4fv(glGetUniformLocation(shader, "u_MVP"), 1, GL_FALSE, glm::value_ptr(mvp));
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, texture);
glUniform1i(glGetUniformLocation(shader, "u_Texture"), 0);
glBindVertexArray(vao);
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
glfwSwapBuffers(window);
glfwPollEvents();
}
// TODO: remove manual delete calls - RAII handles cleanup automatically
glDeleteTextures(1, &texture);
glDeleteBuffers(1, &ibo);
glDeleteBuffers(1, &vbo);
glDeleteVertexArrays(1, &vao);
glDeleteProgram(shader);
glfwDestroyWindow(window);
glfwTerminate();
return 0;
}
Solution
Try the tasks above before revealing the solution.