渲染复杂网格物体的最佳方法是什么?我在下面写了不同的解决方案,并想知道你对它们有什么看法。
让我们举一个例子:如何渲染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的新类中。 p>
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的Buffer绑定数量!我认为一个好的解决方案应该是分配一个新的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比前一个更好!你怎么看待这个?
另一个优化应该是将selfza模型的所有纹理(或纹理组)存储在纹理的数组中!但是如果你下载了sponza包,你会发现所有纹理都有不同的尺寸!所以我认为由于它们的格式差异,它们不能捆绑在一起。
但是如果可能的话,渲染器的版本4应该只使用较少的纹理绑定而不是整个网格的25个绑定!你认为这可能吗?
那么,根据你的说法,渲染sponza网格的最佳方法是什么?你有其他建议吗?
答案 0 :(得分:5)
你专注于错误的事情。有两种方式。
首先,没有理由不将所有网格物体的顶点数据粘贴到单个缓冲区对象中。请注意,这与批处理有 nothing 。请记住:批处理是关于绘制调用的数量,而不是您使用的缓冲区数量。您可以从同一个缓冲区中渲染400个绘制调用。
这个"最大尺寸"您似乎想拥有的是小说,基于现实世界中的任何内容。如果你愿意,你可以拥有它。不要期望它能让你的代码更快。
因此,在渲染此网格时,根本没有理由切换缓冲区。
其次,批处理并不是关于绘制调用的数量(在OpenGL中)。它实际上是关于绘制调用之间的状态更改的成本。
This video clearly spells out (about 31 minutes in),不同状态变化的相对成本。发出两个绘制调用而它们之间没有状态变化是便宜的(相对而言)。但是不同类型的国家变化有不同的成本。
更改缓冲区绑定的成本非常小(假设您正在使用separate vertex formats,因此更改缓冲区并不意味着更改顶点格式)。更改程序甚至纹理绑定的成本要高得多。所以即使你不得不制作多个缓冲对象(你也不必这样做),这不会成为主要的瓶颈。
因此,如果性能是您的目标,那么您最好专注于昂贵的状态变化,而不是廉价的状态变化。制作一个可以处理整个网格的所有材质设置的着色器,这样您只需要更改它们之间的制服。使用数组纹理,以便您只有一个纹理绑定调用。这会将纹理绑定转换为统一设置,这是一种更便宜的状态更改。
你甚至可以做更好的事情,包括基本实例计数等。但对于这样一个微不足道的例子来说,这太过分了。