VAO/VBO机制理解:OpenGL绑定VAO后渲染异常问题排查
你对VAO的工作机制存在两处关键理解偏差,同时代码里有一个会直接破坏VAO状态的错误操作,才会出现这个现象。
先明确VAO实际存储的状态
VAO根本不是「和着色器绑定、存储着色器顶点属性格式」的对象,它存储的是一整套顶点绘制的输入状态,具体包含三类信息:
- 所有顶点属性的启用/禁用状态(通过
glEnableVertexAttribArray/glDisableVertexAttribArray设置) - 每个顶点属性的完整配置:包括数据格式(分量数、数据类型、是否归一化、步长、偏移),以及调用
glVertexAttribPointer时绑定到GL_ARRAY_BUFFER靶位的VBO对象引用 - 最后一次绑定到
GL_ELEMENT_ARRAY_BUFFER靶位的EBO对象引用
这里要特别注意:GL_ARRAY_BUFFER靶位的绑定是全局上下文状态,只有调用glVertexAttribPointer的瞬间,当前绑定的VBO才会和对应属性的配置一起被记录到当前绑定的VAO中,单纯调用glBindBuffer(GL_ARRAY_BUFFER, xxx)不会修改VAO里存储的VBO关联关系。
你的代码为什么出错
VAO创建逻辑完全错误
你按着色器创建共享VAO,初始化VAO属性的时候,根本没有绑定对应物体的私有VBO,此时VAO里记录的VBO引用要么是空值,要么是你初始化时临时绑定的测试VBO,和后续要绘制的物体的VBO完全没有关系。除非你把所有同顶点格式的物体数据都塞进同一个全局大VBO靠偏移区分,否则持有独立VBO的物体是不能共享VAO的。
你在draw方法里虽然调用glBindBuffer(GL_ARRAY_BUFFER, m_vbo)绑定了当前物体的VBO,但没有重新调用glVertexAttribPointer,VAO还是会按照初始化时记录的旧VBO引用去读顶点数据,读出来的全是错误内存内容,渲染自然异常。
这也是为什么你取消s->setupVertices()的注释就正常:这行代码在绑定完当前物体VBO之后调用了glVertexAttribPointer,相当于把VAO里记录的VBO引用和属性配置临时改成了当前物体的,所以绘制能正常跑,但这相当于每帧绘制每个物体都在重复修改VAO状态,完全没用到VAO缓存顶点状态的设计初衷。draw方法末尾的EBO解绑操作会破坏VAO状态
你在绘制完成后调用了glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0),而GL_ELEMENT_ARRAY_BUFFER的绑定是直接存在当前VAO里的,这行代码会直接把当前VAO中保存的EBO引用清空,下次再绑定这个VAO时根本找不到索引数据。
额外提一句,绘制完把GL_ARRAY_BUFFER绑成0不会影响VAO状态,但完全没必要——因为绑定下一个VAO的时候,全局的VBO绑定状态会自动被VAO里记录的状态覆盖,多写这行只是无意义的API调用。
正确实现方式
不要按着色器创建共享VAO,应该为每个独立持有VBO/EBO的物体创建专属VAO,在物体初始化阶段一次性完成VAO配置,不要在绘制阶段重复设置属性:
物体初始化阶段执行一次
// 1. 创建并绑定物体专属VAO glGenVertexArrays(1, &m_vao); glBindVertexArray(m_vao); // 2. 绑定当前物体的VBO,上传顶点数据 glBindBuffer(GL_ARRAY_BUFFER, m_vbo); glBufferData(GL_ARRAY_BUFFER, m_vertexDataSize, m_vertexData, GL_STATIC_DRAW); // 3. 绑定当前物体的EBO,上传索引数据 glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, m_ebo); glBufferData(GL_ELEMENT_ARRAY_BUFFER, m_indexDataSize, m_indexData, GL_STATIC_DRAW); // 4. 配置当前物体使用的着色器对应的顶点属性,执行后VBO/EBO引用就全存在VAO里了 // 以下属性location可根据着色器里的layout(location = x)配置调整 glEnableVertexAttribArray(0); glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, m_vertexStride, (void*)offsetof(Vertex, pos)); glEnableVertexAttribArray(1); glVertexAttribPointer(1, 4, GL_UNSIGNED_BYTE, GL_TRUE, m_vertexStride, (void*)offsetof(Vertex, color)); // 如果使用带纹理的第二类着色器,多配置一个纹理坐标属性即可 // glEnableVertexAttribArray(2); // glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, m_vertexStride, (void*)offsetof(Vertex, uv)); // 5. 解绑VAO即可,不要解绑GL_ELEMENT_ARRAY_BUFFER,否则会清空VAO里存的EBO引用 glBindVertexArray(0); glBindBuffer(GL_ARRAY_BUFFER, 0);
绘制阶段逻辑
// 外层按着色器遍历 for (each shader in shaders) { glUseProgram(shader->id); // 先设置全局uniform:投影矩阵、视图矩阵、光照参数等 shader->setMat4("projection", projMat); shader->setMat4("view", viewMat); // 遍历所有使用该着色器的物体 for (each object in objectsByShader[shader]) { // 绑定物体自己的VAO,所有顶点状态自动恢复 glBindVertexArray(object->m_vao); // 设置物体专属uniform:模型矩阵、纹理等 shader->setMat4("model", object->modelMat); // 直接绘制,不需要手动绑VBO/EBO,不需要重复配置属性 glDrawElements(object->m_primitive, object->m_elementSize, GL_UNSIGNED_INT, 0); } } // 全部绘制完成后可选解绑VAO glBindVertexArray(0);
内容的提问来源于stack exchange,提问作者user3093226

