In the last 2 chapters we have learned
- how to feed buffer data into a single shader position attribute
- 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…
- Only create 1 buffer this time and upload the data to it
- Set the jump in the gap between vertices to a larger size (the size of our struct)
- Connect both shader attributes to the single buffer ‘slot’
- 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.

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).

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