【问题标题】:How should you efficiently batch complex meshes?您应该如何有效地批处理复杂的网格?
【发布时间】:2016-02-04 17:39:53
【问题描述】:

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

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

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

如果您通过以下链接下载网格:

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

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

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

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 版)应该是批量收集所有顶点/索引数据(在我们的示例中为 25 个),而不是将 VBO/IBO 存储到子网格类中,而是存储到一个名为 Batch 的新类中。

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 都包含共享完全相同的纹理/材质设置的数据!

好多了!现在我认为 25 VBO 用于渲染 sponza 太多了!问题是渲染 sponza 的缓冲区绑定的数量!我认为一个好的解决方案应该是分配一个新的 VBO,如果第一个“满”(例如让我们假设 VBO 的最大大小(在 VBO 类中作为属性定义的值)是 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 不包含共享完全相同的纹理/材质设置的必要数据!这取决于子网格加载顺序!

好吧,VBO/IBO 绑定减少了,但绘制调用没必要减少! (你对这个肯定没问题吗?)。但总的来说,我认为这个版本 3 比以前的版本更好!您对此有何看法?

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

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

那么,根据您的说法,渲染 sponza 网格的最佳方式是什么?你还有什么建议吗?

【问题讨论】:

  • "例如,假设最大大小为 4MB 或 8MB -> 这取决于您的硬件" 我们不是在您的其他问题中解决了这个问题吗?我在哪里明确指出,除了您的 GPU 有多少内存之外,没有最大大小?你最近见过只有 8MB 的嵌入式 GPU 吗?
  • 你在这些测试中的帧率是多少?如果您要达到 60 fps,那么每帧 400 个绘制调用(带有一些状态更改)确实不成问题。当然,更少总是可取的,但它不应该成为瓶颈。每帧 25 个绘制调用是花生。
  • 对不起,我想说如果我认为最大 VBO 大小(作为我的 VBO 类中的属性)定义为 4MB 或 8MB。 (我告诉它取决于硬件,因为在某些硬件上,VBO 分配可能会因给定大小而失败)。 4MB 对我来说是在执行我的 opengl 应用程序时定义的配置大小。

标签: opengl glsl shader


【解决方案1】:

你专注于错误的事情。有两种方式。

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

您似乎想要拥有的这个“最大尺寸”是一个虚构,没有任何来自现实世界的基础。如果你愿意,你可以拥有它。只是不要指望它能让你的代码更快。

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

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

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

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

因此,如果性能是您的目标,那么您最好关注代价高昂的状态更改,而不是便宜的状态更改。制作可以处理整个网格的所有材质设置的单个着色器,因此您只需更改它们之间的制服。使用数组纹理,以便您只有一个纹理绑定调用。这会将纹理绑定转换为统一设置,这是一种更便宜的状态更改。

您还可以做一些更有趣的事情,包括基本实例计数等。但对于像这样的一个微不足道的例子来说,这太过分了。

【讨论】:

  • 非常感谢您的所有解释,视频很有启发性。所以,我将把注意力集中在相关的状态变化上,比如使用纹理数组进行纹理绑定。关于顶点缓冲区,就像你说的那样,使用一个唯一的缓冲区来存储场景的整个顶点数据似乎更好。在同一个 VBO 中分批收集的所有顶点信息。
猜你喜欢
  • 2020-04-11
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-03-04
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多