顶点数组对象 - 对于当前绑定的顶点缓冲保存了哪些状态信息存在困惑

9
我正在构建一个图形引擎,并通过arcsynthesis的教程进行学习,但是我发现我对VAO的理解不如我以为的那么深入。从第五章.深入了解对象中的教程中可以看到:
“缓冲区绑定和属性关联 您可能会注意到,即使它是渲染属性设置的一部分,glBindBuffer(GL_ARRAY_BUFFER)也没有列在该列表中。与GL_ARRAY_BUFFER的绑定不是VAO的一部分,因为当您调用glBindBuffer(GL_ARRAY_BUFFER)时,缓冲对象与顶点属性之间的关联并没有发生。这种关联是在调用glVertexAttribPointer时发生的。 当您调用glVertexAttribPointer时,OpenGL会将此调用时绑定到GL_ARRAY_BUFFER的任何缓冲区与给定的顶点属性相关联。将GL_ARRAY_BUFFER绑定视为glVertexAttribPointer读取的全局指针。因此,您可以自由地绑定任何想要或完全不绑定到GL_ARRAY_BUFFER的内容,而不会影响最终渲染。因此,VAO确实存储了哪些缓冲区对象与哪些属性相关联;但是,它们不存储GL_ARRAY_BUFFER绑定本身。”
起初我错过了最后一行“但是,它们不存储GL_ARRAY_BUFFER绑定本身”。在注意到这一行之前,我认为一旦调用glVertexAttribPointer,当前绑定的缓冲区会被保存。由于缺乏这个知识,我建立了一个网格类,并成功地渲染了一些网格的场景。
下面列出了代码的一部分。请注意,我在draw函数中没有调用glBindBuffer。
// 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());

现在我即将开始进行光照,所以我想先进行一些调试绘制,以便可以验证我的法线是否正确。目前我只是将每帧要渲染的所有线存储在一个向量中。由于这些数据可能会在每帧更改,所以我使用GL_DYNAMIC_DRAW并在渲染之前指定数据。

最初当我这样做时,我会得到一些只指向无限远处的垃圾线。以下是有问题的代码:

// 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());

经过一番搜索,我找到了之前遗漏的细节,并发现如果在绘制函数中在glBufferData之前调用glBindBuffer,一切都会正常工作。

我对于我的网格渲染一开始为什么能够正常工作感到困惑。难道只有在更改缓冲区数据时才需要再次调用glBindBuffer吗?或者如果不绑定缓冲区,行为是未定义的,我只是运气好让它起作用吗?

请注意,我针对的是OpenGL 3.0版本。

2个回答

11
只有在更改缓冲区中的数据时才需要再次调用glBindBuffer。是的,VAO对象会记住在绑定该VAO时每次调用glVertexAttribPointer时绑定的缓冲区,因此通常不需要再次调用glBindBuffer。但是,如果您想更改缓冲区中的数据,则OpenGL需要知道要更改的缓冲区,因此在调用glBufferData之前需要调用glBindBuffer。此时绑定哪个VAO对象并不重要。

2
好的,这样更有意义。我还有一个小细节不太清楚。假设我使用VAO 1调用glBindVertexArray,它会记住在调用glVertexAttribPointer时绑定了VBO 1。然后我使用glBindBuffer和VBO 2,接着调用glDrawArrays。应该使用哪个缓冲区,VBO 1还是VBO 2?我不认为这是通常会做的事情,我只是想确保我理解了发生了什么。 - Peter Clark
2
将使用VBO 1。请注意,无论您是否使用VAOs,这都是正确的。此外,当您绑定VAO时,可能已经绑定了多个VBO(可能为每个属性绑定一个不同的VBO)。一旦调用glVertexAttribPointer,那时绑定的缓冲区就是将要使用的缓冲区。 - GuyRT
OpenGL 仍然让我感到困惑。当 VBO 绑定到 VAO 上时,究竟是在调用 glGenBuffers 还是 glBindBuffers 时进行的呢?(当然,前提是 VAO 已经被绑定了)。 - IOviSpot

1

顶点数组对象保存了由glEnableVertexAttribArrayglDisableVertexAttribArrayglVertexAttribPointerglVertexAttribIPointerglVertexAttribDivisorgl.bindBuffer(GL_ELEMENT_ARRAY_BUFFER)设置的数据集。

换句话说,你可以将VAO定义为:

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, ..)
};

可以使用以下方式查询有多少个属性

GLint num_attribs;
glGetIntegerv(GL_MAX_VERTEX_ATTRIBS, &num_attribs)

你可以将GL的全局状态视为具有指向VertexArrayObject的指针,该对象使用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);
}

你可以想象以上列出的其他函数是按照这种方式工作的

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;
}

通过可视化方式可能更容易理解它。

enter image description here

this diagram 可以看出,上面的图示中显示的是 offset 而不是 pointer,因为它来自 WebGL,WebGL 不允许客户端数组,只允许顶点缓冲区,因此指针字段始终被解释为偏移量。

在 OpenGL 中,如果该属性的 buffer 字段为 0(间接设置,请参见上文),则 pointer 是指向用户内存的指针。如果属性的 buffer 是非零值,则它是缓冲区中的偏移量。

一个不会存储在 VAO 中的东西是当它被禁用时属性的常量值。如果禁用了属性(它们默认为禁用状态,或者您调用 gl.disableVertexAttribArray),那么该属性将获得一个常量值。可以使用 glVertexAttrib??? 设置这些值。这些值是全局的,它们不是 VAO 状态的一部分。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接