GraphicsBlast
The Basics | C++ | Linux | Command Line

Lesson 4: My First Triangle Part B

Rendering Triangles
Rendering Triangles Part 2: Error handling, classes, and hot-reloading!

In the first part of this lesson, we looked at how shaders work and implemented the simplest working example. Well, in this second part of the lesson, we're going to go back and refactor our code and do it properly.

We'll start by moving our shader source code from a hard-coded string in our main.cpp into two new text files in our workspace. Our program will open these files at run-time, read in the contents, and build the shaders from that.

We'll encapsulate our shader into a class, which we'll implement in a new source code file called shader.cpp. In our main.cpp, we'll then create an instance of this class, and pass it the filenames of our vertex and shader source code. This class will then take care of everything else to load in and compile the shaders, including proper error checking this time.

Because our code will be nicely encapsulated in a class, we can simply call the functions again if we want to reload the shader from the files on disk, so we can hot-reload our shaders while the program is running! This encapsulation will also make it easier in the future if we want to have other shaders for different tasks. While our main shader might work for drawing most objects, we might need very different inputs to our shader if we want to draw water for example. So rather than having one hugely complicated shader which can handle everything, we can neatly split the code between these two shaders, and then switch between them depending on what we're drawing.

Let's get to work!

GLSL files

Hard-coding the shaders into our program is a not a nice solution. It means any small modifications require a recompile and restarting the program. It also complicates our C++ source code, and requires the nasty formatting of multi-line strings.

A much easier solution is to move the GLSL source code into their own text files.

I usually create a folder within my workspace called shaders and put all the GLSL code in there. Inside, I call the files something like main_vertex.glsl and main_fragment.glsl. It is also fairly common to name them something along the lines of main.vert and main.frag.

You can name these files whatever you like, there is no standard file extension to use. OpenGL dictates that it's passed a string, but doesn't say anything about how or where you get that string from. So the choice of filename is up to you.

So to start this lesson, I'm going to move our vertex shader source code from the previous lesson into a new file called shaders/main_vertex.glsl. Notice that we no longer need to add the explicit line endings ("\n"), but can work with the GLSL in a more natural way:

1.
#version 460
2.
3.
layout(location = 0) in vec3 aPosition;
4.
5.
void main()
6.
{
7.
    gl_Position = vec4(aPosition, 1.0f);
8.
}

...and likewise for shaders/main_fragment.glsl:

1.
#version 460
2.
3.
out vec4 fragment;
4.
5.
void main()
6.
{
7.
    fragment = vec4(1.0, 0.48, 0.02, 1.0);
8.
}

Defining our class

Next, let's begin to think about what our shader class will look like.

As we've been using the global scope to store our variables in up until now, we need to be careful about using the constructor of our class. We cannot use any OpenGL code in the constructor as it will be run before our program has initialised, and therefore before the OpenGL context has been created. Instead, we'll just use our constructor to perform some basic initialisation of our class variables, and have the shader program actually be created by an explicit function call.

Likewise, we won't rely on a deconstructor to clear up at the end, but instead use explicit functions to delete the shader program when we no longer need it, so we can tidy up before the OpenGL context is closed.

We'll also need functions to bind and unbind the shader, and to get any relevant information about the shader, such as the filenames and any error messages.

One other thing we'll do is have a function to set the input filenames of the shader, and another to actually load and create the shader program from those files. By separating these, we'll only need to set the input filenames once in our code, and can then call the function to (re-) load the shader from different places in our code without having to write out all the filenames again. This is useful for implementing the hot-reloading!

So let's start turning these ideas into a class, and lay them out in a new header file shader.h in our workspace. We'll start with the includes:

+ 1.
#pragma once
+ 2.
#include <string>
+ 3.
#include <GL/glew.h>
+ 4.
+ 5.
using namespace std;

We use a Pragma Once to make the header file safe to include multiple times elsewhere in our project without re-declaration errors.

We also bring in the string library, which will be helpful for storing the contents of the files, and GLEW. GLEW contains the type definitions for OpenGL, which we will need to store the handle to our shader (which was a GLuint) so we need to include it too.

Our shader class doesn't contain any SDL specific code, so these headers can be omitted entirely here.

I will say I've taken a bit of a shortcut here. Rather than type out std::string every time we use them, I've just used the std namespace here to keep the code concise. You can use which ever you prefer, although the full form is usually considered better practice.

We can now define our Shader class:

5.
using namespace std;
6.
+ 7.
class Shader
+ 8.
{
+ 9.
    public:
+ 10.
        Shader();
+ 11.
+ 12.
        void setSourceFiles(string vertexFilename, string fragmentFilename);
+ 13.
        int loadShader();
+ 14.
        void deleteShader();
+ 15.
+ 16.
        void bind();
+ 17.
        void unbind();
+ 18.
+ 19.
        GLuint getHandle();
+ 20.
        string getFilenames();
+ 21.
        string getError();
+ 22.
+ 23.
    private:
+ 24.
        string vFilename, fFilename;
+ 25.
        GLuint shaderProgram;
+ 26.
        string errorMessage;
+ 27.
+ 28.
        string readFile(string filename);
+ 29.
};

Our class naturally starts with the constructor declaration, which we'll use to make sure that any private variables are initialised to safe starting values.

We then have the function to set the filenames from which the vertex and fragment shader will later be read from. Another to actually load those source files and create the shader program, in this case returning an int with zero indicating success. This is then followed by a third function to control the deletion of the shader program after we're finished with it.

Once the shader has been loaded, we then have a pair of functions to bind and unbind the current shader program.

Then we have a few functions to allow us to access the object's private variables. The first, getHandle(), allows us to access the raw shader program's handle should we ever want it (the private variable shaderProgram). The function getFilenames() returns us the concatenated source code filenames, and getError() returns us any error messages created while attempting to load the shader.

As the loadShader function will need to read the contents of several files, I've broken this bit of code out into a separate function. So the final private element we have is the function readFile(), which simply takes a filename on disk and returns it's content, or an empty string on error.

Implementing our class

Let's create a new file, shader.cpp, and start filling in those functions we just defined:

+ 1.
#include "shader.h"
+ 2.
#include <fstream>
+ 3.
#include <sstream>
+ 4.
+ 5.
Shader::Shader()
+ 6.
{
+ 7.
    shaderProgram = 0;
+ 8.
}
+ 9.
+ 10.
void Shader::setSourceFiles(string vertexFilename, string fragmentFilename)
+ 11.
{
+ 12.
    vFilename = vertexFilename;
+ 13.
    fFilename = fragmentFilename;
+ 14.
}

We start by including our header file, and also fstream and sstream, to help us with manipulating files and strings.

Our constructor just initialises our shaderProgram variable to a safe initial value. Remember, OpenGL uses integers to reference shader programs, where zero is understood as not pointing to any program. Our other string variables in our class are implicitly initialised to empty strings when they were declared in the header, so there is no need to set them to an empty value here.

We also define our setSourceFiles function to simply set the internal filename variables to what has been passed in.

Just before we define our loadShader function, let's first fill in the readFile function which we'll need first. It will accept a filename as a parameter, and return the file's contents as a string:

13.
    fFilename = fragmentFilename;
14.
}
15.
+ 16.
string Shader::readFile(string filename)
+ 17.
{
+ 18.
    ifstream file(filename);
+ 19.
    if(!file.is_open())
+ 20.
    {
+ 21.
        errorMessage += "Cannot open file: ";
+ 22.
        errorMessage += filename;
+ 23.
        return "";
+ 24.
    }
+ 25.
+ 26.
    stringstream buffer;
+ 27.
    buffer << file.rdbuf();
+ 28.
    string contents = buffer.str();
+ 29.
    file.close();
+ 30.
+ 31.
    if(contents.empty())
+ 32.
    {
+ 33.
        errorMessage += "Shader source file is empty: ";
+ 34.
        errorMessage += filename;
+ 35.
    }
+ 36.
+ 37.
    return contents;
+ 38.
}

For reference, there are lots of ways to read a file and convert it's contents into a string in C++. I've chosen this particular one purely because it's concise.

We attempt to open the file, and if we fail, we write an error message and return an empty string. We then use a stringstream to concisely read in the file's contents, and then convert that to a string.

We perform one more check to see if the file actually contained any text or not, as that's something easy to here, and will be problematic later on if we try to compile it. Again we write an error message if so.

Next up is the big one. Our loadShader function, which will actually create the shader program:

37.
    return contents;
38.
}
39.
+ 40.
int Shader::loadShader()
+ 41.
{
+ 42.
    errorMessage = "";
+ 43.
    deleteShader();
+ 44.
+ 45.
    if(vFilename.empty() || fFilename.empty())
+ 46.
    {
+ 47.
        errorMessage = "Shader source filenames not set";
+ 48.
        return -1;
+ 49.
    }
+ 50.
+ 51.
    shaderProgram = glCreateProgram();
+ 52.
    if(shaderProgram == 0)
+ 53.
    {
+ 54.
        errorMessage = "Unable to create shader program";
+ 55.
        return -1;
+ 56.
    }
+ 57.
+ 58.
    ... 

The first thing we do when loading our shader is to make sure that any previous shader programs are properly deleted, and that our variables have been reset to safe starting values. We also check that both the shader's source filenames have been set before proceeding. If not, we write an error message and return negative one to indicate a problem during initialisation.

Then we make a call to glCreateProgram. Unlike in the last lesson, we check the return value of this function. If it is zero, indicating a non-valid program, we again write an error message and return.

54.
        errorMessage = "Unable to create shader program";
55.
        return -1;
56.
    }
57.
+ 58.
    GLuint vShader = glCreateShader(GL_VERTEX_SHADER);
+ 59.
    if(vShader == 0)
+ 60.
    {
+ 61.
        errorMessage = "Unable to create vertex shader";
+ 62.
        deleteShader();
+ 63.
        return -1;
+ 64.
    }
+ 65.
+ 66.
    string vTextString = readFile(vFilename);
+ 67.
    if(vTextString.empty())
+ 68.
    {
+ 69.
        glDeleteShader(vShader);
+ 70.
        deleteShader();
+ 71.
        return -1;
+ 72.
    }
+ 73.
+ 74.
    const char* vText = vTextString.c_str();
+ 75.
    glShaderSource(vShader, 1, &vText, NULL);
+ 76.
    glCompileShader(vShader);
+ 77.
+ 78.
    ...

Similarly, when we create the vertex shader, we check the return value with zero again indicating a problem. If a problem occurs, as well as setting the error message we also make a call to deleteShader(), which ensures that the shader program we created a moment ago is properly deleted, and our variables reset before returning.

With the vertex shader created, we can then read in it's source file by making a call to our previous readFile function. We pass in the vertex shader source file's name, expecting a string of the file's contents back. If anything went wrong, including the file being empty, then the returned string will be empty. We can therefore test the string length to check for an error. If something went wrong, the error message will be set by the readFile function, so we don't need to set any error message here, just delete our new vertex shader and shader program as before.

If everything we OK, we then convert our string to the required type (const char*), pass it in to OpenGL as the shader's source, and try to compile it just like we did in the previous lesson. Finally, we compile the shader.

Just like compiling C++ code though, compiling shaders doesn't always go to plan. So next we need to check the output of the compiler:

75.
    glShaderSource(vShader, 1, &vText, NULL);
76.
    glCompileShader(vShader);
77.
+ 78.
    GLint vStatus;
+ 79.
    glGetShaderiv(vShader, GL_COMPILE_STATUS, &vStatus);
+ 80.
    if(vStatus == GL_FALSE)
+ 81.
    {
+ 82.
        GLint logLength;
+ 83.
        glGetShaderiv(vShader, GL_INFO_LOG_LENGTH , &logLength);
+ 84.
+ 85.
        GLchar* compilerLog = new GLchar[logLength];
+ 86.
        glGetShaderInfoLog(vShader, logLength, NULL, compilerLog);
+ 87.
+ 88.
        errorMessage = "Error compiling vertex shader: ";
+ 89.
        errorMessage += compilerLog;
+ 90.
        delete[] compilerLog;
+ 91.
+ 92.
        glDeleteShader(vShader);
+ 93.
        deleteShader();
+ 94.
        
+ 95.
        return -1;
+ 96.
    }
+ 97.
    glAttachShader(shaderProgram, vShader);
+ 98.
+ 99.
    ...

Woah, that looks pretty complex.

Let's take it line by line.

We make a call to glGetShaderiv to see the compile status (GL_COMPILE_STATUS) of our shader. glGetShaderiv is used to request any information about the shader, with the suffix "iv" following the OpenGL convention of specifying the function's return types at the end of the function names - a vector (array) of ints in this case. We pass in as parameters our shader, what information we want, and a place to store the result: the address of an integer which OpenGL will alter to indicate whether compiling was successful or not.

If compiling was not successful, then we can grab the compiler log and add it to our errorMessage. The idea here is quite old-school, but to get the log we first need to ask OpenGL how long the log is, then we can dynamically allocate that amount of memory as a string, and then copy the log into our string.

So we make another call to glGetShaderiv, similar to before but this time asking for the length of the compiler log. We pass in the shader, tell it we want the log length using GL_INFO_LOG_LENGTH, and pass in an integer for it to put the result into.

Once we have the log length, then we allocate memory for a new string called compilerLog of that length. We can then copy the log data into that string by calling glGetShaderInfoLog. The first parameter of this call is the vertex shader object we tried to compile, and the second is how many characters to copy into our buffer in case we need to break the message up into parts. This might be useful on low-memory devices, but here we just tell it to copy the whole log. For the third parameter, OpenGL allows you pass in a variable to get how many characters were copied across. However as we are just copying everything across into a buffer that we know it will fit into, we don't need to worry about this, in which case the specification recommends simply passing NULL. Finally, we pass in the string which the log will be copied into.

Now that we have the compiler log of our failed vertex shader, we append it to our errorMessage variable. Before returning, we free the dynamically allocated memory, delete the vertex shader, and delete our shader program.

If the shader did compile correctly, fortunately our code is much simpler! We call glAttachShader to add our newly compiled vertex shader to our shader program.

Then we basically just do all of that again for our fragment shader:

97.
    glAttachShader(shaderProgram, vShader);
98.
+ 99.
    GLuint fShader = glCreateShader(GL_FRAGMENT_SHADER);
+ 100.
    if(fShader == 0)
+ 101.
    {
+ 102.
        errorMessage = "Unable to create fragment shader";
+ 103.
        glDetachShader(shaderProgram, vShader);
+ 104.
        glDeleteShader(vShader);
+ 105.
        deleteShader();
+ 106.
        return -1;
+ 107.
    }
+ 108.
+ 109.
    string fTextString = readFile(fFilename);
+ 110.
    if(fTextString.empty())
+ 111.
    {
+ 112.
        glDetachShader(shaderProgram, vShader);
+ 113.
        glDeleteShader(vShader);
+ 114.
        glDeleteShader(fShader);
+ 115.
        deleteShader();
+ 116.
        return -1;
+ 117.
    }
+ 118.
+ 119.
    const char* fText = fTextString.c_str();
+ 120.
    glShaderSource(fShader, 1, &fText, NULL);
+ 121.
    glCompileShader(fShader);
+ 122.
+ 123.
    ...

This code is pretty much exactly the same again, this just with some "f"s, instead of "v"s!

Note that our shader type has change to type GL_FRAGMENT_SHADER, and now in the event of an error we also detach and delete the vertex shader.

So let's check the compiler log for our fragment shader:

121.
    glCompileShader(fShader);
122.
+ 123.
    GLint fStatus;
+ 124.
    glGetShaderiv(fShader, GL_COMPILE_STATUS, &fStatus);
+ 125.
    if(fStatus == GL_FALSE)
+ 126.
    {
+ 127.
        GLint logLength;
+ 128.
        glGetShaderiv(fShader, GL_INFO_LOG_LENGTH , &logLength);
+ 129.
+ 130.
        GLchar* compilerLog = new GLchar[logLength];
+ 131.
        glGetShaderInfoLog(fShader, logLength, NULL, compilerLog);
+ 132.
+ 133.
        errorMessage = "Error compiling fragment shader: ";
+ 134.
        errorMessage += compilerLog;
+ 135.
        delete[] compilerLog;
+ 136.
+ 137.
        glDetachShader(shaderProgram, vShader);
+ 138.
        glDeleteShader(vShader);
+ 139.
        glDeleteShader(fShader);
+ 140.
+ 141.
        deleteShader();
+ 142.
+ 143.
        return -1;
+ 144.
    }
+ 145.
    glAttachShader(shaderProgram, fShader);
+ 146.
+ 147.
    ...

Again, exactly as with the vertex shader.

If it failed to compile then we get the compiler log and add it to the error message. We also need to detach and delete the vertex shader before returning this time however. If it did work though, we also attach the fragment shader to our shader program.

Next, on to the linking:

145.
    glAttachShader(shaderProgram, fShader);
146.
+ 147.
    glLinkProgram(shaderProgram);
+ 148.
+ 149.
    GLint linkStatus;
+ 150.
    glGetProgramiv(shaderProgram, GL_LINK_STATUS, &linkStatus);
+ 151.
    if(linkStatus == GL_FALSE)
+ 152.
    {
+ 153.
        GLint logLength;
+ 154.
        glGetProgramiv(shaderProgram, GL_INFO_LOG_LENGTH , &logLength);
+ 155.
+ 156.
        GLchar* compilerLog = new GLchar[logLength];
+ 157.
        glGetProgramInfoLog(shaderProgram, logLength, NULL, compilerLog);
+ 158.
+ 159.
        errorMessage = "Error linking shader program: ";
+ 160.
        errorMessage += compilerLog;
+ 161.
        delete[] compilerLog;
+ 162.
+ 163.
        glDetachShader(shaderProgram, vShader);
+ 164.
        glDeleteShader(vShader);
+ 165.
        glDetachShader(shaderProgram, fShader);
+ 166.
        glDeleteShader(fShader);
+ 167.
+ 168.
        deleteShader();
+ 169.
+ 170.
        return -1;
+ 171.
    }
+ 172.
+ 173.
    ...

Despite linking only being a single line, again we go through the whole process of getting the logs and clearing up if something failed.

This time we ask OpenGL for the linking status instead of the compile status, with the GL_LINK_STATUS flag. We need to update our function calls as well to glGetProgramiv and glGetProgramInfoLog. Note the "Program" instead of "Shader" in the function names. This time we also need detach and delete both shaders if something went wrong.

But that's basically it!

TIP: Don't use glValidateProgram here! I've seen a lot of tutorials online and questions on Stack Overflow saying again and again that after linking, you should call glValidateProgram to "validate the shader program works properly". This is not what this function does. It's designed to be used for debugging and optimisation, and will tell you if you can actually run the shader given the current state of the program or not. For example, perhaps you forgot to bind some important data or a VAO before using the shader. This function will warn you, or tell you that your code is inefficient in some way. But it's not for testing the building of your shader program! The OpenGL specification is here.

With the shader program linked and ready to go, we just need to tidy up before finishing:

168.
        deleteShader();
169.
170.
        return -1;
171.
    }
172.
+ 173.
    glDetachShader(shaderProgram, vShader);
+ 174.
    glDeleteShader(vShader);
+ 175.
    glDetachShader(shaderProgram, fShader);
+ 176.
    glDeleteShader(fShader);
+ 177.
+ 178.
    unbind();
+ 179.
+ 180.
    return 0;
+ 181.
}

With the program linked and the executable generated, we can safely detach and delete the shaders to free up their memory, we only care about the finished executable now!

We then unbind the shader, and return zero to indicate success.

Anyway that's the bulk of the work out the way for our shader class. The rest of the functions are pretty trivial by comparison.

Next up is the code for deleting the shader:

178.
    unbind();
179.
180.
    return 0;
181.
}
182.
+ 183.
void Shader::deleteShader()
+ 184.
{
+ 185.
    unbind();
+ 186.
    glDeleteProgram(shaderProgram);
+ 187.
    shaderProgram = 0;
+ 188.
}

To delete the shader, we start by making sure the shader is not currently bound. This function will be safe to call whether the program is currently bound or not, or even if it doesn't exist, so we don't need to provide any protections here.

We then call glDeleteProgram which deletes the shader program on the GPU, and frees any memory associated with it. Again this is safe to call whether the program exists or not. If it doesn't, OpenGL understands deleting a value of zero as an instruction to do nothing!

Finally, we reset the rest of our shader program handle back to a default state.

Our functions for binding and unbinding the shader program are similarly straight-forward:

187.
    shaderProgram = 0;
188.
}
189.
+ 190.
void Shader::bind()
+ 191.
{
+ 192.
    glUseProgram(shaderProgram);
+ 193.
}
+ 194.
+ 195.
void Shader::unbind()
+ 196.
{
+ 197.
    glUseProgram(0);
+ 198.
}

Like in the last tutorial, we make a call to glUseProgram to set the currently bound shader program.

Passing in our program will make it current, and as before passing a value of zero has the effect of unbinding any programs.

We've made sure that if our shader program cannot be loaded, our shaderProgram variable will have a value of zero. That means that if our code attempts to bind a problematic shader, we'll effectively be binding nothing, so these functions are safe to call even if something has gone wrong.

We finish up by defining our "getter" functions:

197.
    glUseProgram(0);
198.
}
199.
+ 200.
GLuint Shader::getHandle()
+ 201.
{
+ 202.
    return shaderProgram;
+ 203.
}
+ 204.
+ 205.
string Shader::getFilenames()
+ 206.
{
+ 207.
    return vFilename + " " + fFilename;
+ 208.
}
+ 209.
+ 210.
string Shader::getError()
+ 211.
{
+ 212.
    return errorMessage;
+ 213.
}

In case someone needs to get the raw handle to the actual OpenGL shader program, we provide it through the getHandle function. Again, if something has gone wrong, this function should always return a value of zero, indicating an invalid shader.

We also provide a function for getting the shader source code filenames which our shader is currently using by simply concatenating the variables together, and a function which returns the object's errorMessage variable.

With that, all our functions for our shader class are now defined!

Updating Our Makefile

As we've added a new source code file to our project, we need to remember to compile it into our project too! All we need to do is to add the extra source code filename to the OBJS variable in our makefile:

1.
CC = g++
2.
+ 3.
OBJS = main.cpp shader.cpp
4.
5.
LIBRARIES = -lSDL3 -lSDL3_image -lGL -lGLEW

Remember, you only need to add the source code file (.cpp), not the headers!

Using Our Shader Class

With our new class ready to load and compile shader programs, let's adjust our main code to actually make use of this class to draw the triangle from the previous lesson. Let's begin by including our new class's header file into main.cpp:

1.
#include <stdio.h>
2.
#include <SDL3/SDL.h>
3.
#include <SDL3/SDL_main.h>
4.
#include <SDL3_image/SDL_image.h>
5.
#include <GL/glew.h>
6.
+ 7.
#include "shader.h"
8.
9.
int windowWidth = 1024;
10.
int windowHeight = 600;

...and declaring an instance of the class:

15.
bool programRunning = true;
16.
bool isFullscreen = false;
17.
+ 18.
Shader mainShader;
19.
20.
GLuint vao;
21.
GLuint vbo;

Now with our mainShader object ready to go, we can remove all of the GLSL and old shader compiling code from the previous lesson and replace it with a few simple lines. Note that we still need the code for setting our vertices and initialising our buffers:

84.
        printf("Unable to get a recent OpenGL version!\n");
85.
        return -1;
86.
    }
87.
    printf("%s\n", glGetString(GL_VERSION));
88.
+ 89.
    mainShader.setSourceFiles("shaders/main_vertex.glsl", "shaders/main_fragment.glsl");
+ 90.
    if(mainShader.loadShader() != 0)
+ 91.
    {
+ 92.
        printf("Unable to create shader from files: %s\n", mainShader.getFilenames().c_str());
+ 93.
        printf("Error message: %s\n", mainShader.getError().c_str());
+ 94.
        return -1;
+ 95.
    }
96.
97.
    GLfloat vertices[] = 
98.
    {
99.
        -0.5f, -0.5f, 0.0f,

We make a call to our setSourceFiles function and tell the object where it's GLSL files are located. Then we call our loadShader function which will actually read those files and create the shader program. Finally we provide some simple error checking, printing the error messages and input files, and returning if the shader couldn't be initialised properly. Note that printing the filenames can be incredibly useful for debugging problems, especially if you have multiple shaders.

We also need to update our close function to call deleteShader for our object when we're finished with it, rather than calling glDeleteProgram as we did before:

123.
void close()
124.
{
125.
    glDeleteVertexArrays(1, &vao);
126.
    glDeleteBuffers(1, &vbo);
+ 127.
    mainShader.deleteShader();
128.
129.
    SDL_GL_DeleteContext(context);
130.
    SDL_DestroyWindow(window);
131.
    SDL_Quit();
132.
}

We can then finish up updating our main code by making sure we bind and unbind the object when we make our draw calls:

181.
void draw()
182.
{
183.
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
184.
+ 185.
    mainShader.bind();
186.
    glBindVertexArray(vao);
187.
    glDrawArrays(GL_TRIANGLES, 0, 3);
188.
    glBindVertexArray(0);
+ 189.
    mainShader.unbind();
190.
191.
    SDL_GL_SwapWindow(window);
192.
}

Basically, we just replace the calls to glUseProgram with our custom bind and unbind functions!

Hot Reloading

A nice side-effect of all our shader code now being nicely encapsulated in a class is that we can easily re-call the functions anywhere else in our code. So we'll finish up by reloading our shaders at run-time if the user presses "r" on the keyboard.

This is an incredibly useful feature for debugging. You can leave the program running, freely alter your GLSL files in a text editor, and then when you're ready save them and tap the keyboard to see the result!

And it's incredibly simple to implement:

158.
            else if(event.key.keysym.sym == SDLK_f)
159.
            {
160.
                isFullscreen = !isFullscreen;
161.
                SDL_SetWindowFullscreen(window, isFullscreen);
162.
            }
+ 163.
            else if(event.key.keysym.sym == SDLK_r)
+ 164.
            {
+ 165.
                if(mainShader.loadShader() != 0)
+ 166.
                {
+ 167.
                    printf("Unable to create shader from files: %s\n", mainShader.getFilenames().c_str());
+ 168.
                    printf("Error message: %s\n", mainShader.getError().c_str());
+ 169.
                    programRunning = false;
+ 170.
                }
+ 171.
            }
172.
        }
173.
    }
174.
}

In our handleEvents function, we just need to detect the keypress, make a new call to mainShader.loadShader(), and then do some error checking.

So we alter our keypress code to test for SDLK_r, the "r" key, which I'm using simply because it's the first letter of "reload"! For reference the full list is of keys is here if you want something else.

If it was pressed, we make the call to load the shader. This will cause our shader object to delete the old shader program (the first thing the loadShader function does), re-open the source files, and build a new shader program on the GPU from the contents.

If this worked, the function will return zero and our program can carry on rendering like nothing happened - although perhaps with a new colour scheme! On the other hand, if something bad happened during the re-load, the function will return a non-zero value, so we print the shader's error messages and source filenames. Finally we set programRunning to false, meaning our program will exit before attempting another draw to the screen, so it doesn't matter that our shader program is currently broken.

Conclusion

Compile and run your code, and hopefully after all that, we still just have the exact same triangle on screen! Anyway. We now have our shaders programmed properly, can trivially add more shader objects in the future, and can hot-reload them on command.

If you have any trouble compiling the new code for this lesson, don't forget to update your makefile to tell your compiler it also needs to build shader.cpp too!

If you want to play with your new code a little, while the program is running, go into your fragment shader and try adjusting the colour of the pixels. Hit "r" in your window and watch the results!