我不明白的 glDrawElements 的使用

Posted

技术标签:

【中文标题】我不明白的 glDrawElements 的使用【英文标题】:Use of glDrawElements I Don't Understand 【发布时间】:2014-07-23 12:48:47 【问题描述】:

我正在研究一个开源项目的源代码,他们使用了我不明白的函数 glDrawElements。作为一名程序员,我对 GL API 很陌生,如果有人能告诉我这是如何工作的,我将不胜感激。

让我们从绘图部分开始。代码如下所示:

for (int i = 0; i < numObjs; i++) 

    glDrawElements(GL_TRIANGLES, vboIndexSize(i), GL_UNSIGNED_INT, (void*)(UPTR)vboIndexOffset(i));

vboIndiexSize(i) 返回当前对象的索引数,vboIndexOffset 返回在平面内存数组中的偏移量,其中顶点数据和对象的索引被存储。

我不明白的部分是 (void*)(UPTR)vboIndexOffset(i))。我多次查看代码,函数 vboIndexOffset 返回一个 int32,UPTR 也将返回值转换为一个 int32。那么如何将 int32 转换为 void* 并期望它起作用?但是让我们假设我在那里犯了一个错误,它实际上返回了一个指向这个变量的指针。 glDrawElements 调用的第四个参数是内存块内的字节偏移量。以下是数据在 GPU 上的实际存储方式:

int ofs = m_vertices.getSize();
for (int i = 0; i < numObj; i++)

    obj[i].ofsInVBO = ofs;
    obj[i].sizeInVBO = obj[i].indices->getSize() * 3;
    ofs += obj[i].indices->getNumBytes();


vbo.resizeDiscard(ofs);
memcpy(vbo.getMutablePtr(), vertices.getPtr(), vertices.getSize());
for (int i = 0; i < numObj; i++)

    memcpy(
        m_vbo.getMutablePtr(obj[i].ofsInVBO),
        obj[i].indices->getPtr(),
        obj[i].indices->getNumBytes());

所以他们所做的就是计算存储顶点数据所需的字节数,然后将存储我们要绘制的所有对象的索引所需的字节数加到这个数字上。然后他们分配该大小的内存,并复制该内存中的数据:首先是顶点数据,然后是索引。他们使用以下方法将其推送到 GPU:

glGenBuffers(1, &glBuffer);
glBindBuffer(GL_ARRAY_BUFFER, glBuffer);
checkSize(size, sizeof(GLsizeiptr) * 8 - 1, "glBufferData");
glBufferData(GL_ARRAY_BUFFER, (GLsizeiptr)size, data, GL_STATIC_DRAW);

有趣的是,它们将所有内容都存储在 GL_ARRAY_BUFFER 中。他们从不将顶点数据存储在 GL_ARRAY_BUFFER 中,然后使用 GL_ELEMENT_ARRAY_BUFFER 存储索引。

但是回到绘制完成的代码,他们首先做通常的事情来声明顶点属性。对于每个属性:

glBindBuffer(GL_ARRAY_BUFFER, glBuffer);
glEnableVertexAttribArray(loc);
glVertexAttribPointer(loc, size, type, GL_FALSE, stride, pointer);

这是有道理的,只是标准的。然后是我已经提到的代码:

for (int i = 0; i < numObjs; i++) 

    glDrawElements(GL_TRIANGLES, vboIndexSize(i), GL_UNSIGNED_INT, (void*)(UPTR)vboIndexOffset(i));

所以问题是:即使 (UPTR) 实际上返回了指向变量的指针(代码没有指出这一点但我可能弄错了,这是一个大项目),我不知道是否可以存储所有顶点并使用 GL_ARRAY_BUFFER 然后使用 glDrawElements 并使用相同的内存块索引数据,并且第四个参数是此内存块中当前对象的此索引列表的第一个元素的偏移量。我认为您需要使用 GL_ARRAY_BUFFER 和 GL_ELEMENT_BUFFER 分别声明顶点数据和索引。我不认为您可以使用 GL_ARRAY_BUFFER 一次性声明所有数据,并且无论如何也无法让它在我身边工作。

以前有人见过这个吗?我还没有机会让它工作,并且想知道是否有人可能会告诉我是否需要注意一些具体的事情才能让它工作。我用一个带有位置、法线和纹理坐标数据的简单三角形进行了测试,因此我有 8 * 3 个浮点数作为顶点数据,我有一个由 3 个整数组成的数组作为索引 0、1、2。然后我将所有内容复制到内存块,用这个初始化glBufferData,然后尝试绘制三角形:

int n = 96; // offset in bytes into the memory block, fist int in the index list
glDrawElements(GL_TRIANGLES, 3, GL_UNSIGNED_INT, (void*)(&n));

它没有崩溃,但我看不到三角形。

编辑:

添加似乎对我不起作用的代码(崩溃)。

float vertices[] = 
    0,  1, 0, // Vertex 1 (X, Y)
    2, -1, 0, // Vertex 2 (X, Y)
   -1, -1, 0, // Vertex 3 (X, Y)
    3,  1, 0,
;

U8 *ptr = (U8*)malloc(4 * 3 * sizeof(float) + 6 * sizeof(unsigned int));
memcpy(ptr, vertices, 4 * 3 * sizeof(float));
unsigned int indices[6] =  0, 1, 2, 0, 3, 1 ;
memcpy(ptr + 4 * 3 * sizeof(float), indices, 6 * sizeof(unsigned int));

glGenBuffers(1, &vbo);

glBindBuffer(GL_ARRAY_BUFFER, vbo);

glBufferData(GL_ARRAY_BUFFER, 4 * 3 * sizeof(float) + 6 * sizeof(unsigned int), ptr, GL_STATIC_DRAW);

glGenVertexArrays(1, &vao);
glBindVertexArray(vao);

glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 0, NULL);
glEnableVertexAttribArray(0);

free(ptr);

那么当谈到绘画时:

glBindVertexArray(vao);
glBindBuffer(GL_ARRAY_BUFFER, vbo);

// see ***.com/questions/8283714/what-is-the-result-of-null-int/
typedef void (*TFPTR_DrawElements)(GLenum, GLsizei, GLenum, uintptr_t);
TFPTR_DrawElements myGlDrawElements = (TFPTR_DrawElements)glDrawElements;

myGlDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, uintptr_t(4 * 3 * sizeof(float)));

这会使应用程序崩溃。

请参阅下面的答案以获取解决方案

【问题讨论】:

这里已经详细解释过了:***.com/a/8284829/524368 @datenwold,谢谢,我使用了您的解决方案。不过,我的问题更多是关于将顶点数据和索引打包到 ARRAY_BUFFER 中以使其工作的想法。这导致我的程序现在崩溃。 【参考方案1】:

这是由于 OpenGL 重用了固定功能管道调用。当您绑定GL_ARRAY_BUFFER VBO 时,对glVertexAttribPointer 的后续调用需要一个偏移 到VBO(以字节为单位),然后将其转换为(void *)GL_ARRAY_BUFFER 绑定一直有效,直到另一个缓冲区被绑定,就像 GL_ELEMENT_ARRAY_BUFFER 绑定一直有效,直到另一个“索引”缓冲区被绑定。

您可以使用Vertex Array Object 封装缓冲区绑定和属性指针(偏移)状态。 您示例中的地址无效。使用:(void *) n

投射偏移量

【讨论】:

感谢您的回答。如何将 int 转换为 void 指针?我不明白?你能解释一下吗?另外,我知道我的问题不清楚,但我想知道的是使用 VBO 存储顶点数据和索引是否有效? 指针实际上只是一个表示内存地址的整数。除了大小差异(即不同机器上的指针是 32 位或 64 位)之外,它们之间的转换没有技术问题;这很危险,因为您不想意外取消引用不是真正有效的内存地址的东西。 是的,这是有道理的,只是很奇怪。如果这是一个偏移量,那么为什么不传递一个 int。如果它需要一个指向变量的指针,那为什么不这样做呢。无论如何,我根本无法让它工作,并且仍然想知道是否可以使用 GL_ARRAY_BUFFER 来存储顶点和索引数据。谢谢。 @Wyzard:不幸的是,事情没那么简单。从技术上讲,这种将一些整数转换为指针的方式可能会产生未定义的行为。干净的解决方案是将函数签名转换为接受 uintptr_t 作为数据参数的签名。见***.com/a/8284829/524368 glDrawElements 的签名是在缓冲区对象之前定义的;最初,您将传递一个 actual 指针,指向 client-side vertex array 中的数据。当引入设备端缓冲区时,此函数也被扩展以支持它们,方法是将缓冲区偏移量硬塞到地址参数中。【参考方案2】:

感谢您的回答。我认为尽管如此(并且在对网络进行了一些研究之后),

首先你应该使用glGenVertexArray。这似乎是现在 OpenGL4.x 的标准,因此与其在绘制几何之前调用 glVertexAttribPointer,不如在将数据推送到 GPU 缓冲区时创建 VAO 似乎是最佳实践。

我(实际上)能够在 SAME 缓冲区(GL_ARRAY_BUFFER)中组合顶点数据和索引,然后使用 glDrawElements 绘制图元(见下文)。无论如何,标准方法是将顶点数据推送到 GL_ARRAY_BUFFER 并将索引分别推送到 GL_ELEMENT_ARRAY_BUFFER 。因此,如果这是执行此操作的标准方式,那么最好不要尝试太聪明而只使用这些功能。

例子:

glGenBuffers(1, &vbo);
// push the data using GL_ARRAY_BUFFER
glGenBuffers(1, &vio);
// push the indices using GL_ELEMENT_ARRAY_BUFFER
...
glGenVertexArrays(1, &vao);
// do calls to glVertexAttribPointer
...

如果我错了,请纠正我,但这似乎是正确的(也是唯一的)方法。

编辑:

然而,只要在调用 glDrawElements 之前完成对 glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, vbo) 的调用,实际上就可以将顶点数据和索引“打包”到一个 ARRAY_BUFFER 中。

工作代码(与原始帖子中的代码相比):

float vertices[] = 
    0,  1, 0, // Vertex 1 (X, Y)
    2, -1, 0, // Vertex 2 (X, Y)
   -1, -1, 0, // Vertex 3 (X, Y)
    3,  1, 0,
;

U8 *ptr = (U8*)malloc(4 * 3 * sizeof(float) + 6 * sizeof(unsigned int));
memcpy(ptr, vertices, 4 * 3 * sizeof(float));
unsigned int indices[6] =  0, 1, 2, 0, 3, 1 ;
memcpy(ptr + 4 * 3 * sizeof(float), indices, 6 * sizeof(unsigned int));

glGenBuffers(1, &vbo);

glBindBuffer(GL_ARRAY_BUFFER, vbo);

glBufferData(GL_ARRAY_BUFFER, 4 * 3 * sizeof(float) + 6 * sizeof(unsigned int), ptr, GL_STATIC_DRAW);

glGenVertexArrays(1, &vao);
glBindVertexArray(vao);

glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 0, NULL);
glEnableVertexAttribArray(0);

free(ptr);

那么当谈到绘画时:

glBindVertexArray(vao);
glBindBuffer(GL_ARRAY_BUFFER, vbo); // << THIS IS ACTUALLY NOT NECESSARY

// VVVV THIS WILL MAKE IT WORK VVVV

glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, vbo);

// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

// see ***.com/questions/8283714/what-is-the-result-of-null-int/
typedef void (*TFPTR_DrawElements)(GLenum, GLsizei, GLenum, uintptr_t);
TFPTR_DrawElements myGlDrawElements = (TFPTR_DrawElements)glDrawElements;

myGlDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, uintptr_t(4 * 3 * sizeof(float)));

【讨论】:

使用 OpenGL 核心配置文件时需要使用 VAO。将顶点数据和索引放在同一个缓冲区中是可能的,并且工作正常,但我认为这并不常见。因此,为顶点数据和索引使用单独的缓冲区大多是标准的,这绝对没有错。 谢谢。不过我还没有去上班。是的,我同意如果有另一种方法(也许更标准),为什么还要麻烦,但我想让它工作是为了证明它确实有效。我认为将所有数据放在同一个内存位置会很有趣。如果你有一个让它工作的例子,我很乐意看到这个。它一直在为我崩溃。我已经添加了在原始问题中崩溃的代码。 实际发现问题并添加解决方案。感谢大家的贡献。 更好的方法是在设置 VAO 时调用 glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ...)glVertexAttribPointer() 和其他类似调用在此。元素数组缓冲区绑定是 VAO 状态的一部分。如果这样做,您只需要在绘图代码中使用glBindVertexArray()

以上是关于我不明白的 glDrawElements 的使用的主要内容,如果未能解决你的问题,请参考以下文章

使用 glDrawElements 很难理解索引

这里变量的行为是啥,即闭包。输出未定义,我不明白

我不明白如何让领域服务器在 AWS AMI 上运行

OpenGL:glDrawElements 不绘制

使用 glDrawElements 绘制 std::vector

使用 LWJGL 在 glDrawElements 中偏移