一种帮助优化GPU渲染的算法
这个算法是干什么的这个算法可以为大量物体渲染优化提供理论指导,可以指导编写GPU驱动的渲染管线。如果你正在编写游戏引擎,或者从事渲染管线编写、优化方面的工作,请继续阅读。
文章为纯理论,没有具体实现,也没有推导公式或推导过程。
问题引入
从简单的场景看渲染
让我们从只有一个光源和一个模型的场景开始研究,画出一张图像,然后分析它是如何画出来的。
Kizuna AI模型和一个点光源
我们可以从非常多的视角来分析这个问题,但是在这里我们只关注GPU在绘制这张图像时使用了哪些信息,更具体点说我们只关注“绘制”这个动作使用了哪些信息。
为了简化问题,将整个场景简化为只有物体,而所有场景相关的信息都存在于物体中。
假设绘制这个模型只有一次绘制调用,那么在本次绘制调用中,只有两个信息被使用,分别是:模型、光源。
复杂场景
让我们思考一下更复杂的场景,比如两个点光源和两个模型
一个红色光源和一个绿色光源
绘制这张图像需要两次绘制调用,第一次绘制调用使用了三个信息:模型1、光源1、光源2,第二次绘制调用也使用了三个信息:模型2、光源1、光源2。
思考过后得到以下推论:光源越多,绘制调用使用的信息越多。模型越多,绘制调用次数越多。
推论
虽然我们可以对所有的场景给出这种推论,但是我们也不懂这种推论有什么用。所以直接给结论:游戏引擎里的Shader,可以处理绘制调用中的所有情况。对于每一个情况的绘制调用,游戏引擎都至少有一个Shader来处理。才能正常的渲染出画面。
这种道理每一个写渲染器的人都会懂的,但我还有一个重要的结论:渲染游戏画面的代码要么在产生这种绘制调用,要么在产生绘制调用合集(合批、实例化)。所以研究这个问题有助于我们优化渲染过程,从最大程度上优化渲染,甚至有助于研究从GPU上发出渲染指令,也就是所谓的GPU Driven Pipeline。
研究一般的场景
让我们从更一般的场景开始研究。这个场景中有天空光照,平行光,一个相机,和两个模型。
为了方便理解,我画一张图片,展示两次绘制调用所需的信息:
然后考虑有2个相机的情况,下图是绘制调用所需的信息:
相机的数量增加了1个,绘制调用的次数从2次变成了4次。引入相机会使绘制调用增加。
我这里有一种算法可以求出绘制调用所包含的信息。接下来将会介绍一下这个算法。
算法
输入、输出
这个算法的输入有:场景、场景元素间的关系。算法的输出是绘制调用所需的信息。
例如:对于上文中2个相机、1个天空光、2个模型、1个平行光的场景,输入除了场景本身外、还有场景元素间的关系。我认为元素之间只有两种关系,将其命名为+(加)和×(乘)。例子里元素间的关系表示如下:
{{相机×模型}, {天空光+模型}, {平行光+模型}}
计算没有顺序。计算的输出如下图。+代表将前面的元素附加到后面的元素,×代表将两个元素排列组合
和上一张图片一样
可以从计算结果中得出一部分结论:在不做优化的情况下,渲染这一帧需要4次绘制调用(绘制了2个相机),并且每次绘制调用需要如上图所示的信息。优化建议:如果上图中的模型2和模型1是一样的,则可以使用批处理一次处理两个原始绘制调用。
延迟渲染
上文中的计算结果是针对前向渲染的。如果需要使用这个算法来计算延迟渲染,则需要修改输入中的关系,使其和延迟渲染相适应。
我们假设GBuffer是和相机一一绑定的,因此不再引入新元素。
针对延迟渲染的输入如下:
2个相机、1个天空光、2个模型、1个平行光的场景。{相机×模型}, {平行光×相机}, {天空光×相机}
计算结果如下图:
从计算结果得知,使用延迟渲染渲染一帧需要6次绘制调用。当然你可以使用不同的方法绘制,将关系更改如下。
{相机×模型}, {{平行光+相机}, {天空光+相机}}
计算结果如下图
此时需要4次绘制调用来完成一帧。同时,一次绘制调用需要同时处理平行光和天空光。
光源剔除的情况
一个大的点光源和一个小的点光源
考虑2个点光源、2个模型、天空光照、1个相机的情况。从左至右给模型编号1、2号。1号模型接受1个点光源,2号模型接受2个点光源。
{{点光源+模型}, {天空光+模型}, {相机×模型}}
加入了限制条件(但是没有直接在符号上体现),计算结果如下图
一次调用处理两个光源
此时需要两个绘制调用来完成一帧。在处理这两个绘制调用时,可以使用同一个Shader,也可以使用两个不同的Shader,这取决于程序员的代码能力或是性能基准测试。
你也可以通过检查计算结果,找出存在高性能代价的光照。
镜子或传送门
只需加入{镜子×模型},这个有很多种组合可以满足(你可能需要虚空镜子),结果是每个镜子都会让绘制调用增加很多。
光照探针(组)
使用普通的相机更新光照探针。在数据视角处理光照探针时就像处理普通的光照那样。
优化渲染
这个算法的计算结果是你优化渲染的基础。你可以设计一个算法,自动的将渲染进行合批和实例化,或是手动分析数据,选出最有效的处理方法,提高CPU或是GPU的处理速度。
引入效果
通常来说游戏里会渲染一些粒子、贴花、动画纹理、描边之类的特效,同样可以通过这个算法进行计算。只要你编写的Shader足够多或是足够复杂,可以处理出现的每一种数据组合,那么一定不会出现渲染错误。如果出现了某一种数据组合,而没有适当的Shader对其进行处理,那就要考虑一下是否会出现渲染错误。
举个例子,某两个技能特效都会给模型附加上动态的火焰,但是Shader没办法同时处理这两个火焰,所以只会有一个特效被播放。解决办法是编写一个能同时处理这两个特效的Shader(但确实有难度,一般不会处理)。
另一个例子:许多游戏引擎单个物体所能接受的光照被限制了,这是出于性能考虑的。
另一个例子:有些游戏的镜子不会反射粒子效果,因为它的粒子效果只会在主相机渲染一次。
转向GPU Driven Pipeline
从数据的角度来看,GPU驱动的渲染管线和普通的渲染管线并没有什么不同。但是使用GPU驱动的渲染管线必然有些限制,这样才能最大程度的发挥显卡的性能。在有了以上算法之后,我认为GPU驱动的渲染管线可以在以下方面介入渲染:
[*]收集渲染信息,将场景中的元素联系起来
[*]对收集到的渲染信息进行分组
[*]使用收集到的渲染信息发起绘制调用
[*]使用通用Shader处理不可合批的绘制调用
GPU驱动的渲染管线还会处理网格簇的情况,但是已经不在文章的研究范围内了。如果将网格簇当成普通的模型的话,这个算法可以显示渲染每个模型所需的信息,渲染时需要考虑处理这些信息。
给算法起个名字吧
因为算法还没实现,没有起名的想法,也不知道起什么名字好。
页:
[1]