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();
}

Leave a Comment

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