tl;dr The vao caches the calls to glVertexAttribPointer
et. al.
Every call to glVertexAttribPointer
, glEnableVertexAttribArray
and the binding of gl_Element_Array
will store the parameters into the currently bound vao. In the case of glVertexAttribPointer
it will also store the current binding to GL_VERTEX_ARRAY
in the vao.
This is a major help when drawing a lot of meshes because then the render loop turns from
void drawMesh(Mesh[] meshes){
foreach(mesh in meshes){
glBindBuffer(GL_ARRAY_BUFFER, mesh.vbo);
glVertexAttribPointer(posAttrLoc, 3, GL_FLOAT, false, sizeof(Vertex), mesh.vboOffset + offsetof(Vertex, pos));
glVertexAttribPointer (normalAttrLoc, 3, GL_FLOAT, false, sizeof(Vertex), mesh.vboOffset + offsetof(Vertex, normal));
//...
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, mesh.ebo);
glDrawElements(GL_TRIANGLES, mesh.vertexCount, GL_UNSIGNED_INT, mesh.indexOffset);
}
}
into
//done once per mesh on load
void prepareMeshForRender(Mesh mesh){
glBindVertexArray(mesh.vao);
glBindBuffer(GL_ARRAY_BUFFER, mesh.vbo);
glVertexAttribPointer (posAttrLoc, 3, GL_FLOAT, false, sizeof(Vertex), mesh.vboOffset + offsetof(Vertex, pos));//will associate mesh.vbo with the posAttrLoc
glEnableVertexAttribArray(posAttrLoc);
glVertexAttribPointer (normalAttrLoc, 3, GL_FLOAT, false, sizeof(Vertex), mesh.vboOffset + offsetof(Vertex, normal));
glEnableVertexAttribArray(normalAttrLoc);
//...
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, mesh.ebo); //this binding is also saved.
glBindVertexArray(0);
}
void drawMesh(Mesh[] meshes){
foreach(mesh in meshes){
glBindVertexArray(mesh.vao);
glDrawElements(GL_TRIANGLES, mesh.vertexCount, GL_UNSIGNED_INT, mesh.indexOffset);
}
}
This means a lot less calls and the driver can do some optimizations because it knows which vertex formats will be used. (some hardware has to do software vertex pulling so suddenly having a new format will require a patch of the vertex shader).
In version 4.3 (or with extension ARB_separate_attrib_format) there is a new way to bind the vertex attributes where the vao holds the format separately from the buffer binding to help exactly the above mentioned hardware:
//accessible constant declarations
constexpr int vertexBindingPoint = 0;
constexpr int texBindingPoint = 1;// free to choose, must be less than the GL_MAX_VERTEX_ATTRIB_BINDINGS limit
//during initialization
glBindVertexArray(vao);
glVertexAttribFormat(posAttrLoc, 3, GL_FLOAT, false, offsetof(Vertex, pos));
// set the details of a single attribute
glVertexAttribBinding(posAttrLoc, vertexBindingPoint);
// which buffer binding point it is attached to
glEnableVertexAttribArray(posAttrLoc);
glVertexAttribFormat(normalAttrLoc, 3, GL_FLOAT, false, offsetof(Vertex, normal));
glVertexAttribBinding(normalAttrLoc, vertexBindingPoint);
glEnableVertexAttribArray(normalAttrLoc);
glVertexAttribFormat(texAttrLoc, 2, GL_FLOAT, false, offsetof(Texture, tex));
glVertexAttribBinding(texAttrLoc, texBindingPoint);
glEnableVertexAttribArray(texAttrLoc);
//...
Then during draw you keep the vao bound and only change the buffer bindings.
void drawMesh(Mesh[] mesh){
glBindVertexArray(vao);
foreach(mesh in meshes){
glBindVertexBuffer(vertexBindingPoint, mesh.vbo, mesh.vboOffset, sizeof(Vertex));
glBindVertexBuffer(texBindingPoint, mesh.texVbo, mesh.texVboOffset, sizeof(Texture));
// bind the buffers to the binding point
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, mesh.ebo);
glDrawElements(GL_TRIANGLES, mesh.vertexCount, GL_UNSIGNED_INT, mesh.indexOffset);
//draw
}
}
note: code sourced from now defunct docs.SO article on it cached copy available here