6
$\begingroup$

Before I get downvoted. I am not asking how to use them, I know how to use them. I am trying to understand what they are. For example, an array is actually a contiguous section of memory, a linked list is actually just a clever way of using memory addresses, a 3D array is actually just a 1D array being used in a clever fashion...

Under that logic, VBO's are sections of GPU RAM that contain rendering data. They are thus "arrays". But then what is a VAO? My current understanding is that they are magical "boxes" that somehow store machine state information for a set of VBO's, but what are they actually?

Because for example, as far as I know, you MUST bind the VBO's under a VAO to get rendering output, if you just bind the VAO you won't get outpt, so what is their purpouse?

$\endgroup$
1
  • $\begingroup$ you MUST bind the VBO's under a VAO to get rendering output, if you just bind the VAO you won't get outpt. That's not true. You can bind dummy VAO without attached VBOs and still dispatch drawcall. $\endgroup$
    – mdkdy
    Commented Nov 20, 2017 at 10:06

3 Answers 3

8
$\begingroup$

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

$\endgroup$
4
$\begingroup$

VAOs are essentially 'plumbing' objects that help get data from your buffers into your vertex shader ready for drawing. On the GPU this is handled by the vertex fetch stage, so a VAO is basically just a bundle of vertex fetch state corresponding to a particular model.

You can think of it as a bundle of pointers, but with somewhat more complexity than CPU-side pointers. Since OpenGL buffers are just a blob of untyped bytes by default (that's why you get a void* when you map them), the VAO handles type-casting (to vec3 or whatever) when fetching the data. It also deals with the size of the data not necessarily matching the stride between consecutive elements, and makes sure that each data item (position, normal, uv, etc.) is transmitted to the correct 'channel' in the vertex shader input.

You can render without a VAO in legacy OpenGL, but this is generally more of a PITA, since you need to bind each vertex attribute separately each time you draw the object. Encapsulating all these bindings in a VAO lets you set them up once, then you have a 'plug and play' object that simply needs to be bound before a draw call, and all your vertex data will be ready to go.

$\endgroup$
1
$\begingroup$

Buffer objects contain your vertex data. Vertex array objects tell OpenGL how to interpret that data. Without the VAO, OpenGL just knows that you suck some bytes into some buffers. The VAO says that "in this buffer, at byte offset X, is an array of 4-vector floats, and that data will be fed to attribute index Y."

They're state objects. VAOs are no more "magical" than this C++ data structure:

struct VertexFormat
{
  bool enabled = false;
  int attribute_index;
  VertexFormatType data_type; //Is f32, f64, or int32.
  int num_components = 4;
  bool normalized = false;
  GLenum type;
  int binding;    //The `bindings` index this attribute gets its data from.
  intptr_t offset;
};

struct VertexBufferBinding
{
  BufferObject buffer;
  intptr_t offset;
  intptr_t stride;
  int instancing;
};

struct VertexArrayObject
{
  VertexFormat formats[16];
  VertexBufferBinding bindings[16];
};

The fields of VertexFormat are specified by the glVertexAttrib*Format or glVertexAttrib*Pointer functions. In the case of the latter, it also specifies the corresponding VertexBufferBinding data.

Note that VertexBufferBinding stores a buffer object. That's where that binding point reads its data.

This structure is not magical; it simply explains how to read vertex data from its associated buffers.

$\endgroup$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.