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.

Leave a Comment

Your email address will not be published. Required fields are marked *