Render Hell —— 史上最通俗易懂的GPU入门教程(四)

声明:文本非原创,只是翻译,原文链接如下:
https://simonschreibt.de/gat/renderhell-book4/

Render Hell – Book IV

在这里插入图片描述
这下越来越有趣了!本篇我将向大家介绍一些我在研究过程中发现的一些解决方法,希望大家能够了解如何去优化资源,以使其具有良好的可渲染性。

1. 排序(Sorting)

首先,在填充命令缓冲区之前,你可以对所有的命令进行排序(例如按 Render State 进行排序),这样就可以大大减少那些不必要的 Change State 命令,因为在切换状态之前,你已经遍历了所有使用相同 Render State 的网格。

在这里插入图片描述
但是,如果挨个挨个的渲染每个网格,那么你仍然会面临大量的开销。为了减少这种开销,有一种叫做 批处理 的技术似乎很有用。

2. 批处理(Batching)

在对网格进行排序时,你可以将它们堆成相同的堆,下一步便是立即告诉 GPU 去渲染这样一个堆。以下是关于批处理的定义:

“批处理是指在调用 API 渲染网格之前,先将多个网格组合在一起。这就是为什么渲染一个大网格要比渲染多个小网格所花费的时间更少的原因” [a36]

因此,不要为每个(使用相同 Render State 的)网格都添加一个 Draw Call …

在这里插入图片描述
… 而应该将它们组合起来并封装成一个 Draw Call 来进行渲染。这是一个非常有趣的话题,因为只要使用相同的 Render State(换句话说使用相同的材质参数),就可以同时渲染不同的网格(石头、椅子或宝剑)。

在这里插入图片描述
值得一提的是,在系统内存(RAM)中合并网格,然后将合并后的大网格拷贝到显存(VRAM)上,这是要花时间的!因此,批处理对于静态对象(例如石头、房屋等)是很有用的,这些静态对象只需合并一次,就可以长时间保留在内存中了。
你也可以将动态对象(例如空间游戏中的激光子弹)批处理在一起,但因为它们是在不断运动的,你就不得不为每一帧创建子弹云网格并发送到 GPU 显存上!

另一个需要注意的地方(感谢 koyima 提醒):如果一个对象不在摄像机的视锥体范围内,你可以直接将它剔除掉(即在渲染时忽略它)。但是,如果将多个对象批处理在一起,那么在渲染的时候就不得不将整个大的新网格都考虑进去(即使其中只有一小部分是最终可见的),这在某些情况下这可能会降低 GPU 性能。

其实,有一种更好的方法用来处理动态对象,那就是实例化

3. 实例化(Instancing)

实例化的意思就是,你只需要发送一个网格(例如激光子弹)而不是多个,并让 GPU 对其复制多次。但这样我们只会得到位置完全相同、且采用相同旋转或动画的一组对象,那就没啥意思了。因此你需再要提供一个额外的数据流,如变换矩阵,这样就可以将那些副本渲染到不同的位置(和不同的姿势)了。

“每个实例的典型属性包括模型到世界的变换矩阵、实例的颜色以及一个骨骼蒙皮动画播放器。”[a37]

别跟我说了,但据我所知,这个数据流只不过是 RAM 中的一个列表,GPU 也可以访问这段内存。

这也使得每种类型的网格只对应一个 Draw Call!与批处理相比,它们的不同之处在于,所有实例化的网格看起来都长的一样(因为它们是同一网格的副本),而批处理的网格则由多个不同的网格组成,只要它们使用同一个 Render State 参数。

在这里插入图片描述
接下来开始变得更具创造性了,即使以下技巧只适用于某些特殊场景,我也觉得它们真的很酷:

4. 多材质着色器(Multi-Material-Shader)

因为一个着色器可以访问多个纹理,所以它不仅可以有一个漫反射 / 法线 / 镜面反射 /… 贴图,还可以有两个(或更多)纹理 —— 换句话说,你可以在一个着色器中使用两种材质,这些材质通过 混合纹理(blend-texture) 进行彼此混合。当然,这会增加 GPU 的功耗,因为混合操作本身的功耗就很高,但它减少了 Draw Call 的次数,因为含有两种或两种以上材质的网格将不再被大卸八块(如之前在 “4. 网格与多种材质” 中的描述)。

点击此处 阅读更多关于多材质着色器的相关信息。
在这里插入图片描述该文章表明,与这种高代价的技术相比,使用相对较多的 Draw Call 仍然是一个不错的选择。不管怎样,我觉得挺有意思的,如果你只是想要得到一些喜人的统计数字,那你可以反驳说多层材质降低了 Draw Call 的调用次数(即使这里讨论的并不是性能问题 … 但是,嘘!)。

5. 骨骼蒙皮技术(Skinned Meshes)

你还记得我说过的激光子弹吗?我说过这个网格必须每帧都更新一次,因为子弹是在不断运动的。但是将它们进行批处理然后每帧都发送新的网格,这代价确实有点大。
解决该问题的一个有趣方法是,自动为每颗子弹添加一个 骨骼(bone),并提供 蒙皮信息(skinning information)。有了它,你将拥有一个大网格,该网格可以保留在内存中,而你只需要更新每一帧的骨骼数据即可。当然,如果发射了一颗新子弹或销毁了一颗旧子弹,你就不得不创建一个新的网格。但对我来说这真的是一个很有趣的思路。

点击此处 了解更多关于骨骼蒙皮技术的相关信息。

6. 减少过度渲染(Reduce Overdraw)

Alex 提到了一个很好的方法,他在他的 App 中使用该方法来避免渲染一个暗角效果图时出现的过度渲染问题:

为了减少全屏暗角效果(通常用全屏四边形和半透明纹理来制作)的过度渲染问题,我们使用内部中空(即100%透明)的四边形网格,并使用顶点颜色而不是纹理。这样能产生更好看的暗角效果(没有纹理压缩伪影)和更好的性能(更少的过度渲染 —— 只会在不透明的区域进行绘制)。
—— Alex / Axiomworks

在这里插入图片描述
CryEngine 文档 也谈到了这一点,另外 这篇讨论 也非常值得一读。

7. 以及更多的魔法

图形编程是在不断变化的,API 和硬件都在不断的发展,编写高效的渲染引擎用于处理大量的用例已经不是一件容易的事了,它需要大量复杂的工程。幸运的是,许多游戏开发人员乐于分享他们的经验总结,人们可以在各种活动或开发网站上找到有关特效或渲染架构方面的有趣资料(siggraph 高级实时渲染(real-time rendering)课程、虚幻引擎(unreal engine)文档和源码、寒霜引擎(frostbite engine)的 PPT …),所有主流硬件厂商通常也有关于如何充分利用图形 API 以及他们硬件的文档、示例代码或 PPT。

如今的好处是,多亏了大量的渲染中间件,许多繁重的工作都已经帮我们处理完了。因此,对于美术师来说,主要还是要清楚他们在制作内容时,实际需要关注哪些优化,因为有些事情可能是在幕后完成的。

在这里插入图片描述请随时给我发送更多关于减少 Draw Call 的创造性解决方案的链接!

差不多就这些了!现在,您应该对如何更快地渲染资源有一个大致的理解了。别担心,下一篇文章会很短。


本篇到此结束。

在这里插入图片描述继续阅读下一篇:总结,或返回目录索引

已标记关键词 清除标记
相关推荐
©️2020 CSDN 皮肤主题: 编程工作室 设计师:CSDN官方博客 返回首页