你应该如何有效地批处理复杂的网格?

How should you efficiently batch complex meshes?

渲染复杂网格的最佳方法是什么?我在下面写了不同的解决方案,想知道你对它们有什么看法。

举个例子:如何渲染'Crytek-Sponza'网格?

PS:我不使用 Ubershader,只使用单独的着色器

如果在下面link下载mesh:

http://graphics.cs.williams.edu/data/meshes.xml

并将其加载到 Blender 中,您会看到整个网格由大约 400 个子网格组成,分别具有自己的 materials/textures。

虚拟渲染器(版本 1)将分别渲染 400 个子网格中的每一个!这意味着(为了简化情况)400 个绘制调用,每个绘制调用都绑定到 material/texture。对性能非常不利。很慢!

pseudo-code version_1:

foreach mesh in meshList //400 iterations :(!
 mesh->BindVBO();

  Material material = mesh->GetMaterial();
  Shader bsdf = ShaderManager::GetBSDFByMaterial(material);

  bsdf->Bind();
   bsdf->SetMaterial(material);
   bsdf->SetTexture(material->GetTexture()); //Bind texture

    mesh->Render();

现在,如果我们注意正在加载的材料,我们会注意到 Sponza 实际上仅由(如果我记性好的话:)) 25 种不同的材料组成!

所以更聪明的解决方案(版本 2)应该是分批收集所有 vertex/index 数据(在我们的例子中是 25 个)而不是将 VBO/IBO 存储到子网格 class es 但进入一个名为 Batch.

的新 class
pseudo-code version_2:

foreach batch in batchList //25 iterations :)!
  batch->BindVBO();

  Material material = batch->GetMaterial();
  Shader bsdf = ShaderManager::GetBSDFByMaterial(material);

  bsdf->Bind();
   bsdf->SetMaterial(material);
   bsdf->SetTexture(material->GetTexture()); //Bind texture

    batch->Render();

在这种情况下,每个 VBO 包含共享完全相同 texture/material 设置的数据!

好多了!现在我认为 25 VBO 用于渲染 sponza 太多了!问题是渲染 sponza 的 Buffer 绑定的数量!我认为一个好的解决方案应该是如果第一个 'full' 分配一个新的 VBO(例如,假设 VBO 的最大大小(VBO class 中定义的值作为属性)是 4MB或 8MB)。

pseudo-code version_3:

foreach vbo in vboList //for example 5 VBOs (depends on the maxVBOSize)

 vbo->Bind();

 BatchList batchList = vbo->GetBatchList();

 foreach batch in batchList

  Material material = batch->GetMaterial();
  Shader bsdf = ShaderManager::GetBSDFByMaterial(material);

  bsdf->Bind();
   bsdf->SetMaterial(material);
   bsdf->SetTexture(material->GetTexture()); //Bind texture

    batch->Render();

在这种情况下,每个 VBO 不包含共享完全相同 texture/material 设置的必要数据!这取决于子网格加载顺序!

好吧,VBO/IBO 绑定变少了,但没有必要减少绘制调用! (这个肯定你还好吗?)。但总的来说,我认为这个版本 3 比前一个好!你怎么看这个?

另一个优化应该是将 sponza 模型的所有纹理(或纹理组)存储在纹理数组中!但是如果你下载 sponza 包你会看到所有的纹理都有不同的大小!所以我认为它们不能绑定在一起,因为它们的格式不同。

但如果可能的话,渲染器的版本 4 应该只使用较少的纹理绑定而不是整个网格的 25 个绑定!你觉得可能吗?

那么,根据您的意见,渲染 sponza 网格的最佳方法是什么?您还有其他建议吗?

你关注的是错误的事情。两种方式。

首先,没有理由不能将网格的 所有 顶点数据粘贴到单个缓冲区对象中。请注意,这与批处理没有任何关系。请记住:批处理是关于 绘图调用的数量 ,而不是您使用的缓冲区的数量。您可以从同一个缓冲区中渲染 400 个绘制调用。

您似乎想要的 "maximum size" 是虚构的,完全没有现实世界中的任何内容。如果你愿意,你可以拥有它。只是不要指望它能让你的代码更快。

所以在渲染这个网格时,根本没有理由切换缓冲区。

其次,批处理与绘制调用的数量无关(在 OpenGL 中)。这实际上是关于 绘制调用之间状态变化的成本。

This video clearly spells out (about 31 minutes in),不同状态的相对成本变化。发出两个绘制调用且它们之间没有状态变化是便宜的(相对而言)。但是不同种类的状态改变有不同的代价。

更改缓冲区绑定的成本非常小(假设您正在使用 separate vertex formats,因此更改缓冲区并不意味着更改顶点格式)。更改 程序 甚至纹理绑定的成本要高得多。因此,即使您必须制作多个缓冲区对象(同样,您不必这样做),这也不会成为主要瓶颈。

因此,如果性能是您的目标,那么您最好关注昂贵的状态更改,而不是廉价的状态更改。制作一个可以处理整个网格的所有 material 设置的着色器,这样您只需要更改它们之间的制服。使用阵列纹理,以便您只有一个纹理绑定调用。这会将纹理绑定变成统一设置,这是一种成本更低的状态更改。

您甚至可以做更奇特的事情,包括基础实例计数等。但对于这样一个微不足道的例子来说,这太过分了。