VAO和元素数组缓冲区状态

24

最近我在写OpenGL 3.3的代码,使用了顶点数组对象(VAO),后来在英特尔显卡上测试时发现,令我失望的是,元素数组缓冲区绑定显然不是VAO状态的一部分,因为调用:

glBindVertexArray(my_vao);
glDrawElements(GL_TRIANGLE_STRIP, count, GL_UNSIGNED_INTEGER, 0);

没有效果,而:

glBindVertexArray(my_vao);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, my_index_buffer); // ?
glDrawElements(GL_TRIANGLE_STRIP, count, GL_UNSIGNED_INTEGER, 0);

渲染出几何形状。我认为这只是Intel实现的OpenGL中的一个小错误(因为在GL_ARB_vertex_array_object(甚至在GL_OES_vertex_array_object)中明确指出元素数组保存状态的一部分),但在移动NVIDIA Quadro 4200上也发生了。这不好玩。

这是驱动程序错误、规范错误还是代码中的错误?该代码在GeForce 260和480上运行得完美无缺。

有没有类似的经验?

另外奇怪的是,GL_EXT_direct_state_access没有函数将元素数组缓冲区绑定到VAO(但它确实有用于指定顶点属性数组(因此用于数组缓冲区)的函数)。GPU制造商是在扰乱规范并欺骗我们吗?

编辑:

我最初没有打算展示任何源代码,因为我认为这里没有必要。但如请求的那样,这是重现问题的最小测试用例:

static GLuint n_vertex_buffer_object, p_index_buffer_object_list[3];
static GLuint p_vao[2];

bool InitGLObjects()
{
    const float p_quad_verts_colors[] = {
        1, 0, 0, -1, 1, 0,
        1, 0, 0, 1, 1, 0,
        1, 0, 0, 1, -1, 0,
        1, 0, 0, -1, -1, 0, // red quad
        0, 0, 1, -1, 1, 0,
        0, 0, 1, 1, 1, 0,
        0, 0, 1, 1, -1, 0,
        0, 0, 1, -1, -1, 0, // blue quad
        0, 0, 0, -1, 1, 0,
        0, 0, 0, 1, 1, 0,
        0, 0, 0, 1, -1, 0,
        0, 0, 0, -1, -1, 0 // black quad
    };
    const unsigned int p_quad_indices[][6] = {
        {0, 1, 2, 0, 2, 3},
        {4, 5, 6, 4, 6, 7},
        {8, 9, 10, 8, 10, 11}
    };
    glGenBuffers(1, &n_vertex_buffer_object);
    glBindBuffer(GL_ARRAY_BUFFER, n_vertex_buffer_object);
    glBufferData(GL_ARRAY_BUFFER, sizeof(p_quad_verts_colors), p_quad_verts_colors, GL_STATIC_DRAW);
    glGenBuffers(3, p_index_buffer_object_list);
    for(int n = 0; n < 3; ++ n) {
        glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, p_index_buffer_object_list[n]);
        glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(p_quad_indices[n]), p_quad_indices[n], GL_STATIC_DRAW);
    }

    glGenVertexArrays(2, p_vao);
    glBindVertexArray(p_vao[0]);
    {
        glBindBuffer(GL_ARRAY_BUFFER, n_vertex_buffer_object);
        glEnableVertexAttribArray(0);
        glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), p_OffsetInVBO(0));
        glEnableVertexAttribArray(1);
        glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), p_OffsetInVBO(3 * sizeof(float)));
        glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, p_index_buffer_object_list[0]); // red
    }
    glBindVertexArray(0);

    glBindVertexArray(p_vao[1]);
    {
        glBindBuffer(GL_ARRAY_BUFFER, n_vertex_buffer_object);
        glEnableVertexAttribArray(0);
        glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), p_OffsetInVBO(0));
        glEnableVertexAttribArray(1);
        glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), p_OffsetInVBO(3 * sizeof(float)));
        glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, p_index_buffer_object_list[1]); // blue
    }
    glBindVertexArray(0);

#ifdef BIND_BLACK_QUAD_ELEMENT_ARRAY_BUFFER
    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, p_index_buffer_object_list[2]);
    // bind the buffer with the black quad (not inside VAO, should NOT be seen)
#endif // BIND_BLACK_QUAD_ELEMENT_ARRAY_BUFFER

    // [compile shaders here]

    return true; // success
}

以上代码创建了一个包含三个四边形的顶点缓冲区,分别是红色的、蓝色的和黑色的。然后创建了三个索引缓冲区,它们指向各个四边形。接着创建并设置了两个VAO,一个应该包含红色四边形的索引,另一个应该包含蓝色四边形的索引。黑色的四边形不应该被渲染(假设BIND_BLACK_QUAD_ELEMENT_ARRAY_BUFFER已定义)。

void onDraw()
{
    glClearColor(.5f, .5f, .5f, 0);
    glClear(GL_COLOR_BUFFER_BIT);
    glDisable(GL_DEPTH_TEST);

    glUseProgram(n_program_object);

    static int n_last_color = -1;
    int n_color = (clock() / 2000) % 2;
    if(n_last_color != n_color) {
        printf("now drawing %s quad\n", (n_color)? "blue" : "red");
        n_last_color = n_color;
    }

    glBindVertexArray(p_vao[n_color]);
#ifdef VAO_DOESNT_STORE_ELEMENT_ARRAY_BUFFER
    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, p_index_buffer_object_list[n_color]); // fixes the problem
#endif // VAO_DOESNT_STORE_ELEMENT_ARRAY_BUFFER
    glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
    glBindVertexArray(0);
}

这段代码将视口清空为灰色,并以重复方式呈现蓝色或红色的四边形(同时打印该颜色)。虽然在台式机GPU上可以正常工作,但在笔记本GPU上却不能正常工作(除非定义了VAO_DOESNT_STORE_ELEMENT_ARRAY_BUFFER宏,否则会渲染黑色四边形)。取消定义BIND_BLACK_QUAD_ELEMENT_ARRAY_BUFFER宏可以使四边形变为蓝色,因为最后绑定了蓝色索引缓冲区。但是无论如何都不会呈现红色四边形。

所以我认为,要么是我对VAO工作原理的理解存在严重误解,要么是我的代码有bug,要么是一个驱动程序的bug。

完整源码
二进制文件(Windows,32位)


7
很可能是你的代码最初没有将元素缓冲区放入VAO中。为什么不让我们看看你的VAO初始化代码呢? - Nicol Bolas
7
别这么愚蠢嘛。而且我说过它在GeForce 260/480上是可行的。在撰写评论之前请先阅读帖子。我完全能够调试我的OpenGL代码。这个问题是关于不同OpenGL实现和兼容性的区别。 - the swine
7
代码能够运行并不意味着它是“正确”的。无论是由于偶然的情况还是其他原因,代码都可能成功运行。它在NVIDIA驱动程序上既失败又成功的事实表明了用户的错误。如果它在NVIDIA上运行良好但在ATI上失败,或者反之亦然,那么更有可能是驱动程序的错误。但是NVIDIA特别相似。因此,如果它在某些NVIDIA硬件上有时可以工作,有时却不能,那就像是用户的错误。 - Nicol Bolas
5个回答

26

过了一段时间,我发现这其实是我的问题。搭载移动NVIDIA Quadro 4200图形卡的笔记本电脑默认设置为所有应用程序都在性能模式下运行Intel图形卡。我不理解为什么有人要这样做,因为这样就没有任何应用程序可以使用更强大的OpenGL GPU(虽然仍然可以通过OpenCL进行显式设备选择,也许还可以通过DirectX - 这可以解释为什么一些游戏可以顺畅运行)。

尽管如此,上述错误行为只是Intel驱动程序中的一个bug而已。 Intel驱动程序不保存ELEMENT_ARRAY_BUFFER_BINDING。 就是这样。

非常抱歉提出这个问题,因为如果不知道以上内容,就没有好的答案可以回答。


你向英特尔报告了这个错误吗?这个问题有任何后续进展吗?作为一个初学者的图形程序员,知道世界上存在这样微妙的问题让我深感不安。 - Philip Guin
@Philip 我并没有考虑过这一点,我几乎认为Intel GPU不可用,因此对我来说很容易说我不会支持Intel GPU。但也许这是个好主意。如果您这么做了,请在评论中发布错误跟踪器的链接。谢谢。 - the swine
8
即使对您没有帮助,将驱动程序错误记录在SO上也对我有帮助。 - Justin

19

我实际上相信 ARB VAO 缺少元素数组缓冲区绑定(或任何其他缓冲区绑定)状态。

不需要相信,规范说明了事实。

ARB_vertex_array_object 规范中得知:

该命令

void GenVertexArrays(sizei n, uint *arrays);
返回结果:

返回先前未使用的顶点数组对象名称。这些名称被标记为已使用,仅用于GenVertexArrays,并使用表6.6(除CLIENT_ACTIVE_TEXTURE选择器状态外),6.7和6.8中列出的状态进行初始化(ARRAY_BUFFER_BINDING状态除外)。

因此,VAO所包含的整个状态是这三个表的内容,有特定例外。

该扩展针对OpenGL图形规范第2.1版(PDF)编写。因此,任何页面编号、章节标签或表编号都是相对于该规范引用的。

我不会在这里复制那三个表。但是,如果您查看第273页(按照规范的页数)/第287页(按照实际页数),您将找到表6.8。在该表上,可以找到以下内容:

  • ELEMENT_ARRAY_BUFFER_BINDING

这里没有任何歧义。信息可能没有明确说明。但毫无疑问存在ELEMENT_ARRAY_BUFFER_BINDING是VAO状态的一部分。

因此,您的问题可能来自以下两个来源之一:

  1. 驱动程序错误。正如我在评论中所述,驱动程序错误似乎不太可能。不是不可能,只是不太可能。 NVIDIA的驱动程序在不同硬件上非常相似,而VAO几乎没有在硬件上镜像。除非您使用不同版本的驱动程序,否则很少有理由认为错误是由于驱动程序错误引起的。

  2. 用户错误。我知道您声称您的代码有效,因此没有问题。每个人都会对某些代码进行这种声明。我也有过无数次发誓某些代码工作得非常好。然而,它是损坏的; 它刚好通过了。事情就是这样。如果您发布代码,那么至少我们将能够排除此可能性。否则,我们只有您的话。考虑到人类多么容易犯错,这并不值得一提。


好的,我不能发布完整的代码,但我会尝试提供最小的代码来重现错误。此外,ARB的东西很可靠,状态似乎确实存在。 - the swine
这里是源代码(在原问题中编辑)。那么这是驱动程序的错误还是我的问题? - the swine

2
“GL_EXT_direct_state_access”并没有绑定一个元素数组缓存到VAO的函数,这也很奇怪。如果有人在处理至少OpenGL 4.5版本的问题, 可以直接使用 glVertexArrayElementBuffer函数将元素数组缓存直接绑定到VAO上,而不必再次绑定VAO,这可能会解决你挖出此问题的任何问题。”

0
可能的原因是您的Intel适配器无法提供OpenGL 3.3上下文,而是默认为2.1或类似版本。正如其他人指出的那样,在早期版本的OpenGL中,元素数组缓冲区状态不是VAO的一部分。

1
那是不可能的,因为我正在创建上下文作为向前兼容的,所以它不能默认为较低版本。或者如果它这样做了,那肯定是不应该的。 - the swine
我认为有一个glGet调用可以查询上下文版本。以找出答案。 - Steven Lu
@StevenLu 是的,有glGetString(GL_VERSION),但这只意味着一个错误(元素数组未被缓存)是由一个更严重的错误引起的(创建旧的2.1上下文而不是向前兼容的上下文)。 - the swine

-1

我可以想象 ELEMENT 缓存未被缓存;如果您这样做:

glBindBuffer(GL_ARRAY_BUFFER, n_vertex_buffer_object);
glEnableVertexAttribArray(0);
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), p_OffsetInVBO(0));

这就像是在说:

gBoundBuffer_GL_ARRAY_BUFFER=n_vertex_buffer_object;
currentVAO->enable|=(1<<0);
currentVAO->vertexBuffer=IndexToPointer(gBoundBuffer_GL_ARRAY_BUFFER);

换句话说,glBindBuffer() 并没有做任何事情,只是设置了 GL_ARRAY_BUFFER 的值。在 glVertexAttribPointer() 中修改 VAO。因此,当你执行以下操作时:
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, p_index_buffer_object_list[0]);
glBindVertexArray(0);

你真的做到了:

gBoundBuffer_GL_ELEMENT_ARRAY_BUFFER=p_index_buffer_object_list[0];
currentVAO=0;

在某些情况下,GL_ELEMENT_ARRAY_BUFFER绑定没有任何作用。 但我不确定是否有类似于glVertexAttribPointer()的元素变量(如glElementPointer()),它实际上会对VAO产生影响。


好的,你所描述的行为(元素数组未缓存)实际上违反了规范,这样做没有多大意义。当你想要绘制一些几何图形时,只有极少数情况下顶点和索引是可分离的。大多数情况下,模型是从艺术家创建的文件中加载的,一个模型的索引不适合任何其他模型的顶点。因此,在“图形应用程序,显示3D网格”的场景中,同时绑定索引缓冲区和顶点属性指针实际上是完全有道理的。 - the swine

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