OpenGL 4.3和OpenGL ES 3.1添加了一些用于指定顶点数组的替代函数:glVertexAttribFormat
,glBindVertexBuffers
等。但是我们已经具有用于指定顶点数组的函数。即glVertexAttribPointer
。
为什么要添加与旧API具有相同功能的新API?
新的API如何工作?
最佳答案
glVertexAttribPointer
有两个缺陷,一个是半主观的,另一个是目标。
第一个缺陷是它对GL_ARRAY_BUFFER
的依赖。这意味着glVertexAttribPointer
的行为取决于GL_ARRAY_BUFFER
在被调用时绑定到的内容。但是一旦被调用,绑定到GL_ARRAY_BUFFER
的内容就不再重要了;缓冲区对象的引用将复制到VAO中。即使对于某些半经验的用户,所有这些都是非常直观和令人困惑的。
它还要求您在缓冲区对象中提供一个偏移量作为“指针”,而不是整数字节偏移量。这意味着您要执行从整数到指针的尴尬转换(必须与驱动程序中同样尴尬的转换匹配)。
第二个缺陷是,它将两个逻辑上完全分开的操作合并在一起。为了定义OpenGL可以读取的顶点数组,您必须提供两件事:
如何从内存中获取数据。
数据看起来像什么。glVertexAttribPointer
同时提供这两个。 GL_ARRAY_BUFFER
缓冲区对象,加上偏移量“指针”和跨距定义了数据的存储位置以及如何获取数据。其他参数描述单个数据单元的外观。让我们将其称为数组的vertex format。
实际上,与顶点格式相比,用户更有可能更改顶点数据的来源。毕竟,场景中的许多对象都以相同的方式存储其顶点。无论采用哪种方式:3个浮点表示位置,4个无符号字节表示颜色,2个无符号短裤表示tex-coords等,通常来说,您只有几种顶点格式。
而您拥有更多从中提取数据的位置。即使对象全部来自同一缓冲区,您也可能需要更新该缓冲区内的偏移量以在对象之间切换。
使用glVertexAttribPointer
,您不能仅更新偏移量。您必须一次指定整个格式+缓冲区信息。每次。
VAO减轻了必须针对每个对象进行所有这些调用的麻烦,但事实证明,它们并不能真正解决问题。哦,可以,您不必实际调用glVertexAttribPointer
。但这并不能改变更改顶点格式的成本高昂的事实。
As discussed here,更改顶点格式非常昂贵。当您绑定一个新的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);
}
请注意,此等效函数对属性位置和缓冲区绑定索引使用相同的索引值。如果您要使用交错属性,则应尽可能避免这种情况。相反,对从同一缓冲区交错的所有属性使用单个缓冲区绑定。