【转载】Shader学习 (20)延迟渲染和前向渲染

720 阅读9分钟

原文链接

Shader学习 (20)延迟渲染和前向渲染 | 九猫

原文写得很好,但是主题格式我不太喜欢,所以我对文章的格式进行了修整,方便自己日后阅读

正文

简述延迟渲染和前向渲染

用一句话概括,前向着色就是 先计算光照再裁剪,延迟着色就是 先裁剪再计算光照

前向渲染

前向渲染是现在最基础,也是最多引擎使用的标准。前向渲染的 流程 是给定一个几何体,引擎对其进行从顶点到像素着色器的一系列计算,然后输出到最终的图像缓冲区。场景中有多个几何体时,引擎就是对其 挨个 进行渲染,完成一个再继续下一个。

前向渲染的问题

前向渲染有一个问题就是 无效渲染 太多,比如场景中有四个物体,互相之间存在叠压关系,按照前向渲染的流程,先前渲染了一个物体之后,它的一部分被后一个渲染的物体挡住了,那么被挡住的这部分就是无效的计算,毕竟我们在屏幕上是看不到这部分的。

另一个问题在于难以支持 过多的光源,对于每个需要逐像素计算的光源,渲染一个几何体的时候需要逐个做一次光照计算。如果有一个场景,其中有 10 个几何体需要进行渲染,有 4 个光源对整个场景产生影响,那么渲染整个场景需要进行 40 次光照计算。而且其中还有很多的计算被挡住了。

前向渲染的优点

如果需要在场景中使用 多个着色模型,甚至是每个几何体都使用 不同的 着色模型和渲染技术,前向渲染是可以很好的支持的。

另外,因为前向渲染这种逐个渲染的特点,它非常适合渲染 半透明 物体。

image.png

上面图中就是简单的前向着色的示意图。一个几何模型,进入渲染流程中,首先就是要经过顶点着色器的计算,顶点着色器一般会对顶点做空间变换,有的还有顶点位移和 UV 偏移等计算。经过顶点着色器之后就到了曲面细分着色器,这个阶段是可选的,用来给模型的表面增加顶点数,并且产生更细微的凹凸,当然游戏中的大部分模型是不会做这个阶段的计算的,一般用在高质量游戏的渲染。几何着色器也是可选的,一般会用来做基于模型表面的顶点渲染出一片草丛等功能。像素着色器当然是大部分模型都要经历的,用来做采样贴图和计算光照等计算。

前向实时剔除

前向渲染的缺点这么明显,难道没有人想办法解决一下吗?当然有一些解决办法的,比如Early-ZZ-PrepassHi-Z 等剔除方法。

  1. Early-Z : 传统的渲染管线中深度测试是在计算完光照之后才进行的,但是计算完光照再进行深度剔除,被挡到的部分就白计算了。在光栅化阶段,每个模型的深度就已经写入深度缓存了,为什么不先进行深度剔除再计算光照呢? 这就是 Early-Z 功能,这也是现在所有硬件和渲染管线都有的功能。

  2. Z-Prepass :上面说的 Early-Z 优化虽然是在每个模型计算光照之前就进行了深度剔除,但是还是不能完全避免无效光照计算的问题。比如第一个模型渲染的时候,深度通道是没有信息的,这时哪怕这个模型会被后来渲染的模型完全遮挡,它也会计算光照。Z-Prepass 就可以 解决这个问题,首先把整个场景中的模型都渲染一遍,全部都写入 Z-Buffer ,这次渲染 除了 Z-Buffer 其他的信息都不计算。然后再渲染一遍场景,这次渲染关闭深度写入,每个像素和 Z-Buffer 中已经存在的深度信息进行对比,只有通过测试的像素才会计算光照。这种方法虽然避免了无效的光照计算,但是却执行了两次顶点着色器,所以最好是在场景中物体的 光照计算非常复杂但是顶点数量却不是很多 的情况下使用。

  3. Hi-Z :上面两种方法都是在 GPU 段进行的,而 Hi-Z 这种方法是在 CPU 端进行的,在几何体被提交到 GPU 之前会进行遮挡测试,如果几何体被别的物体遮挡了就不会提交到 GPU 。这种方法直接减少了 GPU 需要渲染的几何体数量,适用于 细碎且数量多 的模型。

延迟渲染

延迟渲染可以支持 大量的实时光照,所以现在的大型电脑游戏基本都已经是延迟渲染了。

延迟渲染就是 把光照计算延迟到深度测试之后 的渲染方式,延迟着色适合在场景中实时光照很多的情况下使用,而且延迟着色可以 对每个光源都按逐像素 的方式计算。那 Z-prepass 也是先渲染出深度缓存进行深度测试后再计算光照的,和延迟渲染 有什么不同?最大的不同点就在于 G-buffer ,Z-prepass 在深度测试后也还是按照一个几何体渲染完再进行下一个这种方式来渲染的 ,延迟渲染是几何体的信息传递到 G-buffer 之后就和几何体没多大关系了,接下来的操作都是对 G-buffer 进行的。

如果说前向渲染中几何体数量 N 和光照数量 M 产生的计算量是 N*M 的话,延迟渲染产生的计算量就是 N+M 。这是因为 延迟渲染的思路 就是先把几何体的信息都渲染到二维空间中(G-Buffer),然后把 G-Buffer 整体进行光照计算,G-Buffer 中存在的信息都是会最终呈现在屏幕上的,不会有无效计算。

那么 G-Buffer 中到底会储存哪些信息?不同的引擎对于 G-Buffer 的处理也是不一样的,对于一个针对 PBR 渲染的引擎来说,至少会有 深度、模板、颜色、法线、世界位置、金属、粗糙、高光 这些信息。

在 UE4 的缓冲显示中可以很方便的查看这些缓冲中的信息。

如果把延迟渲染的流程做一个图来示意的话就是这样:

image.png

延迟渲染的优点

当场景中所有几何体都写入 G-Buffer 之后,光照计算就和场景中有多少物体,多少三角形没有关系了。

只渲染可见的像素,不会有无效的计算。

延迟渲染的缺点

延迟渲染要求场景中要 使用更少的 Shader 种类,毕竟如果每个模型的每个材质都使用不同的Shader ,那到底应该在 G-Buffer 中储存哪些信息呢?这对于程序和项目管理上来说可以说是个巨大的优点,可以节省性能,优化流程什么的。但是对美术来说却是个 大缺点,因为限制了美术的表现效果,所以我一定要把它写在缺点里面。

前向渲染只需要深度缓冲和最终的颜色缓冲就够了,延迟渲染需要缓存的信息实在太多了,这造成了 带宽的开销大幅增加。这也是为什么现在的手机游戏基本都是前向渲染的原因,毕竟手机带宽太小,延迟渲染虽然看起来很美好,但是用不了。

对于 半透明无能为力,所有半透明的物体都需要等待不透明物体以延迟渲染完成之后,在用前向渲染的方式渲染半透明物体。

分块延迟渲染

虽然延迟渲染已经把光照数量和三角形数量不再关联了,但是当场景中的光源数量不断上升之后,就算是延迟渲染也有点撑不住。

这时就需要分块延迟渲染,它的 思路 是把 G-Buffer 分成很多个小块,分析每个小块受到哪些光源的影响,然后逐个分块进行着色,那些受光源影响的数量少的小块就不需要那么多的光照计算了。这种渲染方式在光源数量少的情况下效果不明显,但是光源数量越多它优化的性能也就越多。

关于引擎

现在的游戏引擎比如 Unity 和 UE4 ,不管是延迟渲染还是前向渲染都有做支持,你可以在这两个引擎中选择一种渲染管线来使用。虽然二者都支持两种渲染管线,但是给人们的印象里,还是 Unity 用前向渲染的比较多,而 UE4 则几乎是代表了延迟渲染,实际体验中也可以得出相似的结论。

现在一般做次世代游戏都选择 UE4,比如《盗贼之海》《星球大战》等。

而风格化的游戏都不太选择 UE4,Unity 对风格化的游戏具有更好的支持,可以很方便的配置各种渲染效果。而 UE4 如果想要方便的获得一个风格化效果,只能是靠后处理来实现,而要想获得真正理想的风格化效果,就需要花很大力气来改管线了。

手游一般都是前向渲染,而且国内的手游一般都是用 Unity 做的,最近这些年也有很多项目开始使用 UE4 做手游了,但是即使是使用 UE4 做手游也都是前向渲染。

这里不得不提一个异类就是《原神》,作为使用 Unity 开发的手游,居然使用了延迟渲染管线,可以说是一次非常有前瞻性的尝试了。我认为随着以后手机的性能不断进步,人们对画面质量的要求越来越高,延迟渲染也迟早会在手机上铺开的。