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

Lab 14 - Renderer and Refactoring

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:

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.

  1. Open src/Renderer.h. At the top, after #include <GL/glew.h>, add includes for the three classes that Draw depends on:
    #include "VertexArray.h"
    #include "IndexBuffer.h"
    #include "Shader.h"
  2. Inside the Renderer class, add the two Draw declarations:
    void Draw(const VertexArray& va, const IndexBuffer& ib, const Shader& shader) const;
    void Draw(const VertexArray& va, const IndexBuffer& ib) const;
    The second overload is for when the shader is already bound externally (for example, when the same shader is shared across multiple draw calls).
  3. 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 .cpp file. The reference block below shows the complete final header.
src/Renderer.h
#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.

  1. Add the first overload. Bind the shader, then the VAO, then the IBO. Call glDrawElements with GL_TRIANGLES, ib.GetCount(), GL_UNSIGNED_INT, and nullptr (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));
    }
  2. 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));
    }
  3. Build the project. There should be no new errors at this point.
src/Renderer.cpp
#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.

  1. Replace the #include block 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.
  2. Delete the standalone parseShader function, compileShader function, and createProgram function - the Shader class handles all of that.
  3. 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);
  4. Replace the shader creation block with Shader shader("res/shaders/transform.shader");.
  5. Replace the texture setup block with Texture tex("res/Textures/1.png");.
  6. Add Renderer renderer;. Before the render loop, bind the shader and set the sampler uniform once: shader.Bind(); shader.setUniform1i("u_Texture", 0);.
  7. Inside the render loop, replace the clear calls with renderer.Clear();.
  8. Replace the raw uniform, bind, and draw calls with: tex.Bind(0); shader.setUniformMat4f("u_MVP", proj * view * model); renderer.Draw(vao, ibo, shader);.
  9. Delete the entire cleanup block at the end of main() (glDeleteTextures, glDeleteBuffers, etc.). RAII handles all cleanup automatically.
  10. 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.
src/main.cpp
// 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.