glVertexAttribPointer 和 glVertexAttribFormat:有什么区别?
glVertexAttribPointer and glVertexAttribFormat: What's the difference?
OpenGL 4.3 和 OpenGL ES 3.1 添加了几个用于指定顶点数组的替代函数:glVertexAttribFormat
、glBindVertexBuffers
等。但是我们已经有了用于指定顶点数组的函数。即glVertexAttribPointer
.
为什么要添加与旧 API 功能相同的新 API?
新 API 如何工作?
glVertexAttribPointer
有两个缺陷,一个是半主观的,另一个是objective.
第一个缺陷是它对 GL_ARRAY_BUFFER
的依赖。这意味着 glVertexAttribPointer
的行为取决于调用时绑定到 GL_ARRAY_BUFFER
的任何内容。但是一旦它被调用,绑定到 GL_ARRAY_BUFFER
的内容就不再重要了;缓冲区对象的引用被复制到 VAO 中。所有这些都是非常不直观和令人困惑的,即使对于一些半经验的用户也是如此。
它还要求您以 "pointer" 的形式提供缓冲区对象的偏移量,而不是整数字节偏移量。这意味着您执行从整数到指针的笨拙转换(必须在驱动程序中匹配同样笨拙的转换)。
第二个缺陷是它将两个在逻辑上完全独立的操作混为一谈。为了定义 OpenGL 可以读取的顶点数组,您必须提供两件事:
- 如何从内存中获取数据。
- 数据是什么样的。
glVertexAttribPointer
同时提供这两个。 GL_ARRAY_BUFFER
缓冲区对象加上偏移量 "pointer" 和步幅定义了数据的存储位置以及获取数据的方式。其他参数描述了单个数据单元的外观。让我们称其为数组的 vertex format。
实际上,与顶点格式相比,用户更有可能更改顶点数据的来源。毕竟,场景中的许多对象都以相同的方式存储它们的顶点。无论哪种方式可能是:3 个浮点数用于位置,4 个无符号字节用于颜色,2 个无符号短裤用于 tex-coords 等。一般来说,您只有几种顶点格式。
而您可以从更多位置提取数据。即使所有对象都来自同一个缓冲区,您也可能希望更新该缓冲区内的偏移量以从一个对象切换到另一个对象。
使用 glVertexAttribPointer
,您不能只更新偏移量。您必须一次指定整个格式+缓冲区信息。每一次。
VAO 减轻了对每个对象进行所有这些调用的麻烦,但事实证明它们并没有真正解决问题。哦当然,您不必实际调用 glVertexAttribPointer
。但这并没有改变改变顶点格式 昂贵.
的事实
,更改顶点格式非常昂贵。当您绑定一个新的 VAO 时(或者更确切地说,当您在绑定一个新的 VAO 之后进行渲染时),实现要么改变顶点格式,要么必须比较两个 VAO 以查看它们定义的顶点格式是否不同。无论哪种方式,它都在做不需要做的工作。
glVertexAttribFormat
和 glBindVertexBuffer
解决了这两个问题。 glBindVertexBuffer
直接指定缓冲区对象并将字节偏移量作为实际(64 位)整数。所以 GL_ARRAY_BUFFER
绑定的使用并不尴尬;该绑定仅用于操作缓冲区对象。
并且因为这两个独立的概念现在是独立的函数,您可以拥有一个存储格式的 VAO,绑定它,然后为您渲染的每个对象或一组对象绑定顶点缓冲区。更改顶点缓冲区绑定状态比顶点格式状态便宜。
请注意,这种分离在 GL 4.5 中正式化 direct state access APIs。即没有glVertexAttribPointer
的DSA版本;您必须使用glVertexArrayAttribFormat
和其他单独的格式API。
单独的属性绑定函数是这样工作的。 glVertexAttrib*Format
函数提供属性的所有顶点格式化参数。它的每个参数都与等效调用 glVertexAttrib*Pointer
.
的参数具有完全相同的含义
glBindVertexBuffer
.
让事情变得有点混乱
它的第一个参数是一个索引。但这 不是 属性位置;它只是一个缓冲区绑定点。这是一个来自属性位置的 单独数组 ,具有自己的最大限制。因此,您将缓冲区绑定到索引 0 这一事实意味着 与属性位置 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 在内存中的位置,而格式偏移量定义了每个属性的数据来自 顶点的位置。
最后要了解的是缓冲区绑定是定义 步幅 的地方。这可能看起来很奇怪,但从硬件的角度考虑一下。
缓冲区绑定应包含硬件将顶点索引或实例索引转换为内存位置所需的所有信息。完成后,顶点格式将解释如何解释该内存位置中的字节。
这也是实例除数通过 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);
}
请注意,此等效函数对属性位置和缓冲区绑定索引使用相同的索引值。如果您正在处理交错属性,则应尽可能避免这种情况;相反,对从同一缓冲区交错的所有属性使用单个缓冲区绑定。
OpenGL 4.3 和 OpenGL ES 3.1 添加了几个用于指定顶点数组的替代函数:glVertexAttribFormat
、glBindVertexBuffers
等。但是我们已经有了用于指定顶点数组的函数。即glVertexAttribPointer
.
为什么要添加与旧 API 功能相同的新 API?
新 API 如何工作?
glVertexAttribPointer
有两个缺陷,一个是半主观的,另一个是objective.
第一个缺陷是它对 GL_ARRAY_BUFFER
的依赖。这意味着 glVertexAttribPointer
的行为取决于调用时绑定到 GL_ARRAY_BUFFER
的任何内容。但是一旦它被调用,绑定到 GL_ARRAY_BUFFER
的内容就不再重要了;缓冲区对象的引用被复制到 VAO 中。所有这些都是非常不直观和令人困惑的,即使对于一些半经验的用户也是如此。
它还要求您以 "pointer" 的形式提供缓冲区对象的偏移量,而不是整数字节偏移量。这意味着您执行从整数到指针的笨拙转换(必须在驱动程序中匹配同样笨拙的转换)。
第二个缺陷是它将两个在逻辑上完全独立的操作混为一谈。为了定义 OpenGL 可以读取的顶点数组,您必须提供两件事:
- 如何从内存中获取数据。
- 数据是什么样的。
glVertexAttribPointer
同时提供这两个。 GL_ARRAY_BUFFER
缓冲区对象加上偏移量 "pointer" 和步幅定义了数据的存储位置以及获取数据的方式。其他参数描述了单个数据单元的外观。让我们称其为数组的 vertex format。
实际上,与顶点格式相比,用户更有可能更改顶点数据的来源。毕竟,场景中的许多对象都以相同的方式存储它们的顶点。无论哪种方式可能是:3 个浮点数用于位置,4 个无符号字节用于颜色,2 个无符号短裤用于 tex-coords 等。一般来说,您只有几种顶点格式。
而您可以从更多位置提取数据。即使所有对象都来自同一个缓冲区,您也可能希望更新该缓冲区内的偏移量以从一个对象切换到另一个对象。
使用 glVertexAttribPointer
,您不能只更新偏移量。您必须一次指定整个格式+缓冲区信息。每一次。
VAO 减轻了对每个对象进行所有这些调用的麻烦,但事实证明它们并没有真正解决问题。哦当然,您不必实际调用 glVertexAttribPointer
。但这并没有改变改变顶点格式 昂贵.
glVertexAttribFormat
和 glBindVertexBuffer
解决了这两个问题。 glBindVertexBuffer
直接指定缓冲区对象并将字节偏移量作为实际(64 位)整数。所以 GL_ARRAY_BUFFER
绑定的使用并不尴尬;该绑定仅用于操作缓冲区对象。
并且因为这两个独立的概念现在是独立的函数,您可以拥有一个存储格式的 VAO,绑定它,然后为您渲染的每个对象或一组对象绑定顶点缓冲区。更改顶点缓冲区绑定状态比顶点格式状态便宜。
请注意,这种分离在 GL 4.5 中正式化 direct state access APIs。即没有glVertexAttribPointer
的DSA版本;您必须使用glVertexArrayAttribFormat
和其他单独的格式API。
单独的属性绑定函数是这样工作的。 glVertexAttrib*Format
函数提供属性的所有顶点格式化参数。它的每个参数都与等效调用 glVertexAttrib*Pointer
.
glBindVertexBuffer
.
它的第一个参数是一个索引。但这 不是 属性位置;它只是一个缓冲区绑定点。这是一个来自属性位置的 单独数组 ,具有自己的最大限制。因此,您将缓冲区绑定到索引 0 这一事实意味着 与属性位置 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 在内存中的位置,而格式偏移量定义了每个属性的数据来自 顶点的位置。
最后要了解的是缓冲区绑定是定义 步幅 的地方。这可能看起来很奇怪,但从硬件的角度考虑一下。
缓冲区绑定应包含硬件将顶点索引或实例索引转换为内存位置所需的所有信息。完成后,顶点格式将解释如何解释该内存位置中的字节。
这也是实例除数通过 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);
}
请注意,此等效函数对属性位置和缓冲区绑定索引使用相同的索引值。如果您正在处理交错属性,则应尽可能避免这种情况;相反,对从同一缓冲区交错的所有属性使用单个缓冲区绑定。