【OpenGL经验谈01】Vertex 规范最佳实践

一、概述

在使用GLSL中,越是深入使用,越觉得难以把控,而且常常是黑屏无Debug状态。究其原因,buffer需要优化,程序需要优化,本文从buffer的角度提出问题和解决方案。

二、缓冲区对象的大小

在为缓冲区对象分配存储时,可以使用任何大小。但是,有一些规则需要牢记。

创建大量微小缓冲区(大小约为千字节)可能会导致驱动程序问题。某些驱动程序只能从图形内存进行如此多的分配,而不管这些分配的大小如何。请注意,“很多”可能意味着数千。因此,将较小的对象放在一个大缓冲区中是个好主意。

缓冲区大小存在两个相互竞争的问题。

较大的缓冲区意味着将多个对象放在一个缓冲区中。这允许您渲染更多对象,而无需更改缓冲区对象状态。从而提高性能。
较大的缓冲区意味着将多个对象放在一个缓冲区中。但是,映射缓冲区意味着不能使用整个缓冲区(除非永久映射它)。因此,如果一个对象中有多个对象,并且需要映射一个对象的数据,则在修改该对象时,其他对象将无法使用。

三、格式化VBO数据

VBO 在使用它们的方式上非常灵活。例如,有多种方法可以在 VBO 中表示顶点属性数据:

(VVVV) (NNNN) (CCCC)
使用它们的一种选择是,对于每个批处理(绘制调用),为每个顶点属性分配一个单独的 VBO。这当然是可能的。如果将顶点、法线和颜色作为顶点属性,则从图形上看,这是:(VVVVV) (NNNN) (CCCC)
(VVVVNNNNCCCC)
另一种方法是将顶点属性块批量存储在同一个块中,一个接一个地存储在同一个块中,并将它们全部填充到同一个 VBO 中。通过 glVertexAttribPointer 调用指定顶点属性时,需要将字节偏移量传递到 VBO 中,以传递给 ptr 参数。从图示上看,这是:(VVVVNNNNCCCC)。
(VNCVNCVNCVNC)
另一种方法是在批处理中交错每个顶点的顶点属性,然后按顺序存储每个交错的顶点块,再次将所有顶点属性组合到单个缓冲区中。和以前一样,您需要将字节偏移量传递到 VBO 中,以 glVertexAttribPointer ptr 参数,但您还需要使用 stride 参数来确保每个顶点属性数组仅访问该属性数组的触及元素。从图形上看,此选项为:(VNCVNCVNCVNC)
现在这只是一个批次。也没有什么可以阻止您将多个批次的顶点属性数据存储在单个 VBO 或一组 VBO 中。

最佳布局取决于特定的 GPU 和驱动程序(以及 OpenGL 实现),但有些东西通常是好主意。

3.1 最小化顶点状态变化

渲染多个不同的网格时,请尝试组织数据,以便尽可能多的网格驻留在具有相同顶点格式的同一缓冲区对象中。简而言之,您希望尽量减少 glVertexAttribPointer(或 glVertexAttribFormat,如果可用)调用的数量。

glDrawArrays 和其他数组样式的呈现可以很容易地用于选择此缓冲区的子区域进行呈现。

索引渲染有点棘手。您必须根据缓冲区中在每个网格之前出现的其他顶点的数量来偏置每个网格的索引数据。您可以通过在上传索引数据之前递增索引数据来手动执行此操作,也可以使用 BaseVertex 呈现调用,例如 glDrawElementsBaseVertex。基本顶点是应用于每个索引的偏移量。这个绘制函数的好处是,顶点少于 65536 个的网格可以按顺序存储在同一个顶点缓冲区中,因为索引(作为 GLushort 存储,没有任何变化)可用于索引位置大于 65536 的顶点。

3.2 属性大小

可以使属性数据越小越好(尽管有一定的对齐限制)。利用使用有符号/无符号规范化短短字符和字节以及其他专用格式的功能。以下是针对特定类型数据的一些建议:

2D 纹理坐标
在大多数情况下,它们可以以标准化GL_SHORT或GL_UNSIGNED_SHORT存储,而不会降低质量。
法线
法线的精度通常并不那么重要。由于归一化向量始终在 [-1, 1] 范围内,因此最好使用某种归一化整数格式。法线的三个分量可以通过 GL_INT_2_10_10_10_REV 类型存储在单个 32 位整数中。您可以忽略最后一个 2 位组件,也可以找到一些有用的东西来插入其中。
颜色
除非它们需要是 HDR 颜色,否则它们可以以归一化GL_UNSIGNED_BYTE存储,因此单个颜色可以打包成 4 个字节。如果您需要更高的颜色精度,可以使用GL_UNSIGNED_INT_2_10_10_10_REV,其中 2 位用于 alpha。如果您绝对需要 HDR 颜色,您可以使用GL_R11F_G11F_B10F,前提是浮动精度有效。如果没有,您可以雇用GL_HALF_FLOAT而不是GL_FLOAT费用。
位置
这些很难比GL_FLOAT更有效地打包,但这取决于您的数据以及您愿意做多少工作。您可以使用GL_HALF_FLOAT,但请记住相对于 32 位浮点数的范围和精度限制。
一个经过时间考验的替代方案是使用标准化的 GLshorts。为此,您需要重新排列模型空间数据,以便将所有位置都打包在原点周围的 [-1, 1] 框中。您可以通过在所有位置中查找 XYZ 中的最小值/最大值来做到这一点。然后,从所有顶点位置减去最小值/最大值框的中心点;然后将所有位置缩放为最小/最大框的宽度/高度/深度的一半。您需要保持中心点和比例因子。
在构建模型到视图矩阵(或模型到任何矩阵)时,需要在变换堆栈的顶部应用中心点偏移和缩放(因此,在最后,就在绘制之前)。请注意,此偏移和缩放不应应用于法线,因为它们具有单独的模型空间。
有一些事情你应该注意。任何属性数据的对齐方式应不少于 4 个字节。因此,如果您有 GLushorts 的 vec3,则不能将第 4 个组件用于新属性(例如 GLbytes 的 vec2)。如果你想把一些东西打包进去,而不是有无用的填充,你需要把它变成 GLushorts 的 vec4。

3.3 交织

交错属性在多大程度上有助于渲染性能尚不清楚。需要分析数据。由于对齐需要,交错的顶点数据可能比未交错的顶点数据占用更多的空间。

3.4 流属性

流式处理属性(每帧或非常频繁地更改的属性)需要使用缓冲区对象流式处理技术。这些通常不能很好地与静态属性配合使用,并且许多流式处理技术需要丢弃整个缓冲区对象。因此,将流式处理属性与未流式处理属性放在同一缓冲区中实际上毫无意义。

四、顶点、法线、特坐标

是否应该为每个 VBO 创建单独的 VBO?你会失去性能吗?
如果对象是静态的,则将它们全部合并到尽可能少的 VBO 中,以获得最佳性能。有关布局注意事项的更多详细信息,请参阅上一节。

如果只有一些顶点属性是动态的,即经常变化,则将它们放在单独的 VBO 中可以更轻松、更快速地进行更新。

例如,如果在 CPU 上模拟水,则每个顶点的位置可能会一直变化,但其颜色保持不变。

示例:每批多个顶点属性 VBO

// Binding the vertex
glBindBuffer(GL_ARRAY_BUFFER, vertexVBOID);
glVertexPointer(3, GL_FLOAT, sizeof(float)*3, NULL); // Vertex start position address

// Bind normal and texcoord
glBindBuffer(GL_ARRAY_BUFFER, otherVBOID);
glNormalPointer(GL_FLOAT, sizeof(float)*6, NULL); // Normal start position address
glTexCoordPointer(2, GL_FLOAT, sizeof(float)*6, sizeof(float*3)); // Texcoord start position address

五、动态VBO

主条目:缓冲区对象流式处理
如果 VBO 的内容是动态的,您应该调用 glBufferData 还是 glBufferSubData(或 glMapBuffer)?
如果要更新一小部分,请使用 glBufferSubData。如果要更新整个 VBO,请使用 glBufferData(据报道,此信息来自 nVidia 文档)。但是,在更新整个缓冲区时,另一种被认为效果良好的方法是使用 NULL 指针调用 glBufferData,然后使用新内容调用 glBufferSubData。指向 glBufferData 的 NULL 指针让驱动程序知道你不关心前面的内容,因此可以自由替换完全不同的缓冲区,这有助于驱动程序管道更有效地上传。

您可以做的另一件事是双缓冲 VBO。这意味着您制作了 2 个 VBO。在第 N 帧上,更新 VBO 2 并使用 VBO 1 进行渲染。在第 N+1 帧上,更新 VBO 1 并从 VBO 2 进行渲染。这也大大提升了 nVidia 和 ATI/AMD 的性能。

六、顶点布局规范

很多新代码都是这样编写的

glBindBuffer(GL_ARRAY_BUFFER, vboID);
glEnableClientState(GL_VERTEX_ARRAY);
glVertexPointer(3, GL_FLOAT, sizeof(TVertex_VNTWI), info->posOffset);
glTexCoordPointer(2, GL_FLOAT, sizeof(TVertex_VNTWI), info->texOffset);
glEnableClientState(GL_TEXTURE_COORD_ARRAY);
glNormalPointer(GL_FLOAT, sizeof(TVertex_VNTWI), info->nmlOffset);
glEnableClientState(GL_NORMAL_ARRAY);
// --------------------
int weightPosition = glGetAttribLocation(programID, "blendWeights");
glVertexAttribPointer(weightPosition, 4, GL_FLOAT, GL_FALSE, sizeof(TVertex_VNTWI), info->weightOffset);
glEnableVertexAttribArray(weightPosition);
// --------------------
int indexPosition = glGetAttribLocation(programID, "blendIndices");
glVertexAttribPointer(indexPosition, 4, GL_UNSIGNED_BYTE, GL_FALSE, sizeof(TVertex_VNTWI), info->indexOffset);
glEnableVertexAttribArray(indexPosition);
// --------------------
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, iboID);
glDrawElements(GL_TRIANGLES, numIndices, GL_UNSIGNED_SHORT, 0);

在着色器中,将使用 gl_Vertex、gl_Normal 和 gl_MultiTexCoord0。最好对顶点、法线和特质坐标也使用通用顶点属性,因为这是指定顶点布局的现代方式。您已经将它用于 blendWeights 和 blendIndices。

核心上下文中,您必须使用自己的顶点属性来调用 glVertexAttribPointer。

相关推荐

  1. OpenGL经验01Vertex 规范最佳实践

    2024-03-14 19:32:02       42 阅读
  2. Git 最佳实践规范

    2024-03-14 19:32:02       35 阅读
  3. OpenGL实践06】如何读入模型文件obj数据

    2024-03-14 19:32:02       32 阅读
  4. OpenGL 教程06 】 关于着色器(01

    2024-03-14 19:32:02       43 阅读

最近更新

  1. docker php8.1+nginx base 镜像 dockerfile 配置

    2024-03-14 19:32:02       98 阅读
  2. Could not load dynamic library ‘cudart64_100.dll‘

    2024-03-14 19:32:02       106 阅读
  3. 在Django里面运行非项目文件

    2024-03-14 19:32:02       87 阅读
  4. Python语言-面向对象

    2024-03-14 19:32:02       96 阅读

热门阅读

  1. SpringCloud中Gateway提示OPTIONS请求跨域问题

    2024-03-14 19:32:02       41 阅读
  2. 如何详细自学python?

    2024-03-14 19:32:02       41 阅读
  3. 自动化运维工具Ansible之playbooks剧本

    2024-03-14 19:32:02       49 阅读
  4. Android 卫星通信计算方位角,仰角,极化角

    2024-03-14 19:32:02       36 阅读
  5. el-table 合集行合并

    2024-03-14 19:32:02       39 阅读
  6. 数据库基础知识

    2024-03-14 19:32:02       38 阅读
  7. WIFI攻击方法总结

    2024-03-14 19:32:02       48 阅读
  8. 并发、并行、串行有什么区别和联系?

    2024-03-14 19:32:02       39 阅读