Vertex Array Objects - Confusion regarding exactly what state information is saved about the currently bound vertex buffer
Asked Answered
D

2

9

I'm working through the excellent tutorials at arcsynthesis while building a graphics engine and have discovered I don't understand VAOs as much as I thought I had.

From the tutorial Chapter 5. Objects In Depth

Buffer Binding and Attribute Association

You may notice that glBindBuffer(GL_ARRAY_BUFFER) is not on that list, even though it is part of the attribute setup for rendering. The binding to GL_ARRAY_BUFFER is not part of a VAO because the association between a buffer object and a vertex attribute does not happen when you call glBindBuffer(GL_ARRAY_BUFFER). This association happens when you call glVertexAttribPointer.

When you call glVertexAttribPointer, OpenGL takes whatever buffer is at the moment of this call bound to GL_ARRAY_BUFFER and associates it with the given vertex attribute. Think of the GL_ARRAY_BUFFER binding as a global pointer that glVertexAttribPointer reads. So you are free to bind whatever you want or nothing at all to GL_ARRAY_BUFFER after making a glVertexAttribPointer call; it will affect nothing in the final rendering. So VAOs do store which buffer objects are associated with which attributes; but they do not store the GL_ARRAY_BUFFER binding itself.

I initially missed the last line "...but they do not store the GL_ARRAY_BUFFER binding itself". Before I noticed this line I thought that the currently bound buffer was saved once glVertexAttribPointer was called. Missing this knowledge, I built a mesh class and was able to get a scene with a number of meshes rendering properly.

A portion of that code is listed below. Note that I do not call glBindBuffer in the draw function.

// MESH RENDERING

/* ...            */
/* SETUP FUNCTION */
/* ...            */

// Setup vertex array object
glGenVertexArrays(1, &_vertex_array_object_id);
glBindVertexArray(_vertex_array_object_id);

// Setup vertex buffers  
glGenBuffers(1, &_vertex_buffer_object_id);
glBindBuffer(GL_ARRAY_BUFFER, _vertex_buffer_object_id);
glBufferData(GL_ARRAY_BUFFER, _vertices.size() * sizeof(vertex), &_vertices[0], GL_STATIC_DRAW);

// Setup vertex attributes
glEnableVertexAttribArray(e_aid_position);
glEnableVertexAttribArray(e_aid_normal);
glEnableVertexAttribArray(e_aid_color);
glEnableVertexAttribArray(e_aid_tex);

glVertexAttribPointer(e_aid_position, 3, GL_FLOAT, GL_FALSE, sizeof(vertex), (GLvoid*)offsetof(vertex, pos));
glVertexAttribPointer(e_aid_normal,   3, GL_FLOAT, GL_FALSE, sizeof(vertex), (GLvoid*)offsetof(vertex, norm));
glVertexAttribPointer(e_aid_color,    4, GL_FLOAT, GL_FALSE, sizeof(vertex), (GLvoid*)offsetof(vertex, col));
glVertexAttribPointer(e_aid_tex,      2, GL_FLOAT, GL_FALSE, sizeof(vertex), (GLvoid*)offsetof(vertex, tex));  

/* ...           */
/* DRAW FUNCTION */
/* ...           */

glBindVertexArray(_vertex_array_object_id);  
glDrawArrays(GL_TRIANGLES, 0, _vertices.size());

Now that I'm about to start lighting I wanted to get some debug drawing up so I could verify all my normals are correct. Currently I just store all lines to be rendered for a frame in a vector. Since this data will likely change every frame, I'm using GL_DYNAMIC_DRAW and specify the data right before I render it.

Initially when I did this I would get garbage lines that would just point off into infinity. The offending code is below:

// DEBUG DRAW LINE RENDERING 

/* ...            */
/* SETUP FUNCTION */
/* ...            */

// Setup vertex array object
glGenVertexArrays(1, &_vertex_array_object_id);
glBindVertexArray(_vertex_array_object_id);

// Setup vertex buffers  
glGenBuffers(1, &_vertex_buffer_object_id);
glBindBuffer(GL_ARRAY_BUFFER, _vertex_buffer_object_id);
  // Note: no buffer data supplied here!!!

// Setup vertex attributes
glEnableVertexAttribArray(e_aid_position);
glEnableVertexAttribArray(e_aid_color);

glVertexAttribPointer(e_aid_position, 3, GL_FLOAT, GL_FALSE, sizeof(line_vertex), (GLvoid*)offsetof(line_vertex, pos));
glVertexAttribPointer(e_aid_color,    4, GL_FLOAT, GL_FALSE, sizeof(line_vertex), (GLvoid*)offsetof(line_vertex, col));

/* ...           */
/* DRAW FUNCTION */
/* ...           */

glBindVertexArray(_vertex_array_object_id);
  // Specifying buffer data here instead!!!
glBufferData(GL_ARRAY_BUFFER, _line_vertices.size() * sizeof(line_vertex), &_line_vertices[0], GL_DYNAMIC_DRAW);
glDrawArrays(GL_LINES, 0, _line_vertices.size());

After a bit of hunting, as well as finding the detail I missed above, I found that if I call glBindBuffer before glBufferData in the draw function, everything works out fine.

I'm confused as to why my mesh rendering worked in the first place given this. Do I only need to call glBindBuffer again if I change the data in the buffer? Or is behavior undefined if you don't bind the buffer and I was just unlucky and had it work?

Note that I am targeting OpenGL version 3.0.

Dahna answered 21/6, 2013 at 2:59 Comment(0)
A
11

Do I only need to call glBindBuffer again if I change the data in the buffer?

Yes, The VAO object remembers which buffers were bound each time you called glVertexAttribPointer whilst that VAO was bound, so you don't usually need to call glBindBuffer again. If you want to change the data in the buffer however, OpenGL needs to know which buffer you are changing, so you need to call glBindBuffer before calling glBufferData. It is irrelevant which VAO object is bound at this point.

Ace answered 21/6, 2013 at 11:7 Comment(3)
Ok, that makes more sense. I have one minor detail that I'm still a little fuzzy on. Let's say I call glBindVertexArray with VAO 1 which remembers VBO 1 was bound when glVertexAttribPointer was called.Then I call glBindBuffer with VBO 2, followed by glDrawArrays. Which buffer should be used, VBO 1 or VBO 2? I don't believe this is something that one would normally do, I just want to make sure I understand what's going on.Dahna
VBO 1 will be used. Note that this is true whether or not you are using VAOs. Also you might have bound several VBOs whilst you had the VAO bound (perhaps a different one for each attribute). Once you have called glVertexAttribPointer the buffer bound at that time is the one which will be used.Ace
OpenGL still confuses me a bunch. When exactly does the VBO "attach" to the VAO while the VAO is bound? Upon calling glGenBuffers or glBindBuffers? (Again, while the VAO is currently bound, of course).Discriminator
F
1

A Vertex Array object holds the data set by glEnableVertexAttribArray, glDisableVertexAttribArray, glVertexAttribPointer, glVertexAttribIPointer, glVertexAttribDivisor and gl.bindBuffer(GL_ELEMENT_ARRAY_BUFFER)

To put it another way you could define a VAO as

struct VertexAttrib {
  GLint size;           // set by gVertexAttrib(I)Pointer
  GLenum type;          // set by gVertexAttrib(I)Pointer
  GLboolean normalize;  // set by gVertexAttrib(I)Pointer
  GLsizei stride;       // set by gVertexAttrib(I)Pointer
  GLint buffer;         // set by gVertexAttrib(I)Pointer (indirectly)
  void* pointer;        // set by gVertexAttrib(I)Pointer
  GLint divisor;        // set by gVertexAttribDivisor
  GLboolean enabled;    // set by gEnable/DisableVertexAttribArray
};

struct VertexArrayObject {
  std::vector<VertexAttrib> attribs;
  GLuint element_array_buffer;  // set by glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ..)
};

How many attributes there are can be queried with

GLint num_attribs;
glGetIntegerv(GL_MAX_VERTEX_ATTRIBS, &num_attribs)

You can think of GL's global state has having a pointer to a VertexArrayObject that is set with glBindVertexArray

struct GLGlobalState {
  VertexArrayObject default_vao;
  VertexArrayObject* current_vao = &default_vao;
  ...

  GLint current_array_buffer;  // set by glBindBuffer(GL_ARRAY_BUFFER, ...)
}
GLGloalState gl_global_state;

void glBindVertexArray(GLint vao) {
  gl_global_state.current_vao = vao == 0 ? &default_vao : getVAOById(vao);
}

And you can think of the other functions listed above working like this

void glEnableVertexAttribArray(GLuint index) { 
  gl_global_state.current_vao->attribs[index].enabled = GL_TRUE;
}

void glEnableVertexAttribArray(GLuint index) { 
  gl_global_state.current_vao->attribs[index].enabled = GL_FALSE;
}

void glVertexAttribPointer(GLuint index, GLint size, GLenum type, GLboolean normalized,
                      GLsizei stride, const void* pointer) { 
  VertexAttrib* attrib = &gl_global_state.current_vao->attribs[index];
  attrib->size = size;
  attrib->type = type;
  attrib->normalized = normalized;
  attrib->stride = stride;
  attrib->pointer = pointer;
  attrib->buffer = glGlobalState.current_array_buffer;
}

void glVertexAttribDivisor(GLuint index, GLuint divisor) {
  gl_global_state.current_vao->attribs[index].divisor = divisor;
}

It might be easier to see it visually

enter image description here

from this diagram though the diagram above shows offset instead of pointer since it's from WebGL which doesn't allow client side arrays, only vertex buffers, therefore the pointer field is always interpreted as an offset.

In OpenGL pointer is a pointer to user memory if the buffer field for that attribute is 0 (set indirectly, see above). It's an offset into a buffer if the buffer for an attribute is non-zero.

One thing NOT stored in an VAO are the attribute's constant values when it's disabled. If an attribute is disabled (they default to disabled, or you call gl.disableVertexAttribArray) then that attribute will get a constant value. The constant value can be set with glVertexAttrib???. These values are global. They are not part of VAO state.

Filagree answered 4/9, 2022 at 1:9 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.