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