Tutorial

Modern OpenGL: Part 11 Creating a basic Obj loader and loading models into Buffers

Here we go

Get ready, because this is going to be a bit of a longer post than usual because we are going to be writing an obj loader from scratch. This will involve us diving into some of the finer details of understanding vertex and polygon data.

Why write another obj loader?

You might be asking why we need to do this and why aren’t we using a library to load meshes because it’s been done a thousand times already. You might also be frustrated that I am not suggested we use glTF as the format of choice. You are right on all counts. You should use glTF and you should use a library. That doesn’t mean that learning how to write an obj loader isn’t useful. It serves a purpose.

obj is a very odd format in the way that it stores its data. By learning how to read data, interpret it and store it into your own c++ data structures, you will learn some useful skills in data processing and it will add to your arsenal of mental tools in how you approach thinking about 3D data.

I also feel that writing an obj loader is a kind of ‘rite of passage’ of graphics programming. It’s one of those things that everyone seems to want to do and it is accessible enough that anyone with a small knowledge of programming can achieve.

What do we need to do?

One of the issues with obj is that the data stored in the format is not in a layout that is ready to be ingested by OpenGL in its raw form. It will need to be read, analysed, processed and prepared into buffers for OpenGL to read.

First we need to define the final vertex format that we will want to be ended up with. We have the power to decide what the requirements are for the meshes we want to render. We are going to start with just 3 kinds of attributes. positions, normals and texture coordinates. These 3 things alone will be enough for us to get quite far and be able to load meshes, light them and apply textures.

We will define a data structure in our c++ code that we want to load the obj data into for ingesting by OpenGL. Let’s define a struct called vertex3D that has 3 members in it…..

#pragma once

#include <cstdio>
#include <fmt/core.h>
#include <vector>
#include "glm/glm.hpp"

struct vertex3D {
    glm::vec3 position;
    glm::vec3 normal;
    glm::vec2 texCoord;
};

We will be parsing the obj file and processing then loading each vertex’s data into one of these structs. There are many ways we could layout out our data (interleaved vs separate arrays) but we will choose to interleave here. We can always transform the data later on. This struct definition is public to other cpp files because they will use it also. We will also need std vector so lets include that now.

Next we will open a new namespace and put all of our reading code in it.

namespace objLoader {

using namespace std;

Next we will want to have a temporary store of data that will simply read in the data from the obj to get it into memory. This is even before we do the interleaving and store the data in the custom vertex3D struct we defined before. This is just getting the obj file data in to c++ vectors for now.

struct RawMeshData {

    RawMeshData() : positions(1), normals(1), textureCoords(1) {
    }
    // dummy value at 0. removes the need for subtracting 1 from obj file
    std::vector<glm::vec3> positions;
    std::vector<glm::vec3> normals;
    std::vector<glm::vec2> textureCoords;
    std::vector<glm::ivec3> faceIndices;

};

Here is it very important that we initialize the vectors with a dummy value by giving them a size of 1 from the get go. This is a nifty trick that I’d learned in a forum somewhere. The reason is because of the fact that obj indices start at 1 ( they are not zero index based) and this will remove the need for us to have to subtract 1 later.

So now we will write the code to read in the data from the file and store it in the RawMeshData structure. The basic list of things

  1. Loop through each line of the obj
  2. Figure out the line length int erms of how many characters there are
  3. find out what kind of data is on that line
  4. Find out where the spaces are located in the line, store those locations and replace them with a special end line character (for reasons I will explain)
  5. based on the kind of data stored on that line, take the values separated by spaces and store it in one of the vector in the raw mesh data

We are starting off by writing a function called readObjRaw() that takes a file path that is the path to the obj file on disk and returns one of these raw mesh structures that has all the data loaded into it…

RawMeshData readObjRaw(const std::string& filePath) {



So now within this function we will begin by declaring an empty RawMeshData for us to load…

    RawMeshData meshData;

Then we will use some C functions to open the file. I did consider using c++ all the way for this part of the code, but as much as I try and defend c++ from its shortcomings, in this case using c++ iostreams are simply slower than straight C functions so I’ll just going to use the tool that isn’t slow by default. The code is slightly uglier, but I can live with that.

    FILE* fp = fopen(filePath.c_str(), "r");
    if (!fp) {
        fmt::print(stderr, "Error opening file\n");
    }

Next we need to create a small buffer in the form of a character array that we will read each line of the file into. This needs to be big enough to contain the whole line. I am going to choose 128 as the max length of the line. There might very well be obj files out there that contain 128 characters or more per line, but this should cover most cases and is enough for our purposes. We are also going to be calculating the length of the string after it has been read into the buffer, so we declare a variable that we can reuse for that purpose.

    char line[128];
    size_t line_size;

Then we want to have a temporary dynamic buffer of index positions that store where in the line of text that spaces occur. This allows us to track where the values in the text are stores so we can easily parse them into variables. We are also going to have a char pointer variable called end which is going to track the last character in the line that we were reading so we can pass that pointer into the next parsing function to start from…

    std::vector<int> spacePositions(8);

    char* end;
    uint32_t key;

We also add this key variable which is going to store which type of line the current line is.

Now we can start the code that loops through each line of the text…

    while (fgets(line, 128, fp)) {

This line starts a while loop and calls the C function fgets() which takes the buffer that we want to fill with the line, how many characters to read into that buffer (we just choose the same length as the buffer in this case) and the pointer to the file handle that we opened earlier. This while loop will continue to loop until the end of the file.

Now we are going to do some setup work at the beginning of each loop iteration once the line has been read into the buffer…

        { // setup
            line_size = strlen(line);
            spacePositions.clear();
            key = packCharsToIntKey(line[0], line[1]);
        }

What we are doing here is reading how many characters were actually read because each line can be of arbitrary length. Then we are going to clear the vector that stores the positions of where the spaces are in the line. We do this because std vectors are dynamic memory and we want to reuse the memory that has already been allocated and prevent having to reallocate every loop iteration. This is a common pattern in computer graphics where you need to track multiple things each loop iteration where the count can change per iteration.

Then we calling a packCharsToIntKey() function (which we will show how to write next) that reads what the first two characters that are in the line and returns a special ‘key’ that we can use in a switch case statement (we are passing in the first two characters by indexing directly into position 0 and 1 of the line buffer). Switch case mechanisms in c and c++ are useful constructs to be able to branch on multiple values and jump to the right bit of code depending on what the value is. The reason we use this special key conversion is because switch case doesn’t work on multiple characters. We are lucky that two chars can be encoded into a single integer value by using some bit shifting. Then we can shift on the integer.

Here is how we write that encoding packCharsToIntKey() function (which we can put at the top of this file within the namespace)…

constexpr uint32_t packCharsToIntKey(char a, char b) {
    return (static_cast<uint32_t>(a) << 8) | static_cast<uint32_t>(b);
}

This function now gives us a key to switch on, but what do we put in the switch statement as the values of the key? For example, if the line in the obj file starts with a “vn” then we want to switch on the key that is the integer corresponding to those letters. We can use some predetermined values for those keys and then just refer to those values without having to know what the keys actually are.

constexpr uint32_t v = packCharsToIntKey('v', ' ');
constexpr uint32_t vn = packCharsToIntKey('v', 'n');
constexpr uint32_t vt = packCharsToIntKey('v', 't');
constexpr uint32_t vp = packCharsToIntKey('v', 'p');
constexpr uint32_t f = packCharsToIntKey('f', ' ');

Before we start our switching on values, we have to first detect in the current line where the spaces exist. This bit of code does that for us.

        
        // spaces after the first will always be after 3
        for (auto i = 0u; i < line_size; ++i) {
            if (line[i] == ' ') {
                line[i] = '\0';
                spacePositions.push_back(i + 1);
            }
        }
        spacePositions.push_back((int)line_size));

          
        

What this does is loop though the line and when it detects a space, it replaces it with a null character (‘\0’). The reason for this will be made clear soon. It also stores the position of that space which we will use as the starting position for parsing the data. Then we also store the end position which gives us a way later to check wether we are reading a triangle or a quad polygon in the obj file. If there are a particular number of spaces (5), then we know its a quad.

Now lets start our switch case within the while loop on the key that we retrieved in the setup section previously…

        switch (key) {

and for the first case we want to handle when the line is a vertex, (which is denoted by the letter ‘v’)

        case v: {
            meshData.positions.emplace_back(
                std::strtof(&line[spacePositions[0]], nullptr),
                std::strtof(&line[spacePositions[1]], nullptr),
                std::strtof(&line[spacePositions[2]], nullptr));
            break;
        }

This will store the data of the 3 coordinates of the position of the vertex in the positions vector in the meshData structure. The emplace_back function is a function very similar to push_back on the std::vector, but allows us to pass the arguments of the vec3 constructor directly. We could have called push_back, but that would mean we have to construct a vec3 first which could incur a copy.

I will explain the strtof() function as well. This is one of the C functions that can read text and convert to binary floating point values. It takes the position in the line of text where the spaces occur given to us by the spacePositions indices we stores previously and parses the text until it meets a null character. This is why we previously injected null characters into the spaces so that this function would know when to end its parsing between spaces.

The next step is to create the case for a vertex normal, denoted by “vn” at the start of the line. We use the vn variable which is the key associated with those two characters.

        case vn: {
            meshData.normals.emplace_back(
                std::strtof(&line[spacePositions[0]], nullptr),
                std::strtof(&line[spacePositions[1]], nullptr),
                std::strtof(&line[spacePositions[2]], nullptr));
            if (!startGroupTracking) {
                startGroupTracking = true;
            }
            break;
        }

and then we do the same for the vertex texture coordinates…

        case vt: {
            meshData.textureCoords.emplace_back(
                std::strtof(&line[spacePositions[0]], nullptr),
                std::strtof(&line[spacePositions[1]], nullptr));
            break;
        }

That takes care of all of the individual point data. Now we move onto the case that reads the lines in the obj that describe the polygons which are lists of the point indices.

        case f: {
            // is face

            int a = std::strtol(&line[spacePositions[0]], &end, 10);
            int b = std::strtol(end + (*end == '/'), &end, 10);
            int c = std::strtol(end + (*end == '/'), &end, 10);
            meshData.faceIndices.emplace_back(a, b, c);

            int d = std::strtol(&line[spacePositions[1]], &end, 10);
            int e = std::strtol(end + (*end == '/'), &end, 10);
            int f = std::strtol(end + (*end == '/'), &end, 10);

            meshData.faceIndices.emplace_back(d, e, f);

            int g = std::strtol(&line[spacePositions[2]], &end, 10);
            int h = std::strtol(end + (*end == '/'), &end, 10);
            int i = std::strtol(end + (*end == '/'), &end, 10);

            meshData.faceIndices.emplace_back(g, h, i);

            if (spacePositions.size() == 5) {
                // face 0
                meshData.faceIndices.emplace_back(a, b, c);
                // face 2
                meshData.faceIndices.emplace_back(g, h, i);

                // reuse def as those temps aren't needed
                d = std::strtol(&line[spacePositions[3]], &end, 10);
                e = std::strtol(end + (*end == '/'), &end, 10);
                f = std::strtol(end + (*end == '/'), &end, 10);

                meshData.faceIndices.emplace_back(d, e, f);
            }

            break;
        }

You can think of the polygons in an obj file as a game of ‘join the dots’. We look at the line and each index triple is made up of indices into the points we have read, and is in the format position/normal/textureCoordinate.. If the polygon is made up of 3 triples, then it is a triangle, if it is 4 then it is a quad. If it is 5 then you need to talk to your modeller about their life choices.

So what we do in the case of the polygon being a quad is that we first make a triangle from 3 of the 4 vertices, effectively splitting the quad into two triangles. Then we detect the case that it is a quad (with the spacePositions.size() == 5 check, because if there are 5 space, it means there a 4 triples)., and if it is, then we create the other triangle that makes up the quad by emplacing back 3 more vertices. We have to be careful with the order here though to make sure that the winding order is correct.

It is also worth quickly explaining the use of the ‘end’ variable inside of the strtol function. The strtol function converts a string to a long integer data type. It takes the starting position as a pointer and also takes another variable which is an ‘out’ parameter that the function will set to where it detected the end of the string. Then in the next use of the function to get the next index, we use that end variable but add on a value that results in either 0 or 1 depending on the (*end == ‘/’) equality test. This is there so that we can skip the ‘/’ character if it exists. The 10 is the last argument which specifies that we are using base 10 which is the base of everyday numbers that we use on a day to day basis.

Finally we need to provide a default case for the types of lines in obj files that we aren’t reading just yet ( we aren’t supporting comments, groups, parameter space vertices, lines or materials).

        default: {
        }
        }
    }

Now we can simply return the mesh data out of the function to the caller. We will revisit this function in later posts and add support for other parts of the obj format.

    return meshData;
}

So we have loaded all of the point data, but its not in the right order, its just a bunch of points. Also those points are going to be reused by multiple triangle so we need to flatten out the points into a list of triangles that OpenGL understands. For that, we have the face/polygon indices that are a list of triangle triple indexes. But because they are a list of indices, we need to loop through them and turn them into into vertices. First we will define a new struct that we can use as a type that we can return from a function that we will write.

struct MeshDataSplit {
    std::vector<vertex3D> vertices;
}

Now we will write a function that will get the raw obj data and process them into triangle vertex data. It will take the filePath as its only argument. (Why not just return a vector<vertex3D> you might ask? We will be adding to this struct in later posts).

MeshDataSplit readObjSplit(const std::string& filePath) {
    auto rawMeshData = readObjRaw(filePath);

    MeshDataSplit meshData;

    meshData.vertices.resize(rawMeshData.faceIndices.size());
    if (rawMeshData.textureCoords.size() == 0) {
        rawMeshData.textureCoords.resize(rawMeshData.faceIndices.size());
    }

    if (rawMeshData.normals.size() == 0) {
        rawMeshData.normals.resize(rawMeshData.faceIndices.size());
    }

#pragma omp parallel for
    for (int i = 0u; i < rawMeshData.faceIndices.size(); ++i) {
        meshData.vertices[i] = {
            rawMeshData.positions[rawMeshData.faceIndices[i].x],
            rawMeshData.normals[rawMeshData.faceIndices[i].z],
            rawMeshData.textureCoords[rawMeshData.faceIndices[i].y]};
    }

    return meshData;
}

Immediately in this function, we call the previous functionreadObjRaw() we just wrote to get the raw mesh data using the file path. Then we create a blank MeshDataSplit which is what we will return out of the function. Then we resize the std::vectors inside the meshData and also the raw mesh data so that the data is reserved and we know that we can index into them. The vertices we will be writing into and the texture coords and normals we will be reading from. The reason we check the size before resizing is that we might have read an obj file that didn’t contain coords or normals so we are resizing so that if they were empty, we get some default values.

Then we can simply loop through the vertices and set their values using the indices stored in the faceIndices triples. It might be a bit hard to see whats going on here, but we are basically looking up the vertex data by index and pulling the data into the right location. Another great benefit of this approach is that because we pre reserved all the data, we can make this step be done in parallel by adding an openmp pragma to the loop. Thats nice!

Now lets go and write our c++ file to be able to load and draw the mesh. A lot of this will be the same as previous chapters, but we will be modifying our vertex binding to be able to read the vertex format (Vertex3D) we have decided to use.

… previous code (click to expand)
#include "error_handling.hpp"
#include "obj_loader_simple.hpp"

#include <array>
#include <chrono>     // current time
#include <cmath>      // sin & cos
#include <cstdlib>    // for std::exit()
#include <fmt/core.h> // for fmt::print(). implements c++20 std::format
#include <unordered_map>

// this is really important to make sure that glbindings does not clash with
// glfw's opengl includes. otherwise we get ambigous overloads.
#define GLFW_INCLUDE_NONE
#include <GLFW/glfw3.h>

#include <glbinding/gl/gl.h>
#include <glbinding/glbinding.h>

#include <glbinding-aux/debug.h>

#include "glm/glm.hpp"
#include <glm/gtc/matrix_transform.hpp>
#include <glm/gtc/type_ptr.hpp>

using namespace gl;
using namespace std::chrono;

int main() {

    auto startTime = system_clock::now();

    const int width = 900;
    const int height = 900;

    auto windowPtr = [&]() {
        if (!glfwInit()) {
            fmt::print("glfw didnt initialize!\n");
            std::exit(EXIT_FAILURE);
        }

        glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 4);
        glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 5);

        /* Create a windowed mode window and its OpenGL context */
        auto windowPtr = glfwCreateWindow(width, height,
                                          "Chapter 11 - Loading Data from Disk",
                                          nullptr, nullptr);

        if (!windowPtr) {
            fmt::print("window doesn't exist\n");
            glfwTerminate();
            std::exit(EXIT_FAILURE);
        }
        glfwSetWindowPos(windowPtr, 480, 90);

        glfwMakeContextCurrent(windowPtr);
        glbinding::initialize(glfwGetProcAddress, false);
        return windowPtr;
    }();

    // debugging
    {
        glEnable(GL_DEBUG_OUTPUT);
        glDebugMessageCallback(errorHandler::MessageCallback, 0);
        glEnable(GL_DEBUG_OUTPUT_SYNCHRONOUS);
        glDebugMessageControl(GL_DEBUG_SOURCE_API, GL_DEBUG_TYPE_OTHER,
                              GL_DEBUG_SEVERITY_NOTIFICATION, 0, nullptr,
                              false);
    }

Previously we used lambdas to create the vertex and fragment shaders and combine them into a file. I’ve now done a similar thing, except I’ve taken the shader text creation out of the lambda and made them arguments to the lambda function. The lambda now takes two strings which are the source text for the vertex and fragment shaders. This makes the lambda reusable.

    auto createProgram = [](const char* vertexShaderSource,
                            const char* fragmentShaderSource) -> GLuint {
        auto vertexShader = glCreateShader(GL_VERTEX_SHADER);
        glShaderSource(vertexShader, 1, &vertexShaderSource, nullptr);
        glCompileShader(vertexShader);
        errorHandler::checkShader(vertexShader, "Vertex");

        auto fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
        glShaderSource(fragmentShader, 1, &fragmentShaderSource, nullptr);
        glCompileShader(fragmentShader);
        errorHandler::checkShader(fragmentShader, "Fragment");

        auto program = glCreateProgram();
        glAttachShader(program, vertexShader);
        glAttachShader(program, fragmentShader);

        glLinkProgram(program);
        return program;
    };

Next, we are going to be using the same fragment shader for drawing the gradient background and also for the mesh drawing. It is a simple shader that simply takes a colour attribute as input and outputs a pixel value of that interpolated vertex colour….

    const char* fragmentShaderSource = R"(
            #version 450 core

            in vec3 colour;
            out vec4 finalColor;

            void main() {
                finalColor = vec4(colour, 1.0);
            }
        )";

Then, we are going to use our lambda and as the first argument for the vertex shader, pass it an in place string which is the same that we have been using for the gradiwent background. Notice we pass in the fragmentShaderSource variable as the second argument.

    auto programBG = createProgram(R"(
        #version 450 core

        out vec3 colour;

        const vec4 vertices[] = vec4[]( vec4(-1.f, -1.f, 0.0, 1.0),
                                        vec4( 3.f, -1.f, 0.0, 1.0),    
                                        vec4(-1.f,  3.f, 0.0, 1.0));   
        const vec3 colours[]   = vec3[](vec3(0.12f, 0.14f, 0.16f),
                                        vec3(0.12f, 0.14f, 0.16f),
                                        vec3(0.80f, 0.80f, 0.82f));
        

        void main(){
            colour = colours[gl_VertexID];
            gl_Position = vertices[gl_VertexID];  
        }
    )",
                                   fragmentShaderSource);

For our mesh drawing shader program, we want to have a slightly different vertex shader. This is going to take in the attributes from our vertex buffers and interpret the normals as colours and pass them to the fragment shader. The reason we do this is because this shader does a remap operation that shifts the colours from a range of (-1 -> 1) to (0 ->1). This makes the normals easier to visualize. Also see that we are reusing the fragment shader source. This is a powerful feature, being able to reuse shader stages in different shader programs.

    auto program = createProgram(R"(
            #version 450 core
            layout (location = 0) in vec3 position;
            layout (location = 1) in vec3 normal;

            out vec3 colour;

            vec3 remappedColour = (normal + vec3(1.f)) / 2.f;

            void main(){
                colour = remappedColour;
                gl_Position = vec4((position * vec3(1.0f, 1.0f, -1.0f)) +
                                   (vec3(0, -0.5, 0)), 1.0f);
            }
        )",
                                 fragmentShaderSource);

Finally we get to use our obj loader now! Lets read an obj file into our program!

    auto meshData = objLoader::readObjSplit("rubberToy.obj");

That was easy wasn’t it?! We now have a meshData variable which has our vertices in a std::vector ready to use directly. Now lets get that data into a buffer…

This block of code is another one of our creation lambdas that does something for us and returns the result.

    auto createBuffer =
        [&program](const std::vector<vertex3D>& vertices) -> GLuint {
        GLuint bufferObject;
        glCreateBuffers(1, &bufferObject);

        // upload immediately
        glNamedBufferStorage(bufferObject, vertices.size() * sizeof(vertex3D),
                             vertices.data(),
                             GL_MAP_WRITE_BIT | GL_DYNAMIC_STORAGE_BIT);

        return bufferObject;
    };

    auto meshBuffer = createBuffer(meshData.vertices);

In this case, it takes in a vector of our vertices and sends the data from it into a buffer on the GPU.

Next we need to create a vertex array object and set up our binding mapping that will set up how the shader will read and interpret the data from the buffer. This doesn’t actually connect the buffer with the vaertex array object just yet (we will do that next), it just sets up the mapping.

    auto createVertexArrayObject = [](GLuint program) -> GLuint {
        GLuint vao;
        glCreateVertexArrays(1, &vao);

        glEnableVertexArrayAttrib(vao, 0);
        glEnableVertexArrayAttrib(vao, 1);

        glVertexArrayAttribBinding(vao,
                                   glGetAttribLocation(program, "position"),
                                   /*buffer index*/ 0);
        glVertexArrayAttribBinding(vao, glGetAttribLocation(program, "normal"),
                                   /*buffer index*/ 0);

        glVertexArrayAttribFormat(vao, 0, glm::vec3::length(), GL_FLOAT,
                                  GL_FALSE, offsetof(vertex3D, position));
        glVertexArrayAttribFormat(vao, 1, glm::vec3::length(), GL_FLOAT,
                                  GL_FALSE, offsetof(vertex3D, normal));

        return vao;
    };

    auto meshVao = createVertexArrayObject(program);

What this does is…

  • enables 2 attributes with the call to glEnableVertexArrayAttrib()
  • tells the vertex array object that the two attribute locations will be getting their data from a single buffer at a buffer index of 0 with glVertexArrayAttribBinding()
  • Lets the vao know what the format of each attribute is at attribute locations 0 and 1 and tells it where in each vertex the data is as a byte offset with glVertexArrayAttribFormat()
For a reminder of how to set up vertex attribute binding please see chapters 5 & 6. It’s easy to forget this stuff and I find I often need a refresher…
https://dokipen.com/modern-opengl-part-5-feeding-vertex-data-to-shaders/
https://dokipen.com/modern-opengl-part-6-multiple-vertex-buffers/
    glVertexArrayVertexBuffer(meshVao, 0, meshBuffer,
                              /*offset*/ 0,
                              /*stride*/ sizeof(vertex3D));

This is the function that tells opengl how to associate the vertex array object with actual buffer data. Remember we could keep using the vertex array object that matches our vertex format ands shader program attribute binding and use this to switch out to a completely different buffer. (as a side note, it’s even possible to use a shader program and switch out a whole vertex array object and buffer setup that uses completely different vertex formats and layout. Thats outside of the scope of this series though).

    glBindVertexArray(meshVao);

    glEnable(GL_DEPTH_TEST);

    std::array<GLfloat, 4> clearColour{0.f, 0.f, 0.f, 1.f};
    GLfloat clearDepth{1.0f};

    while (!glfwWindowShouldClose(windowPtr)) {

        glClearBufferfv(GL_DEPTH, 0, &clearDepth);

        glUseProgram(programBG);
        glDrawArrays(GL_TRIANGLES, 0, 3);

        glUseProgram(program);
        glDrawArrays(GL_TRIANGLES, 0, (gl::GLsizei)meshData.vertices.size());

        glfwSwapBuffers(windowPtr);
        glfwPollEvents();
    }

    glfwTerminate();
}

All we do now is bind our vao as its the only one we will be using, and do our rendering as usual. In between draw calls, we simply switch the shader program used and draw a different number of triangles each time. And we are done! I hope that was fun for you.

In the next chapter, we will be learning about shader transforms so that we can start to draw our meshes in perspective views and move them around the screen!

Modern OpenGL: Part 10 Sending Uniform Parameters to Shaders

So far, our shader programs that we have been using in previous posts, have been static. By that, I mean that you compile them and all of the variables in the shader are fixed and never change (The only thing that can change is the vertex data itself). In this part on our journey of learning opengl, we are going to be controlling some parameters in the shader program so that we can change values over time by sending them into the shader from our c++ code, every frame. The are basically variables that we can tweak, and they are very fast and easy to change as opposed to having to change vertex data.

The way we do this is by using “uniform parameters”. These are variables in the shader that are exposed to the external c++ code and can be set dynamically. The reason they are called uniform, is because they have the same value across all invocations of the shader on all of your vertices/fragments.

There isn’t much we actually need to do for this to work. First lets deal with what we have to do in our shader. Previously we have some shader variables which were just set in the program itself to a hard coded value. Now we need to declare them as uniform. As we just said before, the word uniform itself describes how the value is going to be the same for all vertices/fragments that the shader program operates on. Instead of varying (which means different per vertex/fragment, like attributes), it is the same for each vertex/fragment invocation.

… previous code (click to expand)
#include "error_handling.hpp"
#include <array>
#include <chrono>     // current time
#include <cmath>      // sin & cos
#include <cstdlib>    // for std::exit()
#include <fmt/core.h> // for fmt::print(). implements c++20 std::format
#include <unordered_map>

// this is really important to make sure that glbindings does not clash with
// glfw's opengl includes. otherwise we get ambigous overloads.
#define GLFW_INCLUDE_NONE
#include <GLFW/glfw3.h>

#include <glbinding/gl/gl.h>
#include <glbinding/glbinding.h>

#include <glbinding-aux/debug.h>

#include "glm/glm.hpp"

using namespace gl;
using namespace std::chrono;

int main() {

    auto startTime = system_clock::now();
    const int width = 1280;
    const int height = 720;

    auto windowPtr = [](int w, int h) {
        if (!glfwInit()) {
            fmt::print("glfw didnt initialize!\n");
            std::exit(EXIT_FAILURE);
        }

        glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 4);
        glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 6);

        /* Create a windowed mode window and its OpenGL context */
        auto windowPtr = glfwCreateWindow(w, h, "Chapter 9 - Full Screen Effects (Diy Shadertoy!)",
                                       nullptr, nullptr);

        if (!windowPtr) {
            fmt::print("window doesn't exist\n");
            glfwTerminate();
            std::exit(EXIT_FAILURE);
        }
        glfwSetWindowPos(windowPtr, 520, 180);

        glfwMakeContextCurrent(windowPtr);
        glbinding::initialize(glfwGetProcAddress, false);
        return windowPtr;
    }(width, height);
    // debugging
    {
        glEnable(GL_DEBUG_OUTPUT);
        glDebugMessageCallback(errorHandler::MessageCallback, 0);
        glEnable(GL_DEBUG_OUTPUT_SYNCHRONOUS);
        glDebugMessageControl(GL_DEBUG_SOURCE_API, GL_DEBUG_TYPE_OTHER,
                              GL_DEBUG_SEVERITY_NOTIFICATION, 0, nullptr, false);
    }

    auto createProgram = [](const char* vertexShaderSource,
                            const char* fragmentShaderSource) -> GLuint {
        auto vertexShader = glCreateShader(GL_VERTEX_SHADER);
        glShaderSource(vertexShader, 1, &vertexShaderSource, nullptr);
        glCompileShader(vertexShader);
        errorHandler::checkShader(vertexShader, "Vertex");

        auto fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
        glShaderSource(fragmentShader, 1, &fragmentShaderSource, nullptr);
        glCompileShader(fragmentShader);
        errorHandler::checkShader(fragmentShader, "Fragment");

        auto program = glCreateProgram();
        glAttachShader(program, vertexShader);
        glAttachShader(program, fragmentShader);

        glLinkProgram(program);
        return program;
    };

    auto program = createProgram(R"(
            #version 450 core

            const vec4 vertices[] = vec4[]( vec4(-1.f, -1.f, 0.0, 1.0),
                                        vec4( 3.f, -1.f, 0.0, 1.0),    
                                        vec4(-1.f,  3.f, 0.0, 1.0));    

            void main(){
                gl_Position = vertices[gl_VertexID]; 
            }
        )",
                                 R"(
        #version 450 core

        // The MIT License
        // Copyright © 2013 Inigo Quilez
        // Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
        // associated documentation files (the "Software"), to deal in the Software without restriction,
        // including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense,
        // and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so,
        // subject to the following conditions: The above copyright notice and this permission notice shall be
        // included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS",
        // WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
        // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
        // COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
        // TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
        // IN THE SOFTWARE.
        //
        // I've not seen anybody out there computing correct cell interior distances for Voronoi
        // patterns yet. That's why they cannot shade the cell interior correctly, and why you've
        // never seen cell boundaries rendered correctly.
        //
        // However, here's how you do mathematically correct distances (note the equidistant and non
        // degenerated grey isolines inside the cells) and hence edges (in yellow):
        //
        // http://www.iquilezles.org/www/articles/voronoilines/voronoilines.htm
        //
        // More Voronoi shaders:
        //
        // Exact edges:  https://www.shadertoy.com/view/ldl3W8
        // Hierarchical: https://www.shadertoy.com/view/Xll3zX
        // Smooth:       https://www.shadertoy.com/view/ldB3zc
        // Voronoise:    https://www.shadertoy.com/view/Xd23Dh
   
        out vec4 fragColor;
        vec4 fragCoord  = gl_FragCoord;
        vec2 iMouse = vec2(960.f,0.f);



        

What we have done here is that we have created the same declarations of the variables that the shader program requires (that were previously provided by shadertoy behind the scenes) but now we want to change two of them (iTime and iResolution) to be exposed to and controlled by our c++ code. Here is how we do that…

        uniform float iTime;
        uniform vec2 iResolution;
        

We have declared them as uniform variables. This is pretty much like declaring any other variable, but it has the “uniform” specifier in front of it. Now when the shader gets compiled, it will take that into account and provide a way for us to set the value of that variable any time we want.

… previous code (click to expand)
        mat2 rot(in float a){float c = cos(a), s = sin(a);return mat2(c,s,-s,c);}
        const mat3 m3 = mat3(0.33338, 0.56034, -0.71817, -0.87887, 0.32651, -0.15323, 0.15162, 0.69596, 0.61339)*1.93;
        float mag2(vec2 p){return dot(p,p);}
        float linstep(in float mn, in float mx, in float x){ return clamp((x - mn)/(mx - mn), 0., 1.); }
        float prm1 = 0.;
        vec2 bsMo = vec2(0);

        vec2 disp(float t){ return vec2(sin(t*0.22)*1., cos(t*0.175)*1.)*2.; }

        vec2 map(vec3 p)
        {
            vec3 p2 = p;
            p2.xy -= disp(p.z).xy;
            p.xy *= rot(sin(p.z+iTime)*(0.1 + prm1*0.05) + iTime*0.09);
            float cl = mag2(p2.xy);
            float d = 0.;
            p *= .61;
            float z = 1.;
            float trk = 1.;
            float dspAmp = 0.1 + prm1*0.2;
            for(int i = 0; i < 5; i++)
            {
                p += sin(p.zxy*0.75*trk + iTime*trk*.8)*dspAmp;
                d -= abs(dot(cos(p), sin(p.yzx))*z);
                z *= 0.57;
                trk *= 1.4;
                p = p*m3;
            }
            d = abs(d + prm1*3.)+ prm1*.3 - 2.5 + bsMo.y;
            return vec2(d + cl*.2 + 0.25, cl);
        }

        vec4 render( in vec3 ro, in vec3 rd, float time )
        {
            vec4 rez = vec4(0);
            const float ldst = 8.;
            vec3 lpos = vec3(disp(time + ldst)*0.5, time + ldst);
            float t = 1.5;
            float fogT = 0.;
            for(int i=0; i<130; i++)
            {
                if(rez.a > 0.99)break;

                vec3 pos = ro + t*rd;
                vec2 mpv = map(pos);
                float den = clamp(mpv.x-0.3,0.,1.)*1.12;
                float dn = clamp((mpv.x + 2.),0.,3.);
                
                vec4 col = vec4(0);
                if (mpv.x > 0.6)
                {
                
                    col = vec4(sin(vec3(5.,0.4,0.2) + mpv.y*0.1 +sin(pos.z*0.4)*0.5 + 1.8)*0.5 + 0.5,0.08);
                    col *= den*den*den;
                    col.rgb *= linstep(4.,-2.5, mpv.x)*2.3;
                    float dif =  clamp((den - map(pos+.8).x)/9., 0.001, 1. );
                    dif += clamp((den - map(pos+.35).x)/2.5, 0.001, 1. );
                    col.xyz *= den*(vec3(0.005,.045,.075) + 1.5*vec3(0.033,0.07,0.03)*dif);
                }
                
                float fogC = exp(t*0.2 - 2.2);
                col.rgba += vec4(0.06,0.11,0.11, 0.1)*clamp(fogC-fogT, 0., 1.);
                fogT = fogC;
                rez = rez + col*(1. - rez.a);
                t += clamp(0.5 - dn*dn*.05, 0.09, 0.3);
            }
            return clamp(rez, 0.0, 1.0);
        }

        float getsat(vec3 c)
        {
            float mi = min(min(c.x, c.y), c.z);
            float ma = max(max(c.x, c.y), c.z);
            return (ma - mi)/(ma+ 1e-7);
        }

        //from my "Will it blend" shader (https://www.shadertoy.com/view/lsdGzN)
        vec3 iLerp(in vec3 a, in vec3 b, in float x)
        {
            vec3 ic = mix(a, b, x) + vec3(1e-6,0.,0.);
            float sd = abs(getsat(ic) - mix(getsat(a), getsat(b), x));
            vec3 dir = normalize(vec3(2.*ic.x - ic.y - ic.z, 2.*ic.y - ic.x - ic.z, 2.*ic.z - ic.y - ic.x));
            float lgt = dot(vec3(1.0), ic);
            float ff = dot(dir, normalize(ic));
            ic += 1.5*dir*sd*ff*lgt;
            return clamp(ic,0.,1.);
        }

        void main() 
        {	
            vec2 q = fragCoord.xy/iResolution.xy;
            vec2 p = (gl_FragCoord.xy - 0.5*iResolution.xy)/iResolution.y;
            bsMo = (iMouse.xy - 0.5*iResolution.xy)/iResolution.y;
            
            float time = iTime*3.;
            vec3 ro = vec3(0,0,time);
            
            ro += vec3(sin(iTime)*0.5,sin(iTime*1.)*0.,0);
                
            float dspAmp = .85;
            ro.xy += disp(ro.z)*dspAmp;
            float tgtDst = 3.5;
            
            vec3 target = normalize(ro - vec3(disp(time + tgtDst)*dspAmp, time + tgtDst));
            ro.x -= bsMo.x*2.;
            vec3 rightdir = normalize(cross(target, vec3(0,1,0)));
            vec3 updir = normalize(cross(rightdir, target));
            rightdir = normalize(cross(updir, target));
            vec3 rd=normalize((p.x*rightdir + p.y*updir)*1. - target);
            rd.xy *= rot(-disp(time + 3.5).x*0.2 + bsMo.x);
            prm1 = smoothstep(-0.4, 0.4,sin(iTime*0.3));
            vec4 scn = render(ro, rd, time);
                
            vec3 col = scn.rgb;
            col = iLerp(col.bgr, col.rgb, clamp(1.-prm1,0.05,1.));
            
            col = pow(col, vec3(.55,0.65,0.6))*vec3(1.,.97,.9);

            col *= pow( 16.0*q.x*q.y*(1.0-q.x)*(1.0-q.y), 0.12)*0.7+0.3; //Vign
            
            fragColor = vec4( col, 1.0 );
        }
    )");



    GLuint vao;
    glCreateVertexArrays(1, &vao);
    glBindVertexArray(vao);

    glUseProgram(program);

    int timeUniformLocation = glGetUniformLocation(program, "iTime");
    int resolutionUniformLocation = glGetUniformLocation(program, "iResolution");

Now in our c++ code, we need a way to be able to refer to the location of that variable from the shader. This is done with the glGetUniformLocation function call. We ask the shader “program” where it’s “iTime” variable is and OpenGL will return us back an integer location that will be something like 0, 1, 2 etc depending on what OpenGL itself decided it would be. It doesn’t actually really matter what it is, because we are storing it as a variable and we never need to know what the actual value is itself. We just need to know that we can use it later on to set the value of the variable in the shader code using that integer ‘location’.

Previously, we used binding locations inside of shaders as a way to refer to the location inside of a shader, however I thought this was a good way to show you an alternative. This is often viewed as an older way of doing things, but I like it and still think it has its uses. So you can decide for yourself which method you want to use.

So now that we have that location, then we can use it to set the value of the shader variable at that location. We do that with a family of functions that all start with

glProgramUniform* (the star/wildcard here states that there is some text missing that dictates the type we want to change).

So for example, if we want to change the uniform variable that has a type in the shader that is a vec2, then we want to use the glProgramUniform2f function. And for a uniform variable that is a single ‘float’, then we want to use glProgramUniform1f.

    glProgramUniform2f(program, resolutionUniformLocation, width, height);


    while (!glfwWindowShouldClose(windowPtr)) {

        auto currentTime = duration<float>(system_clock::now() - startTime).count();
        glProgramUniform1f(program, timeUniformLocation, currentTime);

        // draw full screen triangle
        glDrawArrays(GL_TRIANGLES, 0, 3);

        glfwSwapBuffers(windowPtr);
        glfwPollEvents();
    }

    glfwTerminate();
}

Here, just outside our main loop, we are setting the shader variable using the location “resolutionUniformLocation” that we previously got from asking where the “iResolution” variable was.= in the shader. We pass the two float values in as arguments to the function and they will get passed into the shader for us. This function is being called outside the loop because we only really want to set it once. But it still needs to be called because the resolution might have been set at program start up based on some user input (like command line parameters) and we still need to tell the shader what that is. If the window was changed size and we had written a function to handle that (which glfw allows up to do, although we haven’t explored that yet), then we would be able to update the shader accordingly.

Then we are calling the next glProgramUniform1f function inside the loop. This is because we want to update the value of the “iTime” in the shader with the current value from the runtime of the program. It is “1f” because it is a single float and this is something that is realtime and needs to update every frame.

That is pretty much it then for uniform variables. In the next chapter we will be tackling the ultimate graphics programming rite of passage… writing an obj loader from scratch to be able to load meshes into our programs and have them rendered on the screen!

Modern OpenGL: Part 9 Full Screen Effects for a DIY Shadertoy Project

We are going to be taking a couple of chapters to step away from the mechanical process of just managing the feeding of vertex data into shaders and have a bit of fun by rendering some full screen shader effects.

The idea for this comes from the Shadertoy website that hosts shaders that render interesting effects and scenes using just fragment shaders and does it in the web browser! Really what is happening is that a full screen triangle is being drawn (very much like the background gradient triangle that we drew in the previous chapter) and then the fragment program is being run for every pixel in the screen. Each invocation of the fragment program determines the colour of the pixel, so some very interesting and complex effects can be achieved with some fancy math.

For our program that we are compiling locally on our own machine, all we have to do really is draw a triangle, and assign a fragment shader that contains the code from a shadertoy example and we should get the same results.

We aren’t going to be learning about the specific shadertoy example and the maths behind it in this chapter. We will save that for a future time. We are instead going to be focusing on how we can use what we have already learnt to do something that is fun.

This is the shadertoy example we are going to be running in our program for this example. The basic idea though is that it should technically be possible to get any glsl code and run it in our program wherever it comes from.

https://www.shadertoy.com/view/3l23Rh#

We can’t copy and paste the shader code directly though. There are certain variables that the code references that are needed to be set for it to work. When the shader is running in the browser, the shadertoy page code is responsible for making sure that those variables are present for the program to run. We have to do that manually in this case as well. We will cover that when we get to that point in the shader.

… previous code (click to expand)
#include "error_handling.hpp"
#include <array>
#include <chrono>     // current time
#include <cmath>      // sin & cos
#include <cstdlib>    // for std::exit()
#include <fmt/core.h> // for fmt::print(). implements c++20 std::format
#include <unordered_map>

// this is really important to make sure that glbindings does not clash with
// glfw's opengl includes. otherwise we get ambigous overloads.
#define GLFW_INCLUDE_NONE
#include <GLFW/glfw3.h>

#include <glbinding/gl/gl.h>
#include <glbinding/glbinding.h>

#include <glbinding-aux/debug.h>

#include "glm/glm.hpp"

using namespace gl;
using namespace std::chrono;

int main() {

    auto startTime = system_clock::now();
    const int width = 1600;
    const int height = 900;

    auto windowPtr = [&]() {
        if (!glfwInit()) {
            fmt::print("glfw didnt initialize!\n");
            std::exit(EXIT_FAILURE);
        }

        glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 4);
        glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 5);

        /* Create a windowed mode window and its OpenGL context */
        auto windowPtr = glfwCreateWindow(width, height, "Chapter 9 - Full Screen Effects (Diy Shadertoy!)",
                                       nullptr, nullptr);

        if (!windowPtr) {
            fmt::print("window doesn't exist\n");
            glfwTerminate();
            std::exit(EXIT_FAILURE);
        }
        glfwSetWindowPos(windowPtr, 520, 180);
        glfwMakeContextCurrent(windowPtr);
        glbinding::initialize(glfwGetProcAddress, false);
        return windowPtr;
    }();

    // debugging
    {
        glEnable(GL_DEBUG_OUTPUT);
        glDebugMessageCallback(errorHandler::MessageCallback, 0);
        glEnable(GL_DEBUG_OUTPUT_SYNCHRONOUS);
        glDebugMessageControl(GL_DEBUG_SOURCE_API, GL_DEBUG_TYPE_OTHER,
                              GL_DEBUG_SEVERITY_NOTIFICATION, 0, nullptr, false);
    }

    auto createProgram = [](const char* vertexShaderSource,
                            const char* fragmentShaderSource) -> GLuint {
        auto vertexShader = glCreateShader(GL_VERTEX_SHADER);
        glShaderSource(vertexShader, 1, &vertexShaderSource, nullptr);
        glCompileShader(vertexShader);
        errorHandler::checkShader(vertexShader, "Vertex");

        auto fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
        glShaderSource(fragmentShader, 1, &fragmentShaderSource, nullptr);
        glCompileShader(fragmentShader);
        errorHandler::checkShader(fragmentShader, "Fragment");

        auto program = glCreateProgram();
        glAttachShader(program, vertexShader);
        glAttachShader(program, fragmentShader);

        glLinkProgram(program);
        return program;
    };

auto program = createProgram(R"(
        #version 450 core

        const vec4 vertices[] = vec4[]( vec4(-1.f, -1.f, 0.0, 1.0),
                                        vec4( 3.f, -1.f, 0.0, 1.0),    
                                        vec4(-1.f,  3.f, 0.0, 1.0));    

        void main(){
            gl_Position = vertices[gl_VertexID]; 
        }
    )",

Here we are starting to call our createProgram() function that we created in the earlier chapters and are passing it the vertex shader string. This is the same as we saw in chapter 8 where we drew a fullscreen triangle (we are storing the positions of 3 vertices which we will draw with a call to DrawTriangles() with a count of 3).

The next argument to the createProgram function is our fragment shader string. This is the code that we have copied from shader toy.

 
                                R"(
            
        // Protean clouds by nimitz (twitter: @stormoid)
    // https://www.shadertoy.com/view/3l23Rh
    // License Creative Commons Attribution-NonCommercial-ShareAlike 3.0 Unported License
    // Contact the author for other licensing options

    /*
        Technical details:

        The main volume noise is generated from a deformed periodic grid, which can produce
        a large range of noise-like patterns at very cheap evalutation cost. Allowing for multiple
        fetches of volume gradient computation for improved lighting.

        To further accelerate marching, since the volume is smooth, more than half the the density
        information isn't used to rendering or shading but only as an underlying volume	distance to 
        determine dynamic step size, by carefully selecting an equation	(polynomial for speed) to 
        step as a function of overall density (not necessarialy rendered) the visual results can be 
        the	same as a naive implementation with ~40% increase in rendering performance.

        Since the dynamic marching step size is even less uniform due to steps not being rendered at all
        the fog is evaluated as the difference of the fog integral at each rendered step.

    */

Make sure to adhere to the license of any code you use!

Next there are some things that we need to add to the shader to at this point make it work.

First we have to declare an output attribute in the fragment shader that will be the location that the fragment colour will be written to.

    
    out vec4 fragColor;

Shadertoy also provides a few built in variables that an author of a shader is able to use to figure out things like

  • what the current elapsed time is since the rendering began
  • what the resolution of the shader screen/window is
  • where the mouse is clicking

We have to provide those to the shader, otherwise those variables wont be defined and it wont compile. In this case we have to make sure that the value of the resolution matches our actual resolution of our opengl window. (we will see in the next chanpter how we can ‘tell’ the shader what the resolution is without having to explicitly set in the code).

    
    float iTime = 1.14f;
    vec2 iResolution = vec2(1600, 900);
    vec2 iMouse = vec2(960.f,0.f);

Shadertoy also provides a variable called fragCoord, which is a renamed version of gl_FragCoord. This is a GLSL built in variable that tells you what the current pixel’s screen coordinate is in terms of the resolution (its a vec4, but we are interested in only the first two components). This can normally be used as it but for some reason, this shader uses another name for it. So we could go in and fix that bit in the code, but its just as easy to add this at the top…

    
    vec4 fragCoord  = gl_FragCoord;

Now this is the rest of the fragment shader…

    mat2 rot(in float a){float c = cos(a), s = sin(a);return mat2(c,s,-s,c);}
    const mat3 m3 = mat3(0.33338, 0.56034, -0.71817, -0.87887, 0.32651, -0.15323, 0.15162, 0.69596, 0.61339)*1.93;
    float mag2(vec2 p){return dot(p,p);}
    float linstep(in float mn, in float mx, in float x){ return clamp((x - mn)/(mx - mn), 0., 1.); }
    float prm1 = 0.;
    vec2 bsMo = vec2(0);

    vec2 disp(float t){ return vec2(sin(t*0.22)*1., cos(t*0.175)*1.)*2.; }

    vec2 map(vec3 p)
    {
        vec3 p2 = p;
        p2.xy -= disp(p.z).xy;
        p.xy *= rot(sin(p.z+iTime)*(0.1 + prm1*0.05) + iTime*0.09);
        float cl = mag2(p2.xy);
        float d = 0.;
        p *= .61;
        float z = 1.;
        float trk = 1.;
        float dspAmp = 0.1 + prm1*0.2;
        for(int i = 0; i < 5; i++)
        {
            p += sin(p.zxy*0.75*trk + iTime*trk*.8)*dspAmp;
            d -= abs(dot(cos(p), sin(p.yzx))*z);
            z *= 0.57;
            trk *= 1.4;
            p = p*m3;
        }
        d = abs(d + prm1*3.)+ prm1*.3 - 2.5 + bsMo.y;
        return vec2(d + cl*.2 + 0.25, cl);
    }

    vec4 render( in vec3 ro, in vec3 rd, float time )
    {
        vec4 rez = vec4(0);
        const float ldst = 8.;
        vec3 lpos = vec3(disp(time + ldst)*0.5, time + ldst);
        float t = 1.5;
        float fogT = 0.;
        for(int i=0; i<130; i++)
        {
            if(rez.a > 0.99)break;

            vec3 pos = ro + t*rd;
            vec2 mpv = map(pos);
            float den = clamp(mpv.x-0.3,0.,1.)*1.12;
            float dn = clamp((mpv.x + 2.),0.,3.);
            
            vec4 col = vec4(0);
            if (mpv.x > 0.6)
            {
            
                col = vec4(sin(vec3(5.,0.4,0.2) + mpv.y*0.1 +sin(pos.z*0.4)*0.5 + 1.8)*0.5 + 0.5,0.08);
                col *= den*den*den;
                col.rgb *= linstep(4.,-2.5, mpv.x)*2.3;
                float dif =  clamp((den - map(pos+.8).x)/9., 0.001, 1. );
                dif += clamp((den - map(pos+.35).x)/2.5, 0.001, 1. );
                col.xyz *= den*(vec3(0.005,.045,.075) + 1.5*vec3(0.033,0.07,0.03)*dif);
            }
            
            float fogC = exp(t*0.2 - 2.2);
            col.rgba += vec4(0.06,0.11,0.11, 0.1)*clamp(fogC-fogT, 0., 1.);
            fogT = fogC;
            rez = rez + col*(1. - rez.a);
            t += clamp(0.5 - dn*dn*.05, 0.09, 0.3);
        }
        return clamp(rez, 0.0, 1.0);
    }

    float getsat(vec3 c)
    {
        float mi = min(min(c.x, c.y), c.z);
        float ma = max(max(c.x, c.y), c.z);
        return (ma - mi)/(ma+ 1e-7);
    }

    //from my "Will it blend" shader (https://www.shadertoy.com/view/lsdGzN)
    vec3 iLerp(in vec3 a, in vec3 b, in float x)
    {
        vec3 ic = mix(a, b, x) + vec3(1e-6,0.,0.);
        float sd = abs(getsat(ic) - mix(getsat(a), getsat(b), x));
        vec3 dir = normalize(vec3(2.*ic.x - ic.y - ic.z, 2.*ic.y - ic.x - ic.z, 2.*ic.z - ic.y - ic.x));
        float lgt = dot(vec3(1.0), ic);
        float ff = dot(dir, normalize(ic));
        ic += 1.5*dir*sd*ff*lgt;
        return clamp(ic,0.,1.);
    }

One other thing we have to do in our copy of the shaderytoy example is to change the name of the mainImage function in line 112 of the original source to main(). This is because opengl expects a main() function as its entry point.

    void main() // previously void mainImage( out vec4 fragColor, in vec2 fragCoord )
    {	


Now we paste in the rest of the main function from shadertoy…

        vec2 q = fragCoord.xy/iResolution.xy;
        vec2 p = (gl_FragCoord.xy - 0.5*iResolution.xy)/iResolution.y;
        bsMo = (iMouse.xy - 0.5*iResolution.xy)/iResolution.y;
        
        float time = iTime*3.;
        vec3 ro = vec3(0,0,time);
        
        ro += vec3(sin(iTime)*0.5,sin(iTime*1.)*0.,0);
            
        float dspAmp = .85;
        ro.xy += disp(ro.z)*dspAmp;
        float tgtDst = 3.5;
        
        vec3 target = normalize(ro - vec3(disp(time + tgtDst)*dspAmp, time + tgtDst));
        ro.x -= bsMo.x*2.;
        vec3 rightdir = normalize(cross(target, vec3(0,1,0)));
        vec3 updir = normalize(cross(rightdir, target));
        rightdir = normalize(cross(updir, target));
        vec3 rd=normalize((p.x*rightdir + p.y*updir)*1. - target);
        rd.xy *= rot(-disp(time + 3.5).x*0.2 + bsMo.x);
        prm1 = smoothstep(-0.4, 0.4,sin(iTime*0.3));
        vec4 scn = render(ro, rd, time);
            
        vec3 col = scn.rgb;
        col = iLerp(col.bgr, col.rgb, clamp(1.-prm1,0.05,1.));
        
        col = pow(col, vec3(.55,0.65,0.6))*vec3(1.,.97,.9);

        col *= pow( 16.0*q.x*q.y*(1.0-q.x)*(1.0-q.y), 0.12)*0.7+0.3; //Vign
        
        fragColor = vec4( col, 1.0 );
    }
    )");
It’s personally reasonable to want to have this shader code be a text file on disk and load it when we run our c++ program. That is definitely a useful way to go about it and if you are writing your own application, then that might be a way to go if you are editing and changing the vertex/fragment shaders often and don’t want to recompile your c++ program every time.

Right now if we change the shader code, we have to recompile our c++ program to embed the shader code into the c++ binary. Not a very efficient way of working! But this is for educational purposes, so we shall keep it like this to improve readability. I find if I can make code as linear as possible to read, then the learning isn’t as clouded as you don’t have to go and jump around too much.
   
    glUseProgram(program);

    GLuint vao;
    glCreateVertexArrays(1, &vao);
    glBindVertexArray(vao);

    while (!glfwWindowShouldClose(windowPtr)) {

        // draw full screen triangle
        glDrawArrays(GL_TRIANGLES, 0, 3);

        glfwSwapBuffers(windowPtr);
        glfwPollEvents();
    }

    glfwTerminate();
}

This last piece of the code is just our standard render loop. You can see its even simpler now as we don’t have to clear the screen (we know the triangle is going to be drawing over every pixel).

Isn’t that just a lovely thing to see? Next chapter, we will learn how to communicate values from our c++ program into our shader program thought the use of “Uniform” Shader Variables.

Modern OpenGL: Part 8 Switching Multiple Buffer Objects to draw a Gradient Background

In this chapter we will be seeing how we can use multiple buffers that contain different meshes and switch between them. The use case in this example is that we want to draw a gradient background in our window. This also introduces a new technique for drawing a full screen mesh that will be useful for the next chapter (where we implement a shadertoy-like GLSL shader viewer).

I’m going to preface this chapter with the advice that you should try and bunch together as many of your meshes together into a single buffer as possible. Even if they are different meshes, the vertices can still live together in the same buffer. At the moment, you might be wondering how we can do that and still draw objects that have different textures and shaders etc. Well there are techniques (which we will cover in future chapters) that show you how to do this. The basic idea is that you can store the vertices in one big buffer, but there’s nothing saying you have to render all of them in that buffer. You can draw a subrange quite easily).

However for now, it might be that you absolutely need to have two separate buffers and want to switch between them. Well we have seen that VAOs are the mechanism which marshall the data from buffers to shaders. There are two ways to switch buffers….

  1. call glVertexArrayVertexBuffer() between draw calls to change the buffer object on the currently bound VAO. This is only possible if the vertex format is the same across buffers. If the vertex format/layout changes, then this wont work. Of course you could also call glVertexArrayAttribFormat() and glVertexArrayAttribBinding() on the currently bound VAO, but this kind of defeats the purpose of the VAO which seems to be a wrapper around having to call these functions multiple times when you want to switch buffers that have different formats/layout.
  2. Bind a completely different VAO. This is recommended if your vertex format changes.

In this case, we are going to be using method 1, where we will have a single VAO that shares the vertex format, but we will switch the buffer with a call to glVertexArrayVertexBuffer().

Preparing the Data

Here we are creating our two c++ arrays that hold the data that will go into the two buffers.

… previous code (click to expand)
#include "error_handling.hpp"
#include <array>
#include <chrono>     // current time
#include <cmath>      // sin & cos
#include <cstdlib>    // for std::exit()
#include <fmt/core.h> // for fmt::print(). implements c++20 std::format
#include <unordered_map>

// this is really important to make sure that glbindings does not clash with
// glfw's opengl includes. otherwise we get ambigous overloads.
#define GLFW_INCLUDE_NONE
#include <GLFW/glfw3.h>

#include <glbinding/gl/gl.h>
#include <glbinding/glbinding.h>

#include <glbinding-aux/debug.h>

#include "glm/glm.hpp"

using namespace gl;
using namespace std::chrono;

int main() {

    auto startTime = system_clock::now();

    const auto windowPtr = []() {
        if (!glfwInit()) {
            fmt::print("glfw didnt initialize!\n");
            std::exit(EXIT_FAILURE);
        }

        glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 4);
        glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 6);

        auto windowPtr = glfwCreateWindow(1280, 720, "Chapter 8 - Multiple Vertex Array Objects", nullptr, nullptr);

        if (!windowPtr) {
            fmt::print("window doesn't exist\n");
            glfwTerminate();
            std::exit(EXIT_FAILURE);
        }
        glfwSetWindowPos(windowPtr, 520, 180);

        glfwMakeContextCurrent(windowPtr);
        glbinding::initialize(glfwGetProcAddress, false);
        return windowPtr;
    }();

    // debugging
    {
        glEnable(GL_DEBUG_OUTPUT);
        glDebugMessageCallback(errorHandler::MessageCallback, 0);
        glEnable(GL_DEBUG_OUTPUT_SYNCHRONOUS);
        glDebugMessageControl(GL_DEBUG_SOURCE_API, GL_DEBUG_TYPE_OTHER, GL_DEBUG_SEVERITY_NOTIFICATION, 0, nullptr,
                              false);
    }

    auto createProgram = [](const char* vertexShaderSource, const char* fragmentShaderSource) -> GLuint {
        auto vertexShader = glCreateShader(GL_VERTEX_SHADER);
        glShaderSource(vertexShader, 1, &vertexShaderSource, nullptr);
        glCompileShader(vertexShader);
        errorHandler::checkShader(vertexShader, "Vertex");

        auto fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
        glShaderSource(fragmentShader, 1, &fragmentShaderSource, nullptr);
        glCompileShader(fragmentShader);
        errorHandler::checkShader(fragmentShader, "Fragment");

        auto program = glCreateProgram();
        glAttachShader(program, vertexShader);
        glAttachShader(program, fragmentShader);

        glLinkProgram(program);
        return program;
    };

    auto program = createProgram(R"(
        #version 450 core
        layout (location = 0) in vec3 position;
        layout (location = 1) in vec3 colours;

        out vec3 vertex_colour;

        void main(){
            vertex_colour = colours;
            gl_Position = vec4(position, 1.0f);
        }
    )",
                                 R"(
        #version 450 core

        in vec3 vertex_colour;
        out vec4 finalColor;

        void main() {
            finalColor = vec4(  vertex_colour.x,
                                vertex_colour.y,
                                vertex_colour.z,
                                1.0);
        }
    )");

    struct vertex3D {
        glm::vec3 position;
        glm::vec3 colour;
    };
    // clang-format off
    // interleaved data
    // make 3d to see clipping
    const std::array<vertex3D, 3> backGroundVertices {{
        //   position   |     colour
        {{-1.f, -1.f, 0.f},  {0.12f, 0.14f, 0.16f}},
        {{ 3.f, -1.f, 0.f},  {0.12f, 0.14f, 0.16f}},
        {{-1.f,  3.f, 0.f},  {0.80f, 0.80f, 0.82f}}
    }};

    const std::array<vertex3D, 3> foregroundVertices {{
        //   position   |     colour
        {{-0.5f, -0.7f,  0.01f},  {1.f, 0.f, 0.f}},
        {{0.5f, -0.7f,  -0.01f},  {0.f, 1.f, 0.f}},
        {{0.0f, 0.6888f, 0.01f}, {0.f, 0.f, 1.f}}
    }};
    // clang-format on

Drawing a full screen Mesh

If you want to draw a full screen mesh, then you might think that two triangles that fill the screen to make a ‘quad’ is what you want.

This wilil certainly ‘work’, but there is a simpler way. We can draw a single triangle that covers the whole window. Anything that extends out of the window bounds will simply not get rendered.

Here the red section is the window area (in opengl normalized device coordinates, we will cover this in a future chapter) and the green is the triangle that we are drawing which covers this region.

The second array is the triangle that we are going to draw over the top, like so…..

Did you notice how the z component of one of the vertices (the bottom right one) is negative? What effect do you think this will have on the rendering? The background triangle is centered at 0 in the z direction. So far we haven’t been concerned with 3D, but this is certainly possible to use the third z component to control the layering of our 2D elements. Do we have intersecting geometry here? Lets see once we have rendered.

Creating Objects using C++ Lambdas

I’m going to introduce you to a c++ feature that I like to use when writing small educational programs like this where I like to keep the code readable and locally reasonable. I like not having to jump around code to different locations just to see what a helper function is doing. So using a lambda, we can create a local function right there in place and use it immediately.

    auto createBuffer = [&program](const std::array<vertex3D, 3>& vertices) -> GLuint {     
        GLuint bufferObject;
        glCreateBuffers(1, &bufferObject);

        // upload immediately
        glNamedBufferStorage(bufferObject, vertices.size() * sizeof(vertex3D), vertices.data(),
                             GL_MAP_WRITE_BIT | GL_DYNAMIC_STORAGE_BIT);

        return bufferObject;  
    };

    auto backGroundBuffer = createBuffer(backGroundVertices);
    auto foreGroundBuffer = createBuffer(foregroundVertices);

The way this works is we use the word auto to store the lambda, which is essentially a function object. It doesn’t actually run the code, just stores the function as a variable that we can call later. We do that immediately and create two buffers by invoking that lambda and passing in our c++ arrays. The code inside the lambda gets run for both calls, just like it was a normal function. The difference is, its right there, we can read it and as nothing else needs to use it, why does it need to be a public function anyway? Local reasoning about code is a beautiful thing.

Now we do something similar for the vao. This technically doesn’t need to be a lambda, but I do it anyway (partly out of habit, and partly because it indents the code and wraps things up nicely and the name is descriptive and is self-commenting. Also we will be extending it in the future).

    auto createVertexArrayObject = [](GLuint program){

        GLuint vao;
        glCreateVertexArrays(1, &vao);

        glEnableVertexArrayAttrib(vao, 0);
        glEnableVertexArrayAttrib(vao, 1);


        glVertexArrayAttribBinding(vao, glGetAttribLocation(program, "position"),
                                   /*buffer index*/ 0);
        glVertexArrayAttribBinding(vao, glGetAttribLocation(program, "colours"),
                                   /*buffs idx*/ 0);

        glVertexArrayAttribFormat(vao, 0, glm::vec3::length(), GL_FLOAT, GL_FALSE, offsetof(vertex3D, position));
        glVertexArrayAttribFormat(vao, 1, glm::vec3::length(), GL_FLOAT, GL_FALSE, offsetof(vertex3D, colour));

        return vao;
    }

    auto vertexArrayObject = createVertexArrayObject(program);

Now let’s do our render loop. Notice how we can now remove the calls to glClearBufferfv() as we are drawing a full screen triangle, removing the need to clear the background.

    glUseProgram(program);
    glBindVertexArray(vertexArrayObject);

    while (!glfwWindowShouldClose(windowPtr)) {

        glVertexArrayVertexBuffer(vao, 0, backGroundBuffer, /*offset*/ 0,
                                  /*stride*/ sizeof(vertex3D));
        glDrawArrays(GL_TRIANGLES, 0, 3);

        glVertexArrayVertexBuffer(vao, 0, foreGroundBuffer, /*offset*/ 0,
                                  /*stride*/ sizeof(vertex3D));
        glDrawArrays(GL_TRIANGLES, 0, 3);

        glfwSwapBuffers(windowPtr);
        glfwPollEvents();
    }

    glfwTerminate();
}

We bind the single VAO as normal, but this time, we swap out the vertex arrays directly using the calls to glVertexArrayVertexBuffer() and pass in the bufferObjects.

Introducing Depth

Now we will come back to the point that was raised earlier which is that two of the vertices (left bottom and middle top) are on one side of the background plane in the z direction (coming into/out of the screen) and the bottom right vertex is coming towards the screen. So we should expect some kind of intersection here shouldn’t we?

Well when triangles are rasterized, if a certain feature called depth testing isn’t enabled, then each fragment will just get drawn on top of the one drawn previously. Because we draw the background first, and then the coloured triangle, thats the order in which they appear and the triangles fragments just get placed on top. So how do we enable this depth testing thingy? Like this….

    glEnable(GL_DEPTH_TEST);

We place this line just before our main rendering loop. And we should now see this…

Great! Each pixel’s depth is now being compared to the depth that is already there and if it is ‘behind’ the existing depth value then it is discarded and not rendered.

This is effectively what has happened….

We have made it so that the bottom left and top middle vertices are ‘behind the gradient background triangle and part of it slices through.

1. I say ‘behind’ with quotes because the behaviour of the depth comparison can be controlled by telling opengl if you want to compare greater or lesser than the current depth value.

2. Depth values are stored in the ‘Depth Buffer’. At a future point, the depth buffer becomes important for more advanced effects. For now we are just concerned with simple rendering geometry so we can leave that for a future discussion.

3. We are rendering a background here, but what would happen if we didn’t fill the screen with values and wanted to draw multiple intersecting meshes against a cleared background like we had in previous chapter? In that case we would want to clear the depth buffer as well by creating a default value of 1.0f and calling the glClearBufferfv() function like so…..

std::array<GLfloat, 1> clearDepth{1.0};

while (!glfwWindowShouldClose(windowPtr)) {
   glClearBufferfv(GL_DEPTH, 0, clearDepth.data());
   // render loop...
}

We now have some pretty solid fundamental mechanics in our toolbox. From here we could go on and keep exploring various intricacies of how these things work, but I think we have enough that we can start exploring some more fun concepts. In the next lesson, we will be taking the idea of drawing a full screen triangle and using it to run a ‘shadertoy’ -like fragment shader program. We will actually be using a real shadertoy example and rendering it locally in our own OpenGL program. So see you in the next chapter.

Modern OpenGL: Part 7 Interleaving Vertex Buffer Data

In the last 2 chapters we have learned

  1. how to feed buffer data into a single shader position attribute
  2. how to marshall multiple buffers into multiple shader atrributes

Now in this lesson we will learn how to combine those multiple buffers into a single buffer where the data is interleaved but still feed that data into multiple shader attributes.

So want to go from this….

to this…

So lets crack on with the code and I’ll explain as we go. First we have our previous setup code (you can expand this to see this if you wish).

… previous code (click to expand)
#include "error_handling.hpp"
#include <array>
#include <chrono>     // current time
#include <cmath>      // sin & cos
#include <cstdlib>    // for std::exit()
#include <fmt/core.h> // for fmt::print(). implements c++20 std::format
#include <pystring.h>
#include <unordered_map>

// this is really important to make sure that glbindings does not clash with
// glfw's opengl includes. otherwise we get ambigous overloads.
#define GLFW_INCLUDE_NONE
#include <GLFW/glfw3.h>

#include <glbinding/gl/gl.h>
#include <glbinding/glbinding.h>

#include <glbinding-aux/debug.h>

#include "glm/glm.hpp"

using namespace gl;
using namespace std::chrono;

int main() {

    auto startTime = system_clock::now();

    const auto windowPtr = []() {
        if (!glfwInit()) {
            fmt::print("glfw didnt initialize!\n");
            std::exit(EXIT_FAILURE);
        }

        glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 4);
        glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 6);

        auto windowPtr =
            glfwCreateWindow(1280, 720, "Chapter 7 - Interleaving Vertex Data", nullptr, nullptr);

        if (!windowPtr) {
            fmt::print("window doesn't exist\n");
            glfwTerminate();
            std::exit(EXIT_FAILURE);
        }
        glfwSetWindowPos(windowPtr, 520, 180);

        glfwMakeContextCurrent(windowPtr);
        glbinding::initialize(glfwGetProcAddress, false);
        return windowPtr;
    }();

    // debugging
    {
        glEnable(GL_DEBUG_OUTPUT);
        glDebugMessageCallback(errorHandler::MessageCallback, 0);
        glEnable(GL_DEBUG_OUTPUT_SYNCHRONOUS);
        glDebugMessageControl(GL_DEBUG_SOURCE_API, GL_DEBUG_TYPE_OTHER,
                              GL_DEBUG_SEVERITY_NOTIFICATION, 0, nullptr, false);
    }

    auto program = []() -> GLuint {
        const char* vertexShaderSource = R"(
            #version 460 core
            layout (location = 0) in vec2 position;
            layout (location = 1) in vec3 colour;

            out vec3 vertex_colour;

            void main(){
                vertex_colour = colour;
                gl_Position = vec4(position, 0.0f, 1.0f);
            }
        )";

        const char* fragmentShaderSource = R"(
            #version 460 core

            in vec3 vertex_colour;
            out vec4 finalColor;

            void main() {
                finalColor = vec4(  vertex_colour.x,
                                    vertex_colour.y,
                                    vertex_colour.z,
                                    1.0);
            }
        )";

        auto vertexShader = glCreateShader(GL_VERTEX_SHADER);
        glShaderSource(vertexShader, 1, &vertexShaderSource, nullptr);
        glCompileShader(vertexShader);
        errorHandler::checkShader(vertexShader, "Vertex");

        auto fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
        glShaderSource(fragmentShader, 1, &fragmentShaderSource, nullptr);
        glCompileShader(fragmentShader);
        errorHandler::checkShader(fragmentShader, "Fragment");

        auto program = glCreateProgram();
        glAttachShader(program, vertexShader);
        glAttachShader(program, fragmentShader);

        glLinkProgram(program);
        return program;
    }();

Now we have to tackle how we are going to pack the multiple buffers into a single one. First we have to declare a struct which describes how each vertex will be laid out in memory. The simplest option is to just list one after the other (I’m going to ignore ‘packing’ and ‘padding’ considerations for now, just know that they are a thing).

    struct vertex2D {
        glm::vec2 position;
        glm::vec3 colour;
    };

This doesn’t need much explaining. We are describing a new type called vertex2D that hold a vec2 and a vec3. Now that we described it, lets create some data of that type.

    
    // clang-format off
    // interleaved data
    const std::array<vertex2D, 3> vertices {{
        //   position   |     colour
        {{-0.5f, -0.7f},  {1.f, 0.f, 0.f}},
        {{0.5f, -0.7f},   {0.f, 1.f, 0.f}},
        {{0.0f, 0.6888f}, {0.f, 0.f, 1.f}}
    }};


    const size_t sizeOfVerticesInBytes = vertices.size() * sizeof(vertex2D);
    // clang-format on

Now instead of two separate arrays we are creating a single array with the data interleaved. This is how this can be visualized…

Now we have our c++ array of interleaved data, we now need to modify the way we set up out Vertex Array Object so that the right bits of the array are fed into the right shader attributes.

We need to…

  1. Only create 1 buffer this time and upload the data to it
  2. Set the jump in the gap between vertices to a larger size (the size of our struct)
  3. Connect both shader attributes to the single buffer ‘slot’
  4. Modify the format information so that the shader attributes index into the right part of each vertex2D struct by passing in the offset into that struct.
    // in core profile, at least 1 vao is needed 
    GLuint vao;
    glCreateVertexArrays(1, &vao);

No change here, we just create the VAO.

    GLuint bufferObject;
    glCreateBuffers(1, &bufferObject);
    glNamedBufferStorage(bufferObject, sizeOfVerticesInBytes, vertices.data(),
                         GL_MAP_WRITE_BIT | GL_DYNAMIC_STORAGE_BIT);

(1) Now we create just a single buffer and give it the whole size of the interleaved array.

    glEnableVertexArrayAttrib(vao, 0);
    glEnableVertexArrayAttrib(vao, 1);

No we still enable two shader attribute inputs, so no change here.

    glVertexArrayVertexBuffer(vao, /*slot*/ 0, bufferObject, /*offset*/ 0,
                              /*stride*/ sizeof(vertex2D));

So here we are doing points (2) and (3), where we set the single buffer to the 0th slot, and also inform the vao of the jump between vertices, which are now sizeof(vertex2D) bytes apart.

In this case its 20 bytes, because there are 5 * 32 bit floats in each vertex (from a vec2 + vec3), the size will be 5 * 4 bytes (because a single float is 4 bytes) which is 20 bytes. You don’t really need to know this for each case but it definitely benefits if you understand the underlying principles.
    glVertexArrayAttribBinding(vao, glGetAttribLocation(program, "position"),
                               /*buffer slot index*/ 0);
    glVertexArrayAttribBinding(vao, glGetAttribLocation(program, "colour"),
                               /*buffer slot index*/ 0); // <-- NEW!!! this goes from slot 1 to 0

Now we change where the shader attributes will source their data from. We tell both attribute locations that they will get their data from the single buffer in slot 0. “But won’t they both be getting the same data?” you might ask. Thats where the next change comes in.

        
        
    glVertexArrayAttribFormat(vao, 0, glm::vec2::length(), GL_FLOAT, GL_FALSE,
                              offsetof(vertex2D, position)); // <-- NEW!!! this is now an offset
    glVertexArrayAttribFormat(vao, 1, glm::vec3::length(), GL_FLOAT, GL_FALSE,
                              offsetof(vertex2D, colour)); //<-- NEW!!! this is now an offset

        

Here we tell the vao that the shader attributes can source their data from a different location within each vertex. We state here that each attribute will get its data at a distance specified by the offset in the vertex2D struct. Position is at the start of the struct so is at a distance of 0 bytes from the start of the struct, whereas the colour attribute starts right after the position attribute so is 2 elements later (which is 2 * 4 bytes = 8 byes).

Here we are using the offsetof macro….
https://en.cppreference.com/w/cpp/types/offsetof

It can be tedious to work out these byte offsets yourself so its convenient to use this. But please do make sure that you have a mental picture similar to the one above so that you understand what’s really going on!

Here is another image showing how each vertex will now pull from our buffer and where the offsets come into play….

And that is pretty much it! We can now do our drawing as before and you should see a triangle made up of a single buffer with positions and colours interleaved.

… previous code (click to expand)
    std::array<GLfloat, 4> clearColour;
    glUseProgram(program);
    glBindVertexArray(vao);

    while (!glfwWindowShouldClose(windowPtr)) {

        auto currentTime = duration<float>(system_clock::now() - startTime).count();

        clearColour = {std::sin(currentTime) * 0.5f + 0.5f, std::cos(currentTime) * 0.5f + 0.5f,
                       0.2f, 1.0f};

        glClearBufferfv(GL_COLOR, 0, clearColour.data());
        glDrawArrays(GL_TRIANGLES, 0, 3);
        glfwSwapBuffers(windowPtr);
        glfwPollEvents();
    }

    glfwTerminate();
}

Modern OpenGL: Part 6 Adding Colour with Multiple Vertex Buffers

So we have now tackled the basics of how to not only put data into buffers, but also how to connect that data up to shaders through the use of VAO’s.

Now we will extend that knowledge a little to allow us to use multiple buffers to store separate attributes. One reason you might want to do this is if you want to have your vertices have a colour attribute that never changes, but your positions might be animated and need to be updated every frame. If your vertices had all attributes in one buffer (which is something we will tackle in the next chapter), you would have to update all the vertices. Separating them out into separate buffers means you only have to update the data that you need to. The other reason is that is easier to introduce the concept of multiple attributes in shaders like this and I won’t be overloading you with information.

… previous code (click to expand)
#include "error_handling.hpp"
#include <array>
#include <chrono>     // current time
#include <cmath>      // sin & cos
#include <cstdlib>    // for std::exit()
#include <fmt/core.h> // for fmt::print(). implements c++20 std::format
#include <unordered_map>

// this is really important to make sure that glbindings does not clash with
// glfw's opengl includes. otherwise we get ambigous overloads.
#define GLFW_INCLUDE_NONE
#include <GLFW/glfw3.h>

#include <glbinding/gl/gl.h>
#include <glbinding/glbinding.h>

#include <glbinding-aux/debug.h>

#include "glm/glm.hpp"

using namespace gl;
using namespace std::chrono;

int main() {

    auto startTime = system_clock::now();

    const auto windowPtr = []() {
        if (!glfwInit()) {
            fmt::print("glfw didnt initialize!\n");
            std::exit(EXIT_FAILURE);
        }

        glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 4);
        glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 6);

        auto windowPtr =
            glfwCreateWindow(1280, 720, "Chapter 6 - Multiple Buffers", nullptr, nullptr);

        if (!windowPtr) {
            fmt::print("window doesn't exist\n");
            glfwTerminate();
            std::exit(EXIT_FAILURE);
        }
        glfwSetWindowPos(windowPtr, 520, 180);

        glfwMakeContextCurrent(windowPtr);
        glbinding::initialize(glfwGetProcAddress, false);
        return windowPtr;
    }();

    // debugging
    {
        glEnable(GL_DEBUG_OUTPUT);
        glDebugMessageCallback(errorHandler::MessageCallback, 0);
        glEnable(GL_DEBUG_OUTPUT_SYNCHRONOUS);
        glDebugMessageControl(GL_DEBUG_SOURCE_API, GL_DEBUG_TYPE_OTHER,
                              GL_DEBUG_SEVERITY_NOTIFICATION, 0, nullptr, false);
    }

The first changes we need to make are in the shader programs. We now are going to have 2 attribute inputs. One for position (which is what we had previously) and the additional one, colours.

    auto program = []() -> GLuint {
        const char* vertexShaderSource = R"(
            #version 450 core
            layout (location = 0) in vec2 position;
            layout (location = 1) in vec3 colour; <-- new!

            out vec3 vertex_colour;

            void main(){
                vertex_colour = colour;
                gl_Position = vec4(position, 0.0f, 1.0f);
            }
        )";

        const char* fragmentShaderSource = R"(
            #version 450 core

            in vec3 vertex_colour;
            out vec4 finalColor;

            void main() {
                finalColor = vec4(  vertex_colour.x,
                                    vertex_colour.y,
                                    vertex_colour.z,
                                    1.0);
            }
        )";

        auto vertexShader = glCreateShader(GL_VERTEX_SHADER);
        glShaderSource(vertexShader, 1, &vertexShaderSource, nullptr);
        glCompileShader(vertexShader);
        errorHandler::checkShader(vertexShader, "Vertex");

        auto fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
        glShaderSource(fragmentShader, 1, &fragmentShaderSource, nullptr);
        glCompileShader(fragmentShader);
        errorHandler::checkShader(fragmentShader, "Fragment");

        auto program = glCreateProgram();
        glAttachShader(program, vertexShader);
        glAttachShader(program, fragmentShader);

        glLinkProgram(program);
        return program;
    }();

    

Here is what we have added…

Vertex Shader:
  • a new in vec3 attribute ‘colour’ with the layout qualifier setting it’s location to the ‘1’ array index
  • a new ‘out’ attribute
  • in main(), we directly set the value of this out attribute to the value coming in from the buffers
Fragment Shader:
  • a new in attribute that has the same name as the out attribute that we had in our vertex shader.

If the names match up between the vertex shader and the fragment shader, then the compiler will know that they are meant to be the ‘same’ attribute. Alternatively we could have used layout locations to specify the ‘slots’ and we could name them whatever we want.

Setting up multiple buffers

Now we will use what we learnt about VAO mapping in the previous chapter to connect up the buffer data to the new attribute.

First lets create the initial data in our main program….

    // extra braces required in c++11.
    const std::array<glm::vec2, 3> vertices{{{-0.5f, -0.7f}, {0.5f, -0.7f}, {0.0f, 0.6888f}}};

    const std::array<glm::vec3, 3> colours{{{1.f, 0.f, 0.f}, {0.f, 1.f, 0.f}, {0.f, 0.f, 1.f}}};

You might be able to spot the colours we are setting here. We have one vertex with the first component at 1, a second with the 2nd component one and the third with the third component 1. Those are the red, green and blue colours. So we have an array of 3 vec3s.

Lets create the GPU buffers and provide the data to them

    std::array<GLuint, 2> bufferObjects;
    glCreateBuffers(2, bufferObjects.data());

    glNamedBufferStorage(bufferObjects[0], vertices.size() * sizeof(glm::vec2), nullptr,
                         GL_MAP_WRITE_BIT | GL_DYNAMIC_STORAGE_BIT);
    glNamedBufferStorage(bufferObjects[1], colours.size() * sizeof(glm::vec3), nullptr,
                         GL_MAP_WRITE_BIT | GL_DYNAMIC_STORAGE_BIT);

    glNamedBufferSubData(bufferObjects[0], 0, vertices.size() * sizeof(glm::vec2),
                         vertices.data());
    glNamedBufferSubData(bufferObjects[1], 0, colours.size() * sizeof(glm::vec3),
                         colours.data());

This time we will create an array of 2 buffers and ask the glCreateBuffers to give us 2 unique ids to refer to them.

Then we do something slightly different from the last chapter. In this one, we pass nullptr to the glNamedBufferStorage (along with the index into the bufferObjects array for the buffer that we want to allocate storage for). Passing nullptr, tells opengl to just allocate the storage of the required size, but doesn’t transfer any data yet.

We transfer the data with the glNamedBufferSubData() function. Why did we do this? Well just because if we wanted to have dynamic data (like if the positions or colours changed everyframe), then this would be the way to update the data to those buffers without having to recreate the buffers and reallocate storage and potentially set new binding information in the VAO.

    // in core profile, at least 1 vao is needed
    GLuint vao;
    glCreateVertexArrays(1, &vao);

    glEnableVertexArrayAttrib(vao, 0);
    glEnableVertexArrayAttrib(vao, 1);

    // buffer to index mapping
    glVertexArrayVertexBuffer(vao, 0, bufferObjects[0], /*offset*/ 0, sizeof(glm::vec2));
    glVertexArrayVertexBuffer(vao, 1, bufferObjects[1], /*offset*/ 0, sizeof(glm::vec3));

    glVertexArrayAttribBinding(vao, glGetAttribLocation(program, "position"),
                               /*slot index*/ 0);
    glVertexArrayAttribBinding(vao, glGetAttribLocation(program, "colour"),
                               /*slot index*/ 1);

    // data stride
    glVertexArrayAttribFormat(vao, 0, glm::vec2::length(), GL_FLOAT, GL_FALSE, 0);
    glVertexArrayAttribFormat(vao, 1, glm::vec3::length(), GL_FLOAT, GL_FALSE, 0);    

So lets recap what we have done here that is different from the last chapter.

  • we now enable 2 shader array attribute locations with glEnableVertexArrayAttrib() for the position and colour respectively
  • we connect up the two buffers to to independent binding index slots with glVertexArrayVertexBuffer() (remember those intermediary mapping locations?). We also inform opengl of the byte size of the elements as well (with vec3 for the colour)
  • we connect up the shader attribute locations to those slots with glVertexArrayAttribBinding()
  • we set the format on the attributes inputs for the shader with glVertexArrayAttribFormat(). notice that we set the vec3s type here to

Here is a diagram of our vao setup now…

And we are done!

… previous code (click to expand)
    std::array<GLfloat, 4> clearColour;
    glUseProgram(program);
    glBindVertexArray(vao);


    while (!glfwWindowShouldClose(windowPtr)) {
        auto currentTime = duration<float>(system_clock::now() - startTime).count();
        clearColour = {std::sin(currentTime) * 0.5f + 0.5f, std::cos(currentTime) * 0.5f + 0.5f,
                       0.2f, 1.0f};

        glClearBufferfv(GL_COLOR, 0, clearColour.data());
        glDrawArrays(GL_TRIANGLES, 0, 3);
        glfwSwapBuffers(windowPtr);
        glfwPollEvents();
    }

    glfwTerminate();
}

Modern OpenGL: Part 5 Feeding Vertex Data to Shaders with Vertex Buffers

In the previous chapters, we were able to…

  • create a window
  • draw a triangle
  • set up OpenGL debugging callbacks

Now we want to extend our triangle drawing, but allowing our c++ code to supply the triangle’s vertex positions. We were making things easier before by telling OpenGL what the vertex positions were directly in the shader in an array that was compiled along with the shader. We then indexed into that array with the glVertexID.

In this chapter, we will be

  • setting up out shaders to be ‘given’ the vertex position automatically (no need for glVertexID)
  • setup buffers in our c++ code to move vertex data over to the GPU to ‘feed’ them to the shaders.

I say ‘feed’ because I like to imaging the shader program as being a black box or a factory that is just given data, they are transformed by the shaders and then out the other end triangles just come out. Think of the shader program as a factory, and the vertices as the raw ingredients that go into the factory to make the product (triangles).

Setting up shaders to receive vertex input

We can remove the shaders we had previously as the vertices will now be supplied directly to the shaders. The shaders also don’t need to do any manual indexing as OpenGL will take care of that for us. All we have to do when we supply our data is to make sure that the vertex data is set up so that every 3 vertices represents the positions of our triangle vertices and then to draw the right number of vertices for our data.

… previous code (click to expand)
#include "error_handling.hpp"
#include <array>
#include <chrono>     // current time
#include <cmath>      // sin & cos
#include <cstdlib>    // for std::exit()
#include <fmt/core.h> // for fmt::print(). implements c++20 std::format
#include <unordered_map>

// this is really important to make sure that glbindings does not clash with
// glfw's opengl includes. otherwise we get ambigous overloads.
#define GLFW_INCLUDE_NONE
#include <GLFW/glfw3.h>

#include <glbinding/gl/gl.h>
#include <glbinding/glbinding.h>

#include <glbinding-aux/debug.h>

#include "glm/glm.hpp"

using namespace gl;
using namespace std::chrono;

int main() {

    auto startTime = system_clock::now();

    const auto windowPtr = []() {
        if (!glfwInit()) {
            fmt::print("glfw didnt initialize!\n");
            std::exit(EXIT_FAILURE);
        }

        glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 4);
        glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 5);

        auto windowPtr = glfwCreateWindow(1280, 720, "Chapter 5 - Vertex Data", nullptr, nullptr);

        if (!windowPtr) {
            fmt::print("window doesn't exist\n");
            glfwTerminate();
            std::exit(EXIT_FAILURE);
        }
        glfwSetWindowPos(windowPtr, 520, 180);

        glfwMakeContextCurrent(windowPtr);
        glbinding::initialize(glfwGetProcAddress, false);
        return windowPtr;
    }();

    // debugging
    {
        glEnable(GL_DEBUG_OUTPUT);
        glDebugMessageCallback(errorHandler::MessageCallback, 0);
        glEnable(GL_DEBUG_OUTPUT_SYNCHRONOUS);
        glDebugMessageControl(GL_DEBUG_SOURCE_API, GL_DEBUG_TYPE_OTHER,
                              GL_DEBUG_SEVERITY_NOTIFICATION, 0, nullptr, false);
    }

    auto program = []() -> GLuint {
        const char* vertexShaderSource = R"(
            #version 450 core
            layout (location = 0) in vec2 position;

            out vec3 vertex_colour;

            void main(){
                vertex_colour = vec3(1.0f, 0.0f, 0.0f);
                gl_Position = vec4(position, 0.0f, 1.0f);
            }
        )";

        const char* fragmentShaderSource = R"(
            #version 450 core

            in vec3 vertex_colour;
            out vec4 finalColor;

            void main() {
                finalColor = vec4(vertex_colour, 1.0);
            }
        )";


… previous code (click to expand)
        auto vertexShader = glCreateShader(GL_VERTEX_SHADER);
        glShaderSource(vertexShader, 1, &vertexShaderSource, nullptr);
        glCompileShader(vertexShader);
        errorHandler::checkShader(vertexShader, "Vertex");

        auto fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
        glShaderSource(fragmentShader, 1, &fragmentShaderSource, nullptr);
        glCompileShader(fragmentShader);
        errorHandler::checkShader(fragmentShader, "Fragment");

        auto program = glCreateProgram();
        glAttachShader(program, vertexShader);
        glAttachShader(program, fragmentShader);

        glLinkProgram(program);
        return program;
    }();

I’ve removed the colour array for this example as it will allow us to focus on the important parts. We will be adding back more attributes in later chapters but here, we have just hard coded the colour attribute that is output from the vertex shader (and interpolated across the rasterised triangles) to fixed red colour.

Here we have added the line….

layout (location = 0) in vec2 position;

This will take a bit of dissecting. There are actually two things happening here, the ‘layout qualifier’ and the ‘type qualifier’.

OpenGL can work without the layout (location = 0) part, but the in vec2 positions part is defining the data interface of the shader and states what data is coming in.

Layout Qualifier

layout

This word specifies that we are about to state a layout qualifier. Qualifiers are statements that tell the glsl compiler where storage for a variable will be and other information which we won’t look into just yet..

(location = 0)

The text inside the braces is the qualifier and states that we are going to store a variable at a particular location 0. This location is going to be something that we can also use on the c++ side and is the way that we associate the vertex data in the buffer with the variable in the GLSL shader. You will often hear about the ‘binding’ location. This is location that the attribute is bound to.

Type Qualifier

in

This ‘in’ qualifier simply states that the attribute (data) that we are stating is an ‘in’ attribute and that it is going to be supplied to the shader somehow (we will be providing the data via vertex buffers which is a region of memory from our c++ application).

vec2

This is the data type that the attribute is expected to be. There are various supported data types. This one is a vector that has two values.

position

This is the name that we want to give the attribute that we can refer to in our glsl shader (This is not a glsl keyword).

for more information, see…
https://www.khronos.org/opengl/wiki/Layout_Qualifier_(GLSL)

We could have written the shader without the layout qualifier and just stated the in vec2 positions part and then on the c++ side, asked the OpenGL runtime what locations they were assigned, as OpenGL would have done that for us if we hadn’t have stated explicitly with the layout qualifier. This way though, we have absolute control over the way that the data is laid out for our shaders. When we get to creating buffers though, I will show you how to query the location anyway, as it is a useful thing to be able to do (especially for debugging purposes).

Using a vertex attribute

In the vertex shader, we have now replaced the array indexing with directly accessing the variable supplied to the shader by using the name.

gl_Position = vec4(position, 0.0f, 1.0f);

Allocating Data on the GPU for our vertex data

Before we feed shaders with vertex data, we need to get the data onto the gpu. Thus section deals with communicating with the gpu to tell it to reserve some space in its vram for vertex data and for opengl to give us back an id for us to refer to that buffer. Then we shall show how to fill it with data .

Marshalling the vertex buffer data to our shaders

There is going to be quite a lot of information here so get ready for another brain overload. Don’t worry, once you have these concepts fully grasped, then the use of OpenGL becomes a lot easier (mostly).

Vertex Buffer Objects

These objects are where we will be storing our vertex data that will be fed into the shaders for the GPU to process and draw. typically the data will be held in GPU memory. So we have to allocate memory ON the GPU and transfer into it. Lets create one!

First let’s create the data in our cpu memory. This might be loaded from a model from disk (which we will see in chapter 11), but here we are manually specifying some locations. We have chosen to use a std::array as the data needs to be contiguous in memory.

// extra braces required in c++11.
const std::array<glm::vec2, 3> vertices{{{-0.5f, -0.7f}, {0.5f, -0.7f}, {0.0f, 0.6888f}}};

Now we use the opengl API to ‘create’ a buffer.

GLuint bufferObject;
glCreateBuffers(1, &bufferObject);

Narrator: “This did not create a buffer”.

What we have done here is only tell opengl to give us the id of a buffer. At the moment it is just an abstract handle that we will use to refer to the buffer. There is no actually data allocated on the GPU yet. This is done in the next line.

glNamedBufferStorage(bufferObject, vertices.size() * sizeof(glm::vec2), vertices.data(),
                             GL_MAP_WRITE_BIT | GL_DYNAMIC_STORAGE_BIT);

This glNamedBufferStorage() call is the one that instructs opengl to both

  • allocate the storage and
  • transfer the data

The arguments that we pass in are…

  • the buffer id we created
  • how many bytes we want allocated
  • the pointer to the data from our array so the buffer knows where to get the data to transfer
  • Some flags to tell opengl how we want to use the buffer (this can help opengl know where to place the data to make it efficient)

And we are done! Now how to we get this data into our shaders so the shaders know that this is the buffer it should be processing?

In future chapters, we will be looking at advanced ways of getting data on to the gpu to improve performance and is more in line with the modern AZDO principles. For now, this keeps things simple.

Vertex Array Objects

  • which vertex buffers are being used
  • how the data is going to be interpreted by OpenGL by setting type information of the data
  • which shader attribute locations the vertex data is associated with

Vertex Array Objects (VAOs) are the Worst Named Things In The History Of Humanity. What they actually are, are a construct that maps data from buffers into shaders. You then enable (bind) a single VAO that will control which data will be drawn when you issue a draw call.

Shaders typically have attribute inputs that we have seen just before and these inputs are defined by a location (0,1,2 etc ) and a type. Those locations make up the ‘array’ that we want to map our buffers to. VAOs are the thing that let us hook up the data in our buffers to the shader inputs.

There are 3 components we need to address.

  1. enable the array location we want to feed to
  2. which buffer is associated with with location
  3. the type, offset and stride of the data

DSA vs legacy

VAOs used to work by first having to bind the VOA to make it ‘the current one’, then you would ‘bind’ your buffers with another API call, and the VAO would ‘remember’ which buffer was bound. There is no such mechanism here.

Using Direct State Access (DSA) functions we can explicitly state the relationship between the buffers and the binding location by ‘telling’ the VAO this information, rather than having it implicitly remembered because that was the one that was bound at the time.

So lets create a VAO!

    // in core profile, at least 1 vao is needed
    GLuint vao;
    glCreateVertexArrays(1, &vao);

We do a similar thing that we did with the buffer, we create an id integer and have opengl assign it for us. There is no allocation step here, we can just use the id directly.

We now need to tell opengl how to map the buffer data to the shader locations. We start by simply enabling the first attribute location in the shader.

    glEnableVertexArrayAttrib(vao, 0);

This just ‘opens the gate’ for the data to flow through. This is the input to things like position or colour etc that are the “layout (location = 0)” qualifiers that we specified in the shader previously.

Now we provide the actual mapping between the Buffer Object and a new concept which we will call the ‘mapping slot’ or what the documentation calls a bindingindex. This is basically an intermediary location that we can use as a common agreed upon waypoint to connect the buffer and shader. The shader will never see this location or use it in the shader program. It’s just a connecting up mechanism.

This isn’t actually that useful or interesting at this point, but if we ever have a situation where we have data spread accorss multiple buffers, this mechanism gives us flexibility to pull our data from various sources. It’s especially useful if part of our data is static and never changes but we want to update another buffer per frame and we would only ever need to update one buffer, but the shader can still pull from different sources and not need to be changed.
    // buffer to index mapping
    glVertexArrayVertexBuffer(vao, 0, bufferObject, /*offset*/ 0, sizeof(glm::vec2));

We provide….

  • the vertex array object we are setting data on
  • the mapping slot (or ‘binding point’) location that we are setting the buffer to
  • the buffer object the data is coming from
  • offset and stride information so opengl knows how far apart each ‘element’ is

Because the data is contiguous, how does opengl know that the data is every 8 bytes apart (2 floats of 32 bits each. Since 32 bits is 4 bytes, 2 floats are 8 bytes ). We tell it! Thats why we pass in the sizeof(glm::vec2).

Now we need to connect up the shader attribute location to the mapping slot (binding index).

    // attribute to index mapping. can query shader for its attributes
    glVertexArrayAttribBinding(vao, glGetAttribLocation(program, "position"),
                               /*buffer slot index*/ 0);

Here we are setting the VAO to know how to associate the buffer data to the shader location by passing these arguments…

  • which VAO we are setting data on
  • the shader attribute location (which we can actually query by using the glGetAttribLocation() function. we couuld have explicitly set this based upon what we set in the (layout location = 0))
  • the intermediary binding index we want to pull our data from

Finally, the third and final piece of information we need to provide to the VAO is the vertex format.

    // data stride
    glVertexArrayAttribFormat(vao, /* shader attrobute location */ 0, 
                              glm::vec2::length(), GL_FLOAT, GL_FALSE, 0);

This tells opengl what format the vertex data is. We already passed in the byte offset previously, but this goes into more detail about what is being fed to the shader. We pass…

  • the vao
  • which attribute location we are setting the input format information to
  • how many elements are in each vector type (an integer, 2 in the case of vec2)
  • the type of each individual element in the vector (a float in this case as opposed to int or bool etc)
  • and offset into the buffer to where the data starts. (This will become useful later when we start interleaving vertex data. For now its all the same data in one packed buffer)
It is UP TO YOU as a developer to make sure that the vertex array object that you use (and hence the buffer layout) and the shader program that expects a particular vertex data layout are ‘compatible’. There are various ways to programmatically ensure this, but for now we are manually ensuring this.

Here is a quick diagram of what the 3 functions do to fix up the association between vertex buffer and shader attribute location.

And here is how we typically can use vaos by switching them to change which buffers are being used.

Now to use the buffer we created, we need to bind the vertex array object. I know that we said that we are going to be using DSA functions to avoid having to use the ‘bind’ paradigm in opengl, but alas, opengl hasn’t improved in this area. We still need to provide a ‘current’ VAO that opengl uses to do it’s drawing.

    glBindVertexArray(vao);

We can now continue to do our drawing as normal and hopefully we should see a triangle!

… previous code (click to expand)
    std::array<GLfloat, 4> clearColour;
    glUseProgram(program);

    while (!glfwWindowShouldClose(windowPtr)) {

        auto currentTime = duration<float>(system_clock::now() - startTime).count();

        clearColour = {std::sin(currentTime) * 0.5f + 0.5f, std::cos(currentTime) * 0.5f + 0.5f,
                       0.2f, 1.0f};

        glClearBufferfv(GL_COLOR, 0, clearColour.data());

        glDrawArrays(GL_TRIANGLES, 0, 3);
        glfwSwapBuffers(windowPtr);
        glfwPollEvents();
    }

    glfwTerminate();
}

Modern OpenGL: Part 4 Error Handling

So hopefully, you have had some initial excitement about having your first triangle on the screen. I’m guessing though that you might have had a typo somewhere or passing the wrong value into a function and something went wrong, and maybe it took way more time than it should have to find the issue. You are not alone.

There are ways to find out if something has gone wrong though.

I’m going to briefly introduce you to the legacy way that OpenGL applications could find out what went wrong (you may have seen this shown in other tutorials as it is a widely propagated method of checking for errors) and then I’m quickly going to move on to show you the modern and better way of doing it. In fact if you go to this page, the following method is listed as ‘the hard way’. Once we have briefly covered it, then we will move on to the easy way.

There exists a function called glGetError which asks the OpenGL runtime if anything went wrong. You get back an error code which you are able to check against some particular values to see what kind of error you have. It is usually wrapped up into some kind of helper function like this one that you can call occasionally after your code to see when these errors occur……

void CheckGLError(std::string str) {
    GLenum error = glGetError();
    if (error != GL_NO_ERROR) {
        printf("Error! %s %s\n", str.c_str(), openGLErrorString(error));
    }
}

the openGLErrorString() function might look something like this…..

const char* openGLErrorString(GLenum _errorCode) {
    if (_errorCode == GL_INVALID_ENUM) {
        return "GL_INVALID_ENUM";
    } else if (_errorCode == GL_INVALID_VALUE) {
        return "GL_INVALID_VALUE";
    } else if (_errorCode == GL_INVALID_OPERATION) {
        return "GL_INVALID_OPERATION";
    } else if (_errorCode == GL_INVALID_FRAMEBUFFER_OPERATION) {
        return "GL_INVALID_FRAMEBUFFER_OPERATION";
    } else if (_errorCode == GL_OUT_OF_MEMORY) {
        return "GL_OUT_OF_MEMORY";
    } else if (_errorCode == GL_NO_ERROR) {
        return "GL_NO_ERROR";
    } else {
        return "unknown error";
    }
}

The reason this in not ideal, is that

  • You have to call the function manually yourself when you want to find out if there was an error. If you don’t know where the error came from, you may have to copy and paste it all over your code or wrap your gl* function calls into a CHECK() macro to make sure that you know after which call the error came from
  • Just because you have an error, doesn’t mean that the place that you get an error back is where the issue is. Because OpenGL is a state machine, with complication rules on what operations are valid and allowed it particular states, it could be that the problem is because you did something like forget to bind some framebuffer, or set the wrong read/write bits on a buffer.
  • You just get GL_INVALID_something. No detailed information on what specifically is wrong.

Anyway writing this function is not only the worse way to check for errors, it is also not really necessary because we can simply use our fancy glbindings library to do it for us. If we add a new header and one line of code into our program after the window creation, then all of the above is taken care of for us.

New header…

#include <glbinding-aux/debug.h>

Single line of code to enable basic error checking.

    glbinding::aux::enableGetErrorCallback();

Now if you try some thing like commenting out one single line of our program, for example the function that binds the Vertex Array Object that we had to put in our code in the last chapter, then we should get some error message when we run our program.

    //glBindVertexArray(vao);

This is the output which is just a stream of errors being printed because glDrawArrays is where the error is occuring.

GL_INVALID_OPERATIONglDrawArrays generated

But glDrawArrays() is not the problem. The problem is because there is no Vertex Array Object bound.

The Modern way

Now we are going to use a better way of debugging. First we have to enable ‘debug output’ by calling glEnable….

… surrounding code (click to expand)
#include "error_handling.hpp"
#include <array>
#include <chrono>     // current time
#include <cmath>      // sin & cos
#include <cstdlib>    // for std::exit()
#include <fmt/core.h> // for fmt::print(). implements c++20 std::format
#include <pystring.h>
#include <string>
#include <unordered_map>

// this is really important to make sure that glbindings does not clash with
// glfw's opengl includes. otherwise we get ambigous overloads.
#define GLFW_INCLUDE_NONE
#include <GLFW/glfw3.h>

#include <glbinding/gl/gl.h>
#include <glbinding/glbinding.h>

#include <glbinding-aux/debug.h>

using namespace gl;
using namespace std::chrono;

int main() {

    auto startTime = system_clock::now();

    const auto windowPtr = []() {
        if (!glfwInit()) {
            fmt::print("glfw didnt initialize!\n");
            std::exit(EXIT_FAILURE);
        }

        glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 4);
        glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 6);

        auto windowPtr =
            glfwCreateWindow(1280, 720, "Chapter 4 - Error Handling", nullptr, nullptr);

        if (!windowPtr) {
            fmt::print("window doesn't exist\n");
            glfwTerminate();
            std::exit(EXIT_FAILURE);
        }
        glfwSetWindowPos(windowPtr, 520, 180);

        glfwMakeContextCurrent(windowPtr);
        glbinding::initialize(glfwGetProcAddress, false);
        return windowPtr;
    }();
    glEnable(GL_DEBUG_OUTPUT);

Next we are going to tell OpenGL that we want it to call a function (that we will write) when it detects an error. Isn’t that nice? We do that by setting this so called callback function….

    glDebugMessageCallback(errorHandler::MessageCallback, 0);
    glEnable(GL_DEBUG_OUTPUT_SYNCHRONOUS);
… previous code (click to expand)
    const char* vertexShaderSource = R"VERTEX(
        #version 460 core
        out vec3 colour;

        const vec4 vertices[] = vec4[]( vec4(-0.5f, -0.7f,    0.0, 1.0), 
                                        vec4( 0.5f, -0.7f,    0.0, 1.0),    
                                        vec4( 0.0f,  0.6888f, 0.0, 1.0));   

        const vec3 colours[] = vec3[](  vec3( 1.0, 0.0, 0.0), 
                                        vec3( 0.0, 1.0, 0.0),    
                                        vec3( 0.0, 0.0, 1.0));   

        void main(){
            colour = colours[gl_VertexID];
            gl_Position = vertices[gl_VertexID];  
        }
    )VERTEX";

    const char* fragmentShaderSource = R"FRAGMENT(
        #version 460 core

        in vec3 colour;
        out vec4 finalColor;

        void main() {
            finalColor = vec4(colour.x, colour.y, colour.z, 1.0);
        }
    )FRAGMENT";

    auto vertexShader = glCreateShader(GL_VERTEX_SHADER);
    glShaderSource(vertexShader, 1, &vertexShaderSource, nullptr);
    glCompileShader(vertexShader);

    auto fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
    glShaderSource(fragmentShader, 1, &fragmentShaderSource, nullptr);
    glCompileShader(fragmentShader);

    auto program = glCreateProgram();
    glAttachShader(program, vertexShader);
    glAttachShader(program, fragmentShader);

    glLinkProgram(program);
    glUseProgram(program);

    // in core profile, at least 1 vao is needed
    GLuint vao;
    glCreateVertexArrays(1, &vao);
    glBindVertexArray(vao);

    std::array<GLfloat, 4> clearColour;

    while (!glfwWindowShouldClose(windowPtr)) {

        auto currentTime = duration<float>(system_clock::now() - startTime).count();
        clearColour = {std::sin(currentTime) * 0.5f + 0.5f, std::cos(currentTime) * 0.5f + 0.5f,
                       0.2f, 1.0f};

        glClearBufferfv(GL_COLOR, 0, clearColour.data());

        glDrawArrays(GL_TRIANGLES, 0, 3);
        glfwSwapBuffers(windowPtr);
        glfwPollEvents();
    }

    glfwTerminate();
}

We

This errorHandler::MessageCallback function doesn’t exist yet, so we will write that next. It has to adhere to a specific signature so that when opengl calls it, it can pass the right arguments. Enabling GL_DEBUG_OUTPUT_SYNCHRONOUS is also useful as it will allow us to use the debugger in our c++ program if we need to to ensure that we get the right output when we step through our program (because OpenGL can be asynchronous in its operation, debug messages could appear later than we want them too).

Lets create a new file error_handler.hpp. We won’t be using too many extra files in this series, but to keep the main program cpp a bit leaner, I’ve opted for these header only files for ‘utility’ type functionality only. It does increase compile time a little, but its the order of seconds, not minutes, and we are here to learn so who cares.

#pragma once
#include <fmt/color.h>
#include <fmt/core.h> // for fmt::print(). implements c++20 std::format

#include <glbinding/gl/gl.h>
#include <iostream>
#include <string>
#include <unordered_map>

using namespace gl;

namespace errorHandler {

void MessageCallback(GLenum source, GLenum type, GLuint id, GLenum severity,
                     GLsizei length, const GLchar* message,
                     const void* userParam) {
    std::string src = errorSourceMap.at(source);
    std::string tp = errorTypeMap.at(type);
    std::string sv = severityMap.at(severity);
    fmt::print(
        stderr,
        "GL CALLBACK: {0:s} type = {1:s}, severity = {2:s}, message = {3:s}\n",
        src, tp, sv, message);
}

} // namespace errorHandler
… previous code (click to expand)
#pragma once
#include <fmt/color.h>
#include <fmt/core.h> // for fmt::print(). implements c++20 std::format

#include <glbinding/gl/gl.h>
#include <iostream>
#include <string>
#include <unordered_map>

using namespace gl;

namespace errorHandler {
static const std::unordered_map<GLenum, std::string> errorSourceMap{
    {GL_DEBUG_SOURCE_API, "SOURCE_API"},
    {GL_DEBUG_SOURCE_WINDOW_SYSTEM, "WINDOW_SYSTEM"},
    {GL_DEBUG_SOURCE_SHADER_COMPILER, "SHADER_COMPILER"},
    {GL_DEBUG_SOURCE_THIRD_PARTY, "THIRD_PARTY"},
    {GL_DEBUG_SOURCE_APPLICATION, "APPLICATION"},
    {GL_DEBUG_SOURCE_OTHER, "OTHER"}};

static const std::unordered_map<GLenum, std::string> errorTypeMap{
    {GL_DEBUG_TYPE_ERROR, "ERROR"},
    {GL_DEBUG_TYPE_DEPRECATED_BEHAVIOR, "DEPRECATED_BEHAVIOR"},
    {GL_DEBUG_TYPE_UNDEFINED_BEHAVIOR, "UNDEFINED_BEHAVIOR"},
    {GL_DEBUG_TYPE_PORTABILITY, "PORTABILITY"},
    {GL_DEBUG_TYPE_PERFORMANCE, "PERFORMANCE"},
    {GL_DEBUG_TYPE_OTHER, "OTHER"},
    {GL_DEBUG_TYPE_MARKER, "MARKER"}};

static const std::unordered_map<GLenum, std::string> severityMap{
    {GL_DEBUG_SEVERITY_HIGH, "HIGH"},
    {GL_DEBUG_SEVERITY_MEDIUM, "MEDIUM"},
    {GL_DEBUG_SEVERITY_LOW, "LOW"},
    {GL_DEBUG_SEVERITY_NOTIFICATION, "NOTIFICATION"}};
… previous code (click to expand)
void MessageCallback(GLenum source, GLenum type, GLuint id, GLenum severity,
                     GLsizei length, const GLchar* message,
                     const void* userParam) {
    std::string src = errorSourceMap.at(source);
    std::string tp = errorTypeMap.at(type);
    std::string sv = severityMap.at(severity);
    fmt::print(
        stderr,
        "GL CALLBACK: {0:s} type = {1:s}, severity = {2:s}, message = {3:s}\n",
        src, tp, sv, message);
}

} // namespace errorHandler
GL CALLBACK: SOURCE_API type = ERROR, severity = HIGH, message = GL_INVALID_OPERATION error generated. Array object is not active.  

Amazing! We can see that we are getting a message telling us that the Array object is not active. Awesome. Lets re-enable it then!

I have seen on my Nvidia card that this message is printed sometimes….

“GL CALLBACK: SOURCE_API type = PERFORMANCE, severity = MEDIUM, message = Program/shader state performance warning: Fragment shader in program 3 is being recompiled based on GL state.”

Apparently from my internet searching, this is okay so if you see it, feel free to ingore it…

So this is all very good,

Checking for Shader Compiler Errors

This is a nice state to be in, but there is still one thing that the debug output cannot tell us, and that is what errors are occurring in our shaders. If we get something wrong in our shader (for example if we accidently set the out colour attribute in the vertex shader to a vec4 and not the vec3 it should be), we will get these messages printed…

GL CALLBACK: SOURCE_API type = ERROR, severity = HIGH, message = GL_INVALID_OPERATION error generated. <program> has not been linked, or is not a program object.
GL CALLBACK: SOURCE_API type = ERROR, severity = HIGH, message = GL_INVALID_OPERATION error generated. <program> object is not successfully linked, or is not a program object.

But we don’t get told what was wrong. That is where we have to step in to do a little bit of extra work.

OpenGL has some functions that allow us to query whether the shader compilation was successful and if not, what messages the compiler spat out. Let write a function that wraps up that functionality so we can check the result of the shader compilation after we call glCompileShader().

bool checkShader(GLuint shaderIn, std::string shaderName) {
    GLboolean fShaderCompiled = GL_FALSE;

Here we write the signature of out helper function, which takes a shader ID (the id that was returned to us when we called glCreateShader), and also some ‘user’ text that we can pass in to print to the console which will help indication which shader we are printing information for. We also set up a variable ‘fShaderCompiled’ that we will want OpenGL to set for us next….

    glGetShaderiv(shaderIn, GL_COMPILE_STATUS, &fShaderCompiled);
    if (fShaderCompiled != GL_TRUE) {

Then we use the OpenGL function glGetShaderiv to get some information about the shader that we are interested in. We pass in the shader ID inShader, and the piece of information we are after which is the GL_COMPILE_STATUS. We tell it to set the value of the variable we prepared just before based on wether the compilation was successful or not. If was wasn’t, then the if statement will enter it’s block…

        fmt::print(stderr, "Unable to compile {0} shader {1}\n", shaderName,
                   shaderIn);

First we print a message to the log telling the user of the program that something was wrong.

        GLint log_length;
        glGetShaderiv(shaderIn, GL_INFO_LOG_LENGTH, &log_length);

Then we will use the same glGetShaderiv function to return to us the length of the compiler log message.

        std::vector<char> v(log_length);
        glGetShaderInfoLog(shaderIn, log_length, nullptr, v.data());

Then based on that log length, we will create a vector of chars big enough for OpenGL to store the message in. Using the glGetShaderInfoLog() function, OpenGL will store the compiler message in that vector for us.

        fmt::print(stderr, fmt::fg(fmt::color::light_green), "{}\n", v.data());
        return false;
    }
    return true;
}

Then we simply print that message. Then we return false to let any caller detect wether the compiler failed and to do something about it if they so wish. If there wasn’t any error, we return true;

The reason we use a vector of chars and not a std::string is that until c++17, std:::string doesn’t provide a non const data() method to give to OpenGL to write into. The vector of chars works for c++11 and is fine in this case.

Now if we use this function right after compiling the shaders, we get a message containing the line number in the shader of where the error occurred and what the error was.

    auto vertexShader = glCreateShader(GL_VERTEX_SHADER);
    glShaderSource(vertexShader, 1, &vertexShaderSource, nullptr);
    glCompileShader(vertexShader);
    errorHandler::checkShader(vertexShader, "Vertex");

    auto fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
    glShaderSource(fragmentShader, 1, &fragmentShaderSource, nullptr);
    glCompileShader(fragmentShader);
    errorHandler::checkShader(fragmentShader, "Fragment");

That wraps things up for this chapter. Next we will be getting back to some exciting topics and seeing how we can provide vertex data from our c++ program to feed OpenGL to draw multiple triangles at locations that we set.

Modern OpenGL: Part 3, Hello Triangle

There is quite a lot to learn and absorb in this chapter. Lets get stuck in!

Goals in this chapter

  • Draw our first triangle
  • Learn about vertex and fragment shaders
  • Passing Data from a vertex shader to a fragment shader
  • out and in qualifiers
  • How attributes are interpolated across triangles
  • compiling and linking shader programs
  • the glDrawArrays() function

Drawing a Triangle

That’s why we are here isn’t it? To draw triangles? Well drawing triangles themselves isn’t much fun. But you can make other things out of triangles. So we must walk before we can run.

OpenGL draws triangle by drawing 3 vertices at a time.

We can draw triangles by calling the glDrawArrays() function. We have to tell it what type of geometry to draw (in this case GL_TRIANGLES), an offset paramter (which we will cover in a later chapter) and how many vertices to draw.

When we ask OpenGL to draw 3 vertices when the mode is set to GL_TRIANGLES, it will connect them up and fill in the middle with pixels.

Here we are seeing 3 vertices that we have requested to be drawn by calling the glDrawTriangles function. We haven’t yet specified where those vertices are positioned in space yet (we will get to that very soon) but OpenGL assumes that they form a triangle and will interpret them as such and ‘fill in’ the middle for us.

The glDrawTriangles function just instructs OpenGL to draw triangles and doesn’t require that the positions be provided by your app yet. You can think of it like you are writing a ‘blank check’ where you specify how many vertices you want, but the positions will be specified at a later time. How? We will get to that soon.

If we want to draw more than 1 triangle we can simply draw more vertices. 3 for every triangle.

vertices 0, 1 & 2 form the first triangle, and vertices 3, 4 & 5 form the second. notice how the triangles are defined by the counter clockwise order of the points
You might be wondering, why points 0 & 3, and 2 & 4 are separate points if they share the same position. Isn’t that a waste telling opengl to draw an extra vertex there? You are right! But for now we are going to draw two separate triangle and move on to reusing vertices in chapter 13.

Setting Vertex Positions

We need a way to tell OpenGL where these vertices are, so it knows where to draw them. We need a way to process each vertex and output a position. We do that by using Shader Programs. These are actual programs that we write in a language called GLSL (GL Shading Language). They have a main function in which we set the position of the vertex. What does this program look like? Is it a big for loop that sets each vertex position one after the other? No actually, it is a program that each vertex get run on individually. That’s right, a little program is run for each vertex that sets the position for that vertex.

What does one of these programs look like?

Vertex Shader

#version 460 core

void main(){
    gl_Position = vec3(1.f, 2.71f, 3.1459f);
}

Here we see that it looks very much like other c languages. We have the #version 460 core line which tells opengl which version of the shader language we want this to be. New features are added in newer versions so we want to make sure that a certain feature set is available to us.

Then the main() function is what will be executed for each vertex. We set a special variable call gl_Position. This is the in-built variable that we have to write to to tell OpenGL where this particular vertex is positioned. OpenGL can then go on and draw the triangle using that position for that vertex. In this case, if we ran this program over all 3 vertices, then the vertices would all be at a single point and the triangle would have no area and hence wouldn’t be visible. That’s not good. So we need a way to tell each individual vertex to have it’s own unique position.

One way we could do that is to declare an array directly in the shader program itself.

#version 460 core

const vec4 positions[] = vec4[]( vec4(-0.5f, -0.7f,    0.0, 1.0), 
                                 vec4( 0.5f, -0.7f,    0.0, 1.0),    
                                 vec4( 0.0f,  0.6888f, 0.0, 1.0));

void main(){
    gl_Position = ???
}

But how do we index into that array for that particular vertex? Luckily in this situation, OpenGL provides us with a read-only built-in integer variable called gl_VertexID which is the number of the vertex being drawn. If we use that to index into the array, then each vertex will run the program, and get the right value written to gl_Position.

#version 460 core

const vec4 positions[] = vec4[]( vec4(-0.5f, -0.7f,    0.0, 1.0), 
                                 vec4( 0.5f, -0.7f,    0.0, 1.0),    
                                 vec4( 0.0f,  0.6888f, 0.0, 1.0));   

void main(){
    gl_Position = vertices[gl_VertexID];  
}

This diagram is a rough approximation of what is happening. We call glDrawArrays, tell it to draw 3 vertices and because it is told that the mode is GL_TRIANGLES, then we get a single triangle for those 3 vertices. Then each vertex will run the shader program that we wrote and get its position set by indexing into the array. The triangle will get Rasterized which means the stage where the pixels are filled with colour values. But there are a couple of issues that we need to consider…

  1. What colour will the pixels be set to?
    • Right now we haven’t stated what value the filled pixels will have. Can we even call them pixels yet?
  2. What if we have more than 1 triangle or want the values to change?
    • If we draw 6 vertices, or 9, or any amount, that means that we need a massive array in our vertex shader to be able to index into. Also we can’t access that array at the moment. Its statically compiled into the shader program. That isn’t feasible. In the chapter 5 we will tackle this issue (spoiler alert, we will be looking at vertex buffers).

For now lets look at the first issue, which is “what colour do we want to set the pixels to be and how do we do that?”

Setting Colour of Pixels

So we have sent our vertex positions, and the rasterizer has filled in where the triangle is going to be. But what has it filled? How can we colour the pixels if there aren’t any yet? Well that where fragments come in. You can think of fragments as like pixels that haven’t been born yet. They are a placeholder until we set what the colour is and then there are various process to get them turned into a pixel (outside the scope of this chapter).

So we have our fragments which are the tiny blank canvases which wil become our pixels, we just need to tell OpenGL what colour they should be. We do that with some thing called a fragment shader.

Fragment Shader – https://www.khronos.org/opengl/wiki/Fragment_Shader

Just like Vertex shaders which ran a little program over every vertex, we can run a little program over every fragment to set it’s colour. Just like with vertex shaders, we do that by setting attributes. The attribute that we set though is no longer a built-in like gl_Position was. We have to define an ‘out’ attribute of type vec4 that we will write to.

#version 460 core

out vec4 finalColor;

void main(){
    finalColor= vec4(1.0f, 1.0f, 1.0f, 1.0f);
}

This example shader sets the output colour of the fragment to pure white with an alpha value of 1 (fully opaque).

The values that we write to out attributes to set fragments are sent to temporary locations called framebuffers. This is a slightly advanced topic and the order and type of output attributes from fragment shaders and how they attach to framebuffers won’t be tackled now. All you need to know for now is that if you define 1 attribute of vec4 type, it will get automatically taken care of for you to render to the main ‘colour’ part of the framebuffer.

This example sets the colour for every fragment the same colour. What if we want to set each fragment a different colour? Setting the colour for each individual fragment could get messy if we decide to do it like we did with the vertex shader where we created an array in the shader itself. There are a couple of reasons why. First, we don’t know ahead of time how many fragments there are as the developer of the application (hopefully you) can make the resolution of the window any arbitrary size. Secondly, the array would have to be quite large as there could be hundreds, thousands or even millions of fragments. Thirdly, how would each instance of the fragment program decide which entry in the array it would index into? Before we used gl_vertexID, but there is no such equivelent for fragment shaders. (we’ll actually there kind of is but I’m not going to dwell on it here. Message me if you want to know more).

The alternative is to specify colours on the vertex shader and have the fragment shader recieve those colours as an attribute. So lets go back to our vertex shader and create an out attribute.

const char* vertexShaderSource = R"(
    #version 460 core
    out vec3 colour;

    const vec4 vertices[] = vec4[]( vec4(-0.5f, -0.7f,    0.0, 1.0), 
                                    vec4( 0.5f, -0.7f,    0.0, 1.0),    
                                    vec4( 0.0f,  0.6888f, 0.0, 1.0));   

    const vec3 colours[] = vec3[](  vec3( 1.0, 0.0, 0.0), 
                                    vec3( 0.0, 1.0, 0.0),    
                                    vec3( 0.0, 0.0, 1.0));   

    void main(){
        colour = colours[gl_VertexID];
        gl_Position = vertices[gl_VertexID];  
    }
)";

Line 3: Here we are explicitly telling OpenGL that for every vertex, we are setting a value that will be sent out of the vertex shader. In this case it is of type vec3 and we give it a name of our choosing, ‘colour’.

Lines 9-11: We are using the same trick as before by creating an array directly in the vertex shader.

Line 14: Then we use the same gl_vertexID built in variable to index into that array to pick a particular value for that vertex.

Receiving attributes in the fragment shader

So we are sending it out of the vertex shader, what happens to the attribute now? Well with the position attribute, OpenGL uses that information to know where to draw the triangle. For the colour attribute, by default it doesn’t do anything. We have to state in the the fragment shader that…

  • there is an in attribute that it will have to deal with
  • how we want that attribute to colour the pixels

The ‘in’ attribute qualifier

The first thing we do is state in the fragment shader that we are expecting an in attribute.

const char* fragmentShaderSource = R"(
    #version 460 core

    in vec3 colour;
    out vec4 finalColor;

    void main() {
        finalColor = vec4(colour, 1.0);
    }
)";

Here, we state the in attribute has type vec3 and has the name colour. You might notice this is named exactly the same as the out variable from the vertex shader. This is intentional. OpenGL will automatically detect that there is an out and in attribute on both the vertex and fragment shaders respectively and be able to figure out that they should be the source and target for each other.

There is another way to specify the association of the out and in attributes with special ‘layout qualifiers’. You don’t need to know about that just yet, but it’s worth planting a seed here so that later on I can introduce that concept and mention “remember when we associated attributes by name?! what were we thinking?!”

Attribute Interpolation

So we now have this attribute that is accessible in the fragment shader, but what value is it. And by that I mean, when the triangle is rasterized, all the fragments in the middle of the triangle are filled in. What vertex do they get the colour from? The nearest? When does it switch from one vertex colour to the next? The answer to this is attribute interpolation. Part of the triangle rasterization is that attributes that are specified per vertex are blended based on distance to the vertices. So if a fragment is in the middle of a triangle, then it will receive equal parts of each vertices colour (divided by 3 if it is in the middle only). If it is closer to one of the vertices, then it will receive a higher weighting of that vertices colour and if the fragment is exactly at the vertices position, then it will receive the full vertex colour.

TODO: insert illustration of attribute interpolation

The final code for chapter 2

Now that we know a little more about how shader programs work and how data flows between them and finally ends up on the screen, lets walk through the code for chapter 3 and see how it all fits together.

Here is the final code with the sections we haven’t changed collapsed (some parts have also been left expanded to help show the surrounding context).

… previous code (click to expand)
#include <array>
#include <chrono>     // current time
#include <cmath>      // sin & cos
#include <cstdlib>    // for std::exit()
#include <fmt/core.h> // for fmt::print(). implements c++20 std::format

// this is really important to make sure that glbindings does not clash with
// glfw's opengl includes. otherwise we get ambigous overloads.
#define GLFW_INCLUDE_NONE
#include <GLFW/glfw3.h>

#include <glbinding/gl/gl.h>
#include <glbinding/glbinding.h>

using namespace gl;
using namespace std::chrono;

int main() {

    auto startTime = system_clock::now();

    const auto windowPtr = []() {
… previous code (click to expand)
        if (!glfwInit()) {
            fmt::print("glfw didnt initialize!\n");
            std::exit(EXIT_FAILURE);
        }

        glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 4);
        glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 6);
        auto windowPtr = glfwCreateWindow(
            1280, 720, "Chapter 3 - Hello Triangle", nullptr, nullptr);
… previous code (click to expand)
        if (!windowPtr) {
            fmt::print("window doesn't exist\n");
            glfwTerminate();
            std::exit(EXIT_FAILURE);
        }
        glfwSetWindowPos(windowPtr, 520, 180);

        glfwMakeContextCurrent(windowPtr);
        glbinding::initialize(glfwGetProcAddress, false);
        return windowPtr;
  
    }();
 
    const char* vertexShaderSource = R"(
        #version 460 core
        out vec3 colour;

        const vec4 vertices[] = vec4[]( vec4(-0.5f, -0.7f,    0.0, 1.0), 
                                        vec4( 0.5f, -0.7f,    0.0, 1.0),    
                                        vec4( 0.0f,  0.6888f, 0.0, 1.0));   

        const vec3 colours[] = vec3[](  vec3( 1.0, 0.0, 0.0), 
                                        vec3( 0.0, 1.0, 0.0),    
                                        vec3( 0.0, 0.0, 1.0));   

        void main(){
            colour = colours[gl_VertexID];
            gl_Position = vertices[gl_VertexID];  
        }
    )";


This is the vertex shader that we will be telling opengl to use to

  • set the position of the vertices that will be used to rasterize the triangles
  • set the colour attribute that will be interpolated across the surface of the triangle.
    const char* fragmentShaderSource = R"(
        #version 460 core

        in vec3 colour;
        out vec4 finalColor;

        void main() {
            finalColor = vec4(colour, 1.0);
        }
    )";

This is the fragment shader that receives the pre-interpolated attribute that we can use to set the colour of the fragment. In this case we pass the attribute value directly into the final out colour by creating a vec4 value who’s first 3 arguments are specified by passing ina vec3 (our colour) and a float for the opacity to make a vec4.

    auto vertexShader = glCreateShader(GL_VERTEX_SHADER);
    glShaderSource(vertexShader, 1, &vertexShaderSource, nullptr);
    glCompileShader(vertexShader);

When we were learning about shaders in the middle of this chapter, we didn’t say how to actually get the source code into OpenGL and tell it to use that program. That is what we will do here.

  1. We first have to create a shader object by calling the glCreateShader function and passing an argument GL_VERTEX_SHADER which tells opengl that this object is going to be a, you guessed it, vertex shader. This isn’t the object that will do the work, its just a variable that represents the shader and allows us to uniquely reference it by this c++ variable (its actually just an integer id).
  2. We will associate our source code with that shader object by calling glShaderSource and passing it an address to a c-string.
  3. We have to compile the shader by calling glCompileShader. This is similar to how you compile c++ programs, its just that the OpenGL driver compiles the shader while your c++ program is running.
    auto fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
    glShaderSource(fragmentShader, 1, &fragmentShaderSource, nullptr);
    glCompileShader(fragmentShader);

We have to do the same for the fragment shader.

    auto program = glCreateProgram();
    glAttachShader(program, vertexShader);
    glAttachShader(program, fragmentShader);

The shader program isn’t ready just yet. We have to create a program object by calling glCreateProgram. Then we have to attach the vertex and fragment shaders to this program by calling glAttachShader(). This is all very boilerplate-y isn’t it. Is that a word? I don’t know.

    glLinkProgram(program);
    glUseProgram(program);

Finally we are almost ready to use the program. The second to last thing we need to do is to link the program. Compiling is the process of taking the source code and turning it into machine code, but there is one final step to produce and executable, and that is to link the separate vertex and fragment shaders. This is where all attributes are linked up and any final errors in the shaders are found.

Then we need to tell OpenGL to actually use the program. If we don’t it won’t know how to process the vertices and colour the fragments.

    // in core profile, at least 1 vao is needed
    GLuint vao;
    glCreateVertexArrays(1, &vao);
    glBindVertexArray(vao);

This is one step that I wish we didn’t have to do at this stage, as we haven’t learnt about VAO’s yet and dont need to know about them at this stage. But unfortunately, we need to have one of these things in our application for OpenGL to be able to draw things. I hate asking people to do this, but ignore this for now. We will revisit in chapter 5. Just copy and paste it into your application.

For those dying to know, VAO’s are Vertex Array Objects and they store bits of information about vertex data, type, format and location for us.
… previous code (click to expand)
    std::array<GLfloat, 4> clearColour;

    while (!glfwWindowShouldClose(windowPtr)) {

… previous code (click to expand)
        auto currentTime =
            duration<float>(system_clock::now() - startTime).count();
        clearColour = {std::sin(currentTime) * 0.5f + 0.5f,
                       std::cos(currentTime) * 0.5f + 0.5f, 0.2f, 1.0f};

        glClearBufferfv(GL_COLOR, 0, clearColour.data());

        glDrawArrays(GL_TRIANGLES, 0, 3);

And now we have told OpenGL what shader program to use, we just have to tell it to draw a triangle by calling the glDrawArrays() function with 3 vertices and you should see a triangle!

… previous code (click to expand)
        glfwSwapBuffers(windowPtr);
        glfwPollEvents();
    }

    glfwTerminate();
}

Thats the end of this chapter! Phew! That was actually a lot of information and you have just ingested a massive bulk of what OpenGL is all about. No doubt you may have experienced an bug here and there due to typos or mismatches attributes in your shader. If something did go wrong in your c++ program code, hopefully the c++ compiler was able to give you some kind of indication as to where or what the error was. But how do we know if our shader is compiling and linking correctly? You might be getting a black triangle or even no triangle at all and not know why. That can be VERY frustrating, let me tell you (you probably know). I’ve been away at 2am many times trying to get opengl to render a triangle and not know why it isn’t working.

In the next chapter, we will see how we can easily enable some built in debugging features in OpenGL so that if something does go wrong, we can at least be notified about it and even maybe be told what is wrong. Catch you later.

Dokipen

Modern OpenGL: Part 2 Window Creation

Now we get to the fun part!

This chapter will see us…

  • create a window in our operating system,
  • start a render loop
  • clear the background colour

Headers

Lets start with the headers that we need to include

#include <chrono> // current time
#include <cmath> // sin & cos
#include <array>
#include <cstdlib>    // for std::exit()

#include <fmt/core.h> // for fmt::print(). implements c++20 std::format

// this is really important to make sure that glbindings does not clash with
// glfw's opengl includes. otherwise we get ambigous overloads.
#define GLFW_INCLUDE_NONE
#include <GLFW/glfw3.h>

#include <glbinding/gl/gl.h>
#include <glbinding/glbinding.h>

We start with some standard library headers. We use…

  • <chrono> to be able to query the current time
  • <cmath> so we can feed that time into the sin and cos functions to get some animated values.
  • <array> to store some colour values and
  • <cstdlib> for exiting the program if our window isn’t initialized correctly

Then more interestingly we will use…

  • <fmt/core.h>

…to be able to print formatted text to the console. This deviates from using c++ iostreams and std::cout to output from our program. The fmt library is being standardized into c++ and part of it is already in c++20. We use the library here which is c++11 compatible and has the fmt::print() function which we will use extensively.

Then we need a library that will actually open a window for us. We are using GLFW so we will include…

  • <GLFW/glfw3.h>

the header along with a special #define GLFW_INCLUDE_NONE which will allow us to combine glfw with the next library…

  • <glbinding/gl/gl.h>
  • <glbinding/glbinding.h>

which gives us access to all of opengl’s functions (there are ways to do this manually but this library makes it really easy).

using namespace gl;
using namespace std::chrono;

Here we use some ‘using’ aliases to make our code a bit more readable and to also reduce the need to put gl:: in front of all of the OpenGL calls. glbinding actually exposes the opengl functions within it’s own namespace, so this effectively make our program look like its calling opengl functions directly.

int main() {

    auto startTime = system_clock::now();

So here we are in the entry point to our program. We start off by capturing the current time using std::chrono. This will allow us to do animation.

    const auto windowPtr = []() {
        if (!glfwInit()) {
            fmt::print("glfw didnt initialize!\n");
            std::exit(EXIT_FAILURE);
        }

Next we start the initialization process. This is where we will be getting our program ready to be able to use OpenGL. First is calling the glfwInit() function to tell glfw to get ready to do its thing. If glfw failed to initialize then we exit the program. Rather than rip out this code into a separate function, I’ve put this section into a c++ immediately invoked lambda. This is pretty much like any other function, just that its declared ‘in place’. There is no reason to do this in this case as its code that it only runs once and could have been written without it but it indents the code and its a feature that we will use in future chapters.

      
        glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 4);
        glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 6);

Here we are setting a hint to glfw that we will want it to create an OpenGL context that is a particular version. The context is the ‘instance’ of opengl that we send commands to. We want the latest and greatest so lets ask for 4.6

        auto windowPtr = glfwCreateWindow(
            1280, 720, "Chapter 1 - Window Creation", nullptr, nullptr);

Now we actually ask glfw to create our window! We give it the dimensions that we want our window to have as well as a title that will be displays in the title bar. It returns a raw pointer to a GLFWWindow.

        if (!windowPtr) {
            fmt::print("window doesn't exist\n");
            glfwTerminate();
            std::exit(EXIT_FAILURE);
        }

If the pointer returned was null, then glfw had trouble creating the window and we terminate the glfw library and exit the program.

        glfwSetWindowPos(windowPtr, 520, 180);
        glfwMakeContextCurrent(windowPtr);
        glbinding::initialize(glfwGetProcAddress, false);
        return windowPtr;
    }();

Then we instruct glfw to make our window be associated with the current opengl context.

Then we call on the glbindings library to initialize. This will use the glfwGetProcAddress function to create for us all of the opengl functions that the default gl header doesn’t expose. This has to reach into some deep guts of opengl and if we did all of that boilerplate manually, then it would look pretty ugly, so calling this one function really helps us out here. Also it can do this lazily so that it only does this when we call the functions on demand (thats what the false parameter does. If we wanted glbindings to do this work eagerly up front, we could call it with true).

    std::array<GLfloat, 4> clearColour;

We will want to clear the background of our window so we create a std::array of 4 floats which are the Red, Green, Blue and Alpha values that we want to clear. We don’t initialize it yet as we will be filling it our with live values inside our render loop.

    while (!glfwWindowShouldClose(windowPtr)) {

        auto currentTime =
            duration<float>(system_clock::now() - startTime).count();

        clearColour = {std::sin(currentTime) * 0.5f + 0.5f,
                       std::cos(currentTime) * 0.5f + 0.5f, 0.2f, 1.0f};

        glClearBufferfv(GL_COLOR, 0, clearColour.data());
        glfwSwapBuffers(windowPtr);
        glfwPollEvents();
    }

Here we are in our render loop. In each loop iteration we check for the condition of wether the window should close. glfw can tell us this based on wether the you or the user clicked on the close window button, or maybe the os closed the window for some reason.

Then we calculate the current time since the program started. We do that by taking the difference between the current time (which is measured from 1970 or something like that( and the time captured at the beginning of the program. Using that time, we set the value of the clear colour so that the colours sweep though various values as the sine and cosine waves propogate though time.

Then we actually clear the colour buffer using that colour we have just set.

Once the buffer is clear, we then tell glfw to swap the back buffer to show the image that we have just drawn with OpenGL

    glfwTerminate();
}

And finally we terminate glfw so it can wrap itself up. We now have our program displaying some pretty colours!