glVertexAttribPointer和glVertexAttribFormat:有什么区别?

时间:2016-06-22 15:21:14

标签: opengl opengl-es opengl-4

OpenGL 4.3和OpenGL ES 3.1增加了几个用于指定顶点数组的替代函数:glVertexAttribFormatglBindVertexBuffers等。但是我们已经有了指定顶点数组的函数。即glVertexAttribPointer

  1. 为什么要添加与旧版API相同的新API?

  2. 新API如何运作?

1 个答案:

答案 0 :(得分:30)

glVertexAttribPointer有两个缺陷,其中一个是半主观的,另一个是目标。

第一个缺陷是它对GL_ARRAY_BUFFER的依赖。这意味着glVertexAttribPointer的行为取决于在调用它时绑定到GL_ARRAY_BUFFER的任何内容。但是一旦被召唤,GL_ARRAY_BUFFER的约束就不再重要;缓冲区对象的引用被复制到VAO中。所有这些都非常不直观和令人困惑,即使对一些半经验的用户来说也是如此。

它还要求您在缓冲区对象中提供一个偏移量作为"指针",而不是整数字节偏移量。这意味着您执行从整数到指针的笨拙演员(必须与驱动程序中同样笨拙的演员匹配)。

第二个缺陷是它将逻辑上两个操作混为一谈。为了定义OpenGL可以读取的顶点数组,您必须提供两件事:

  • 如何从内存中获取数据。
  • 这些数据是什么样的。

glVertexAttribPointer同时提供这两者。 GL_ARRAY_BUFFER缓冲区对象,加上偏移量"指针"并且步幅定义数据的存储位置以及如何获取数据。其他参数描述了单个数据单元的外观。我们称之为数组的vertex format

实际上,用户比顶点格式更有可能改变顶点数据的来源。毕竟,场景中的许多对象以相同的方式存储它们的顶点。无论那种方式如何:位置为3个浮点数,颜色为4个无符号字节,tex-coords为2个无符号短点等。一般来说,您只有几个顶点格式。

您有更多从中提取数据的位置。即使对象都来自同一缓冲区,您也可能希望更新中的偏移量,以便从对象切换到对象。

使用glVertexAttribPointer,您无法仅更新偏移量。您必须一次指定整个格式+缓冲区信息。每一次。

VAO减少了必须为每个对象进行所有这些调用,但事实证明他们并没有真正解决问题。哦,当然,你不必实际打电话给glVertexAttribPointer。但这并没有改变这样一个事实:更改顶点格式昂贵

As discussed here,更改顶点格式非常昂贵。当您绑定新的VAO时(或者更确切地说,当您在绑定新的VAO之后进行渲染时),实现要么更改顶点格式,要么必须比较两个VAO以查看它们定义的顶点格式是否不同。无论哪种方式,它都在做它不需要做的工作。

glVertexAttribFormatglBindVertexBuffer解决了这两个问题。 glBindVertexBuffer直接指定缓冲区对象,并将字节偏移量作为实际(64位)整数。因此GL_ARRAY_BUFFER绑定没有尴尬的使用;该绑定仅用于操作缓冲区对象。

因为两个独立的概念现在是单独的函数,你可以拥有一个存储格式的VAO,绑定它,然后为你渲染的每个对象或一组对象绑定顶点缓冲区。更改顶点缓冲区绑定状态比顶点格式状态便宜。

单独的属性绑定功能就像这样工作。 glVertexAttrib*Format函数提供属性的所有顶点格式参数。它的每个参数与等效调用glVertexAttrib*Pointer的参数具有完全相同的含义。

事情变得有些混乱的地方是glBindVertexBuffer

它的第一个参数是一个索引。但这是属性位置;它只是一个缓冲区绑定点。这是来自属性位置的单独数组,具有自己的最大限制。因此,将缓冲区绑定到索引0的事实意味着 nothing 关于属性位置0从何处获取其数据。

缓冲区绑定和属性位置之间的连接由glVertexAttribBinding定义。第一个参数是属性位置,第二个参数是用于获取该属性的位置的缓冲区绑定索引。由于函数的名称以" VertexAttrib"开头,因此您应该将其视为顶点格式状态的一部分,因此更改成本很高。

首先,偏移的性质也可能有点令人困惑。 glVertexAttribFormat有一个偏移参数。但glBindVertexBuffer也是如此。但是这些抵消意味着不同的东西。理解差异的最简单方法是使用交错数据结构的示例:

struct Vertex
{
    GLfloat pos[3];
    GLubyte color[4];
    GLushort texCoord[2];
};

顶点缓冲区绑定偏移量指定从缓冲区对象的起点到第一个顶点索引的字节偏移量。也就是说,当您渲染索引0时,GPU将从缓冲区对象的地址+绑定偏移量中获取内存。

顶点格式偏移量指定从每个顶点的起点到该特定属性的数据的偏移量。如果缓冲区中的数据由Vertex定义,则每个属性的偏移量为:

glVertexAttribFormat(0, ..., offsetof(Vertex, pos)); //AKA: 0
glVertexAttribFormat(1, ..., offsetof(Vertex, color)); //Probably 12
glVertexAttribFormat(2, ..., offsetof(Vertex, texCoord)); //Probably 16

因此,绑定偏移定义了顶点0在内存中的位置,而格式偏移定义了每个属性的数据来​​自顶点内的位置。

最后要理解的是缓冲区绑定是定义 stride 的地方。这可能看起来很奇怪,但从硬件角度考虑它。

缓冲区绑定应包含硬件将顶点索引或实例索引转换为内存位置所需的所有信息。一旦完成,顶点格式将解释如何解释该内存位置中的字节。

这也是实例除数通过glVertexBindingDivisor成为缓冲区绑定状态的一部分的原因。硬件需要知道除数才能将实例索引转换为内存地址。

当然,这也意味着您不再依赖OpenGL为您计算步幅。在上面的演员表中,您只需使用sizeof(Vertex)

单独的属性格式完全覆盖了旧的glVertexAttribPointer模型,以至于旧函数现在完全根据新函数定义:

void glVertexAttrib*Pointer(GLuint index​, GLint size​, GLenum type​, {GLboolean normalized​,} GLsizei stride​, const GLvoid * pointer​)
{
  glVertexAttrib*Format(index, size, type, {normalized,} 0);
  glVertexAttribBinding(index, index);

  GLuint buffer;
  glGetIntegerv(GL_ARRAY_BUFFER_BINDING, buffer);
  if(buffer == 0)
    glErrorOut(GL_INVALID_OPERATION); //Give an error.

  if(stride == 0)
    stride = CalcStride(size, type);

  GLintptr offset = reinterpret_cast<GLintptr>(pointer);
  glBindVertexBuffer(index, buffer, offset, stride);
}

请注意,此等效函数对属性位置和缓冲区绑定索引使用相同的索引值。如果您正在进行交错属性,则应尽可能避免这种情况;相反,对从同一缓冲区交错的所有属性使用单个缓冲区绑定。