OpenGL 4.3和OpenGL ES 3.1添加了几个用于指定顶点数组的替代函数:glVertexAttribFormat
、glBindVertexBuffers
等。但我们已经有了用于指定顶点阵列的函数。即CCD_ 3。
-
为什么要添加与旧API功能相同的新API?
-
新的API是如何工作的?
glVertexAttribPointer
有两个缺陷,一个是半主观的,另一个是客观的。
第一个缺陷是它对GL_ARRAY_BUFFER
的依赖性。这意味着glVertexAttribPointer
的行为取决于调用时与GL_ARRAY_BUFFER
绑定的内容。但一旦它被调用,与GL_ARRAY_BUFFER
绑定的内容就不再重要;缓冲区对象的引用被复制到VAO中。所有这些都是非常不直观和令人困惑的,甚至对一些没有经验的用户来说也是如此。
它还要求您将偏移量作为"指针"而不是整数字节偏移量提供给缓冲区对象。这意味着要执行从整数到指针的笨拙转换(必须在驱动程序中进行同样笨拙的转换)。
第二个缺陷是,它将逻辑上完全分离的两个操作混为一谈。为了定义OpenGL可以读取的顶点数组,您必须提供两件事:
- 如何从内存中提取数据
- 这些数据是什么样子的
glVertexAttribPointer
同时提供这两种功能。GL_ARRAY_BUFFER
缓冲区对象,加上偏移量"指针"和步长,定义了数据存储的位置以及如何获取数据。其他参数描述了单个数据单元的外观。让我们称之为数组的顶点格式。
实际上,与顶点格式相比,用户更有可能更改顶点数据的来源。毕竟,场景中的许多对象都以相同的方式存储其顶点。不管是哪种方式:3个浮点表示位置,4个无符号字节表示颜色,2个无符号short表示tex坐标,等等。一般来说,你只有几种顶点格式。
而从中提取数据的位置要多得多。即使对象都来自同一个缓冲区,您也可能希望更新该缓冲区内的偏移量,以便从一个对象切换到另一个对象。
使用glVertexAttribPointer
,不能仅更新偏移量。您必须同时指定整个格式+缓冲区信息。每次。
VAO减轻了对每个对象进行所有这些调用的负担,但事实证明,它们并不能真正解决问题。哦,当然,你实际上不必打电话给glVertexAttribPointer
。但这并不能改变这样一个事实,即更改顶点格式是非常昂贵的。
正如这里所讨论的,更改顶点格式是非常昂贵的。绑定新的VAO时(或者更确切地说,在绑定新VAO后进行渲染时),实现要么会更改顶点格式(不管怎样),要么必须比较两个VAO以查看它们定义的顶点格式是否不同。无论哪种方式,它都在做不需要做的工作。
CCD_ 13和CCD_。glBindVertexBuffer
直接指定缓冲区对象,并将字节偏移量作为实际(64位)整数。所以GL_ARRAY_BUFFER
绑定的使用并不尴尬;该绑定仅用于操作缓冲区对象。
因为这两个独立的概念现在是独立的函数,所以可以有一个VAO来存储格式,绑定它,然后为渲染的每个对象或对象组绑定顶点缓冲区。更改顶点缓冲区绑定状态比顶点格式状态便宜。
请注意,这种分离在GL 4.5的直接状态访问API中是形式化的。也就是说,glVertexAttribPointer
没有DSA版本;必须使用glVertexArrayAttribFormat
和其他单独的格式API。
单独的属性绑定函数是这样工作的。glVertexAttrib*Format
函数提供了属性的所有顶点格式化参数。它的每个参数与对glVertexAttrib*Pointer
的等效调用中的参数具有完全相同的含义。
事情变得有点混乱的地方是glBindVertexBuffer
。
它的第一个参数是索引。但这不是属性位置;它仅仅是一个缓冲绑定点。这是一个独立于属性位置的数组,具有自己的最大限制。因此,将缓冲区绑定到索引0这一事实意味着对于属性位置0从何处获取数据没有任何。
缓冲区绑定和属性位置之间的连接由glVertexAttribBinding
定义。第一个参数是属性位置,第二个参数是用于获取该属性位置的缓冲区绑定索引。由于函数的名称以"VertexAttrib"开头,您应该将其视为顶点格式状态的一部分,因此更改成本很高。
偏移的性质一开始也可能有点令人困惑。CCD_ 23具有偏移参数。但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);
}
请注意,此等效函数对属性位置和缓冲区绑定索引使用相同的索引值。如果您正在执行交错属性,则应尽可能避免这种情况;相反,对从同一缓冲区交错的所有属性使用单个缓冲区绑定。