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