On Vertex Array Objects
So. VAOs. Vertex Array Objects.
Perhaps one of The worst parts of OpenGL from an API design perspective, and one of the larger stumbling points for people trying to learn OpenGL.
An object with an unfortunate historical name, modified by confusingly named functions that do too much, compounded with the bind-to-edit model which has drawn so much ire to OpenGL, makes for a very bad time.
If you’ve suffered this mess, or are planning on suffering it soon, hopefully I can share something here which will make it just a little bit less bad.
So what is it?
A Vertex Array Object or VAO is a kind of container object which bundles several pieces of information together that is needed for rendering.
For most use cases the relevant pieces of information comprising a VAO are:
- A vertex format described by a set of indexed ‘attributes’.
- An ’enabled’ state for each attribute, which when set, informs opengl that it should read attribute data from a buffer.
- A set of buffer bindings, from which vertex data will be read (when enabled).
- A mapping which describes which attributes will be read from which buffer bindings.
- And finally, an optional element buffer binding, for indexed rendering.
You can picture it looking something like this:
type BufferName = u32;
struct Vao {
element_buffer: BufferName,
buffer_bindings: [BufferBinding, MAX_BUFFERS],
attributes: [Attribute; MAX_ATTRIBUTES],
}
struct BufferBinding {
buffer: BufferName,
byte_offset: usize, // Where to start reading vertex data from in the buffer
byte_stride: usize, // The distance between adjacent vertices. For packed, interleaved vertices this is sizeof(Vertex).
instance_divisor: u32, // Used for instancing.
}
struct Attribute {
enabled: bool, // Whether or not this attribute reads from a buffer.
buffer_binding_index: u32, // Which buffer binding to use.
relative_byte_offset: usize, // Where to start reading data for _this attribute_ in the bound buffer.
component_type: GLenum,
num_components: u32, // How many values form the attribute data of a single vertex, up to 4.
normalized: bool, // Whether or not integer typed attributes should be squished into [0, 1] or [-1, 1] float values.
}
During primitive assembly, the GPU will read data from each of the buffers bound to a VAO based on the data formats that each enabled attribute describes. It will then perform any conversions, if the format described by the attribute doesn’t match what the bound shader program expects.
This last part makes VAOs theoretically very flexible, as it means one shader program can be fed data from meshes with many different vertex formats. Your shaders could take floats exclusively, and still be used to render meshes containing all kinds of integer and float formats.
Sounds great! Viva la code reuse or whatever.
So whats the problem?
The Problem.
Well there are several.
The first problem is that VAOs don’t exist in any other API, and that’s for good reason. Beyond the scope of individual draw calls, there really isn’t a reason to combine vertex formats with buffer bindings.
More often than not, a given application will only have a handful of vertex formats between all its meshes and shader programs. So bundling these together means a lot of duplicated state.
Further, when VAOs were introduced in OpenGL 3.0, they were much simpler than they are now. Buffer binding points and attributes were tightly bound together, and so too were the functions that specified them. So not only was it unnecessary, it was not clear that there was any other way.
These things compounded to create a situation where:
- The API was hard enough to grok that many people just defered to tutorials without really understanding the API itself.
- Every single tutorial decided to use VAOs the exact same way, due to how the API implied they should be used.
- Later tutorials neglected to challenge the original tutorials.
Now “Every mesh gets its own VAO” is just kinda, the defacto way VAOs are used. It’s in tutorials, it’s where every beginner starts, and because noone wants to touch VAOs any more than they have to, it’s all still the original OpenGL 3.3 version of the API.
The OpenGL 3.3 Way - aka “““Modern OpenGL”””
Before we move onto the alternatives, lets look at where we’re at. What do VAOs look like in OpenGL 3.3.
Lets start with an example. We have a vertex struct:
struct Vertex {
position: [float; 3],
uv: [u16; 2], // Lets say this is normalized to [0, 1]
color: [u8; 4],
index: u32,
}
And some attributes in our vertex shader.
layout(location=0) in vec3 position;
layout(location=1) in vec2 uv;
layout(location=2) in vec4 color;
layout(location=3) in uint index;
So what does that look like?
let vertex_buffer = create_vertex_buffer();
let element_buffer = create_element_buffer();
let vertex_size = size_of::<Vertex>();
let position_offset = offset_of!(Vertex, position);
let uv_offset = offset_of!(Vertex, uv);
let color_offset = offset_of!(Vertex, color);
let index_offset = offset_of!(Vertex, index);
let mut vao = 0;
glGenVertexArrays(1, &raw mut vao);
glBindVertexArray(vao);
// These two do very different things! :(
glBindBuffer(GL_ARRAY_BUFFER, vertex_buffer); // This doesn't modify the VAO at all!
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, element_buffer); // But this does!
glEnableVertexAttribArray(0);
glEnableVertexAttribArray(1);
glEnableVertexAttribArray(2);
glEnableVertexAttribArray(3);
// Pointer casting :(
// Also this mixes buffer binding with vertex format specification :(
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, vertex_size, position_offset as *const _);
glVertexAttribPointer(1, 2, GL_UNSIGNED_SHORT, GL_TRUE, vertex_size, uv_offset as *const _);
glVertexAttribPointer(2, 4, GL_UNSIGNED_BYTE, GL_TRUE, vertex_size, color_offset as *const _);
glVertexAttribIPointer(3, 1, GL_UNSIGNED_INT, vertex_size, index_offset as *const _);
What’s wrong with this?
glVertexAttribPointer is overloaded and ugly
These functions do way too much.
They both specify the vertex format (which again, you may want to use for more than one mesh), and they attach the current GL_ARRAY_BUFFER
binding to the VAO, And associate it with an attribute.
All of that behind the name glVertexAttribPointer
.
This API requires you to have buffers ready before you can specify your vertex format. There is absolutely no hope of reuse, as swapping out buffers requires respecifying all of the attributes and their formats.
Also the necessary pointer cast for the offset parameter is ugly and needlessly confusing to beginners. Yes, Once upon a time passing actual data here might have been something you did. No, I do not care. The historical justification for an API does not matter to a beginner.
It’s just bad.
Every VAO is a Mesh. Every Mesh a VAO.
A consequence of this API is that the gut reaction is just to make one VAO for every mesh you have. In almost every tutorial out there you have something that looks like this:
class Mesh {
// ...
private:
GLuint vao;
GLuint vbo;
GLuint ebo;
};
This is… fine. But it’s also more complex than it needs to be.
If you are making a small-ish game, then you may very well only have a handful of vertex formats. And while unique VAOs for every mesh doesn’t prohibit that in any way, it doesn’t particularly help either. It’s just another object to manage for everything you want to render - whether it’s loaded from disk or generated at runtime.
Ideally, we could just have one object per vertex format, and then our meshes would compose only buffers, but with this API it’s not really worth the effort to even try.
Overzealous Unbinding
Some may feel the compulsion to unbind everything after use. This is not necessarily bad, but when it comes to VAOs it leads to one very common problem.
There is pretty much no correct way to unbind element buffers if your goal is just to “clean up” after yourself.
Unlike the GL_ARRAY_BUFFER
bind point which lives in the global context, the GL_ELEMENT_ARRAY_BUFFER
bind point lives in the VAO object itself.
This means that if you unbind it before unbinding your VAO:
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
glBindVertexArray(0);
You actually end up detaching your element buffer from your vao, so the next time you call glDrawElements
with this vao bound there’ll be no element buffer bound, and then? Crash!
“Okay” you say “then I should just unbind in the opposite order!”. No!
glBindVertexArray(0);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
Well… The spec doesn’t explicitly tell you you can’t. And some drivers may even let you get away with it.
But technically speaking this is invalid as well. As I said, the GL_ELEMENT_ARRAY_BUFFER
binding is a part of the VAO object.
So if no VAO is bound and you try to set the binding? Who knows what will happen.
If you’re lucky it’ll crash on your machine. If you’re unlucky it’ll crash on someone elses.
So just don’t bother.
Different bind targets have different, hidden preconditions!
The last problem touches on a much deeper, fundamental problem. glBindBuffer
has different requirements depending on the target.
glBindBuffer(GL_ARRAY_BUFFER, 0);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
These two lines look so similar, yet touch completely different parts of the context. One binding point lives in the global context, the other in the current bound VAO. And this gets tripped over constantly by the people I see trying to learn OpenGL.
Building a mental model of the OpenGL state machine is already a massive chore, and stuff like this only serves to make learning OpenGL significantly more difficult.
Thankfully we’re not stuck in OpenGL 3.3 any more, and we have some alternatives we can look to.
Separating Vertex Formats from Vertex Data
In OpenGL 4.3 VAOs are conceptually restructured, and gain a whole new api for interacting with them. The pseudocode at the top of this post describing the state comprising a VAO is technically a description of VAOs as of 4.3.
This new api is referred to as Separate attribute format and is described well enough on the wiki, but the tl;dr is that glVertexAttribPointer
is no longer the main way we construct VAOs.
Instead it has been split in to the three separate functions it conceptually mapped to previously:
glVertexAttribFormat*
- to describe the layout of your vertices in a bufferglBindVertexBuffer
- to actually attach a buffer to a VAO, andglVertexAttribBinding
- to describe which attributes should come from which buffers.
Now our VAO setup looks like this:
let vertex_buffer = create_vertex_buffer();
let element_buffer = create_element_buffer();
let vertex_size = size_of::<Vertex>();
let position_offset = offset_of!(Vertex, position);
let uv_offset = offset_of!(Vertex, uv);
let color_offset = offset_of!(Vertex, color);
let index_offset = offset_of!(Vertex, index);
let mut vao = 0;
glGenVertexArrays(1, &raw mut vao);
glBindVertexArray(vao);
glEnableVertexAttribArray(0);
glEnableVertexAttribArray(1);
glEnableVertexAttribArray(2);
glEnableVertexAttribArray(3);
// Associate buffer binding points with attributes. This is new.
glVertexAttribBinding(0, 0);
glVertexAttribBinding(1, 0);
glVertexAttribBinding(2, 0);
glVertexAttribBinding(3, 0);
// Yay! no more pointer casts!
glVertexAttribFormat(0, 3, GL_FLOAT, GL_FALSE, position_offset);
glVertexAttribFormat(1, 2, GL_UNSIGNED_SHORT, GL_TRUE, uv_offset);
glVertexAttribFormat(2, 4, GL_UNSIGNED_BYTE, GL_TRUE, color_offset);
glVertexAttribIFormat(3, 1, GL_UNSIGNED_INT, index_offset);
// Buffers can be bound in a completely different place, and multiple times!
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, element_buffer);
glBindVertexBuffer(0, vertex_buffer, 0, vertex_size);
This is obviously more code than we had previously, but hopefully it is clear what this is an improvement: each function now only does a single thing.
On top of being easier to learn, this also gives us quite a bit more flexibility in how we choose to use VAOs. Namely, we now have the option to describe the format of our vertex data completely independently of its storage, and conversely, we have the option to quickly swap out buffers without needing to completely respecify our vertex format.
Now we have the option to diverge from the “Old way” - “Every Mesh a VAO”.
Since many engines will only have a fixed number of vertex formats (and in many hobby projects potentially only one or two!), it is not uncommon to relegate VAOs entirely to describing vertex formats and using them to render all meshes sharing the same vertex format. This effectively means that all meshes can now just be a set of buffers to bind.
For example, instead of your rendering loop looking like this:
glUseProgram(my_shader_program);
for instance in mesh_instances {
let mesh = instance.get_mesh();
glBindVertexArray(mesh.vao);
glDrawElements(GL_TRIANGLES, 0, mesh.num_elements);
}
You might write it like this:
glUseProgram(my_shader_program);
glBindVertexArray(my_vertex_format);
for instance in mesh_instances {
let mesh = instance.get_mesh();
// It does kinda suck that we need the vertex size here, but it is what it is.
glBindVertexBuffer(0, mesh.vbo, 0, size_of::<Vertex>());
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, mesh.ebo);
glDrawElements(GL_TRIANGLES, 0, mesh.num_elements);
}
Again this is more code, but it means that creating a mesh is exceedingly simple. And since you’re likely to have far fewer unique vertex formats than meshes, having fewer objects per mesh means potentially significantly less bookkeeping over all, and it means meshes in your renderer are even simpler, dumber objects.
It also has a nice secondary benefit which is: we no longer have to pay the cost of creating all those VAOs. This won’t likely mean much for the vast majority of peoeple that read this (nor does it mean much to me), but vertex specification is not necessarily free.
Vertex format specification can require changes to the programs executed on the GPU, and so having a lot of them or respecifying them often may incur a cost. Binding a buffer however is relatively fast, so being able to do just that part, without respecification can be desirable.
Personally, the conceptual simplicity is much more compelling to me though. In many of my projects I only ever have one VAO and the rest is all just buffers, shaders and textures.
Fewer objects to think about is more better. Especially when I tend to have long hiatuses on said projects :^)
Ditching Bind-to-Edit - aka please use DSA
It is what it says on the tin: please use DSA. Bind to edit is dead.
Direct State Access is a set of apis promoted to core in OpenGL 4.5 which effectively replace the old “bind-to-edit” style apis. That is, most functions that operate exclusively on ‘opengl objects’ now have a replacement which no longer requires binding said objects to the context to modify them - which means:
- no more obsessively unbinding and occasionally messing up your VAO state.
- no more accidentally modifying objects you didn’t mean to because you called a function which binds something else to the target you’re using.
- it’s now clear which function calls interact with which objects, and
- if you really want, it is now actually possible to properly encapsulate OpenGL objects, since modifying them no longer requires touching global state :^)
So in addition to the separate attribute format functions mentioned above, we now also have these:
glVertexArrayAttribFormat
, the DSA equivalent ofglVertexAttribFormat
glVertexArrayAttribBinding
, the DSA equivalent ofglVertexAttribBinding
, andglVertexArrayVertexBuffer
, the DSA equivalent ofglBindVertexBuffer
And these:
glEnableVertexArrayAttrib
, a DSA version ofglEnableVertexAttribArray
(note ArrayAttrib -> AttribArray)glCreateVertexArrays
, the DSA equivalent ofglGenVertexArrays
(see the wiki for an explanation why this is different)glVertexArrayElementBuffer
, a DSA equivalent toglBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ...)
, and
Each of these functions (barre the last two) are effectively the same as their non-DSA equivalent, but take the VAO being modified as the first argument.
Substituting them in we get this:
let vertex_buffer = create_vertex_buffer();
let element_buffer = create_element_buffer();
let vertex_size = size_of::<Vertex>();
let position_offset = offset_of!(Vertex, position);
let uv_offset = offset_of!(Vertex, uv);
let color_offset = offset_of!(Vertex, color);
let index_offset = offset_of!(Vertex, index);
let mut vao = 0;
glCreateVertexArrays(1, &raw mut vao);
// No more binding vao, means no unbinding anything. no accidental modifications, yay!
// All functions modifying `vao` now take it as an argument.
glEnableVertexArrayAttrib(vao, 0);
glEnableVertexArrayAttrib(vao, 1);
glEnableVertexArrayAttrib(vao, 2);
glEnableVertexArrayAttrib(vao, 3);
glVertexArrayAttribBinding(vao, 0, 0);
glVertexArrayAttribBinding(vao, 1, 0);
glVertexArrayAttribBinding(vao, 2, 0);
glVertexArrayAttribBinding(vao, 3, 0);
glVertexArrayAttribFormat(vao, 0, 3, GL_FLOAT, GL_FALSE, position_offset);
glVertexArrayAttribFormat(vao, 1, 2, GL_UNSIGNED_SHORT, GL_TRUE, uv_offset);
glVertexArrayAttribFormat(vao, 2, 4, GL_UNSIGNED_BYTE, GL_TRUE, color_offset);
glVertexArrayAttribIFormat(vao, 3, 1, GL_UNSIGNED_INT, index_offset);
// Both of these modify the VAO, with a consistent API, yay!
// No global binding points!
// No more overloaded glBindBuffer touching non-global state!
// No more confusion!
glVertexArrayElementBuffer(vao, element_buffer);
glVertexArrayVertexBuffer(vao, 0, vertex_buffer, 0, vertex_size);
And I think that’s pretty neat.
Closing notes.
I hope it is safe to say that VAOs in OpenGL 4.5 are a significant improvement over what most resources would have you believe they are. And hopefully I have made some kind of case for making use of the new DSA apis and other goodies that exist in latest OpenGL.
Unfortunately VAOs will probably never have the perfect API. People will continue to get confused by the difference between glVertexArrayAttribFormat
and glVertexArrayAttribIFormat
, and OpenGL is more or less at the end of its life so probably won’t be seeing any big changes to its naming schemes any time soon.
Thankfully there is a way to almost completely remove VAOs from your projects and avoid all of their problems entirely:
Programmable Vertex Pulling
Which is a much more convenient way to deal with vertex data in my opinion, but that is a topic for another post :^)
If you can’t wait for me to write about it you can read about it on someone elses blog.