Deferred Rendering

2020-08-27  本文已影响0人  离原春草

导言

最早为大众知晓并接受的渲染框架或者说渲染模式是前向渲染(Forward Rendering)。前向渲染,用一句话来概括,就是对于场景中的每个物件,都进行一次渲染,而在这一次渲染中,需要对场景中的所的光源都处理一遍(这种处理可以放在一个Shader中完成,也可以放在多个shader中完成,当放置在多个shader中完成时,就需要经过多个pass才能将所有的shader都运行一遍,而这种方式的成本会比较高,所以对于前向渲染而言,通常都是直接使用一个shader完成所有光源的计算处理),并将多个光源的处理结果融合起来作为最终的结果输出到屏幕上。

前向渲染的缺点主要有以下几个,

上面陈述的是最简单的前向渲染的基本思路与实现效果,实际上,还是可以通过一些方法来对前向渲染进行优化的:比如建立一个光源列表,维护这个光源所覆盖的物体的清单,在渲染的时候,按照光源来进行渲染,可以避免shader分支的额外消耗,以及无关物件的计算消耗,但是这种方式也需要通过多个pass来完成,每个pass处理一个或者一组光源,之后将这些pass的输出结果融合得到最终的输出结果。

前向渲染的流程如图所示

按照前向渲染的概念,如果场景中有M个物体,N个光源,那么最坏的情况下,每个物体都处在所有光源的笼罩之下,那么最终需要处理的复杂度就是O(M * N)。每个光源对于物件上的像素的最终输出是起着怎样的一个作用呢,是各自独立对物件施加影响,并在最后将这个颜色值相加吗?按照正常情况,一张白纸,在上面投下强度相同的红光与绿光,其最终显示的颜色应该是两者的叠加,从这个角度揣测,多种光源的作用下,独立计算并叠加就可以了。在场景复杂度较高,或者光源数目较多的情况下,其渲染一帧所需要的时间代价是极为高昂的。如果能找到一种渲染方法,使得其进行光照渲染的时间成本不随场景复杂度而变化就好了,基于这种想法,人们提出了延迟渲染(Deferred Rendering)框架。

对于场景中的每个物体,在渲染的时候都需要经过Vertex Shader(VS)与Fragment Shader(FS)两个处理阶段:物体网格数据被分割成一个个的三角面片,三角面片中的顶点经过VS的处理后,经过光栅化(Rasterization)处理后变成一个个的Fragment,之后经过FS的光照计算处理(实际上,光照处理也可以放在VS阶段,不过其输出结果往往呈现出不连贯的面片状,与真实世界的表现相差太远,所以一般都将光照放在更为精细化的FS中处理,虽然消耗有所增长,但是效果是符合预期的),光照处理完成后输出到FrameBuffer中。

在前向渲染模式下,OverWrite会导致复杂场景存在极大的浪费。而延迟渲染可以将场景中的多个光源的计算处理过程一次性完成,且只有那些处在光源的覆盖范围内的,且最终在屏幕上可见的像素才会参与到光照计算中来,大大节省了时间成本?那么延迟渲染到底是怎么做到的呢?

简单来说,就是将光照的计算处理从之前的逐物体进行,改为只在最终的的back buffer上进行。这其实可以理解成是一种后处理(Post-process),准确来说,是一个multipass post-process,其总体流程如\ref{deferred}所示,总的来说,可以分成三个pass:

G-Buffers

延迟渲染中,最关键核心的一个概念,就是Geometry Buffer,俗成G-Buffer。G-Buffer包含了屏幕上各个像素用于计算光照shading所需的全部元素(如坐标数据,法线数据,深度数据,材质贴图数据等,而通常情况下,为了节省,一般坐标数据是不需要存储的,而是在需要的时候根据深度数据计算得到)。G-Buffer 中常见的几个元素有用于计算diffuse color 的基色贴图Albedo,用于计算高光数据的Specular 贴图(通常包含金属度与粗糙度数据?),还有光影计算必须的一些基础元素如法线贴图与深度贴图等,其表现如图所示:

一般来说,如果将G-Buffer的数据都存储在一张贴图中,那么这个贴图的单个像素的长度肯定非常大,而实际上,G-Buffer的数据通常会分别存储在多张贴图中,这就是我们俗称的Multiple Render Target (简称MRT),而为了尽可能的节省带宽与内存,在如何对数据进行存放也成为了一种艺术。如图所示,此处一共使用了五张贴图用于存储G-Buffer 的数据:

延迟渲染具体流程

延迟渲染,一般可以分成两个阶段:第一个阶段是geometry render阶段,这个阶段的输入是顶点数据,输出则是G-Buffer的各种元素,在这个阶段中,会处理之前前向渲染所应该处理的除了光照之外的一切计算,将后续计算需要的结果存入G-Buffer中;第二个阶段则是shading render阶段,这个阶段的输入就是我们前一个阶段的输出,也就是G-Buffer数据。如前面所述,这是一个后处理过程,也就是说,从本质上来说,这就是一个复杂的Fragment/Pixel Shader执行过程,这个阶段会从G-Buffer中取出与当前计算的Fragment相对应的数据,并对每个光照按照选定的光照方程(Light Equation)计算其光照输出,之后各个光源的输出按照一定的方式(Additive Blending)融合输出到FrameBuffer中。

在多个光源的情况下,延迟渲染的流程大概可以用伪代码来描述:

延迟渲染最终输出到屏幕上的图像表现跟前向渲染输出的结果是一样的,不同的是调整了渲染管线中各个子环节的渲染顺序,避免了许多不必要的计算与处理:

从而实现了整个渲染过程的加速。

延迟渲染算法的改进

延迟渲染的优点是:

而其缺点则在于:

对于上述的缺点,是否有较好的解决方案呢?实际上,是可以做一些改进的,如:

过程 结果

Tile-based Deferred Shading是另外一种对延迟渲染进行提速的实现算法。其基本思想是将G-Buffer分割成一个个的tile,之后对于每个tile,计算各个光源是否与之相交(注意,在Z轴上是需要比较MinZ与MaxZ的),之后对覆盖到这个tile的光源进行处理,即每个tile 加上若干光源作为一个group,之后逐group 的进行处理。而这种思想通过借助于GPU的SIMD特性实现CPU并行计算功能的compute shader,可以得到极大的加速:

这种方法的具体思路大概如图所示:

其实施效果大致如图所示,可以看到,场景中光源数目越多,这种算法对于帧率提升的幅度也越高。

而Quad-based Culling与Tile-based Culling的效果对比如图所示,可以看到在光源数目越多的情况下,Tile-based Culling方法的收益越高:

关于Tile-based Culling与Deferred Shading以及Deferred Lighting(因为使用了单色specular而导致性能有所提升),也有相关的对比数据,如图:

延迟渲染与MSAA

我们知道,MSAA是对于场景中的每个三角面片,都对其覆盖的每个像素进行一次FS/PS计算,之后将结果赋予这个像素中被三角面片所覆盖的采样点sample,最终在输出最终图像的时候,就将单个像素中所有sample的值加起来求平均。

而从刚才描述的延迟渲染的实现流程来看,在最终计算输出到屏幕的FrameBuffer的结果时,已经没有所谓的三角面片,每个G-Buffer 的像素只有一个sample数值,不再存在多个颜色sample,以便在最终输出的时候进行相加求平均了(而这也是透明或者半透明物件的渲染与延迟渲染不兼容的原因),所以,从这个角度来看,延迟渲染与MSAA 算法是不兼容的。

不过从MSAA的原理上来看,我们实际上可以用类似SSAA的方式来实现类似的MSAA,即只对面片的边缘处进行per-sample的计算(每个像素多次计算),而对于其他完全被面片覆盖的像素处,则只进行per-pixel的计算即可。当然,数据的存储量变成了多倍(比如最常见的2x2就是4倍),不过这些数据在其他场景中也是有用的,比如可以用在shadow map上消除锯齿。

而要实现上述MSAA,首先需要解决的问题,就是需要侦测到图像的边缘,通常对边缘的检测有以下几种方式:

这种自己实现的MSAA方法跟前向渲染中的MSAA方法对比,还有自己独特的好处,比如对于边缘比较光滑的三角面片的交界处,并不需要进行逐sample的渲染,进一步降低了计算量。

在Quad-based Deferred Rendering中使用MSAA一般首先要先辨别边缘像素,之后才对边缘像素进行per sample采样计算。而辨别边缘的方式又可以分成shader branch以及stencil两种,shader branch效率低,而stencil则需要使用两个pass(一个标记per sample像素并渲染,另一个标记per pixel像素并渲染),这就会导致需要对光照进行两次Culling操作,虽然如此,但还是比shader branch速度快。

在Tile-based Deferred Rendering中使用MSAA相对于Quad-based Deferred Rendering中有所改进,per sampler以及per pixel的渲染都是放在一个shader pass中完成,且为了避免shader branch,在shader的主流程中只处理sample 0的计算结果,而像素中剩下的sample则通过computer shader的多线程计算功能分散到其他线程中完成。

两种方式实施的性能对比如图所示:

Deferred Shading 与 Deferred Lighting

在延迟渲染的概念中,经常会出现Deferred Shading以及Deferred Lighting的说法,这两者分别代表什么意思,又有什么区别呢?

实际上,Deferred Shading就是我们前面所描述的Deferred Rendering的内容:对所有的物体进行一遍渲染,将后续LIghting/Shading 所需要的所有数据存入G-Buffer,之后通过多个pass对多个灯光进行Lighting+Shading后进行混合,或者只用一个pass对所有的光源进行处理并输出结果,流程如图所示。

deferred shading

与Deferred Shading不同,Deferred Lighting将Lighting与Shading分开,在Lighting阶段,只处理光照相关数据,并将结果输出到一个Normal Buffer中(如这个Buffer中可能只包含Normal,Depth以及Specular相关参数,Normal占用两个float,剩余两个参数各占一个,如果Lighting阶段只需要输出一个Normal Buffer就足够的话,那么就可以去除Deferred Rendering对于MRT的限制,同时也可以避免MRT 带来的带宽与OverWrite浪费),Lighting 的输出结果是Diffuse+Specular Color,在有些项目中,为了将这两者输出到一张Buffer 中,而选择将Specular 压缩到一个通道中(单色Specular 在绝大部分情况下其实基本上都是够用的)。Lighting 之后,就进入了Shading 阶段,在这个过程中,将Lighting阶段得到的光照数据取出来,对物体进行Shading,为了能够处理物体材质相关的逻辑(比如支持头发,次表面散射,自发光等),在这个阶段会对所有的物体再进行一次Geometry Pass (由于之前已经获得了全场景的Depth 数据,这一次的Geometry Pass 要快上很多),各个物体的材质相关数据如Albedo 贴图信息等都可以在进行Geometry Pass的过程中直接取到,从而可以根据不同的材质实现不同的渲染方式。 其流程如图所示。

deferred lighting

通常来说,相对于Deferred Lighting而言,Deferred Shading的主要的局限性在于:

在Deferred Lighting模式下,Geometry Rendering阶段的所有输出,只需要用以支持Lighting即可,从而大大减少了前面提到的Deferred Shading的OverWrite导致的浪费,且因为Lighting需要的信息比较少,比如只需要Normal数据+Depth数据+Specular数据(在不那么挑剔的情况下,这些数据可以直接塞入一张buffer中,从而摆脱平台不支持MRT的限制),从而也可以避免Deferred Shading的高带宽消耗。但是,需要注意的是,Deferred Lighting也有着自身的缺陷:

实际上,仔细分析,相对于Deferred Shading,Deferred Lighting只是将Lighting与Shading分开(一个Pixel Shader变成两个,处理的功能不变),另外增加了一次Geometry Pass(相当于增加了一个Vertex Shader处理),从前面的优劣对比猜想这两者的性能消耗不一定会有比较大的差距,但实际上,大部分情况下Deferred Lighting对于Deferred Shading的改进都无法抵消新增的Vertex Shader的消耗,导致在绝大部分情况下,Deferred Shading的性能都要更胜一筹。

针对Deferred Lighting的优势与弊端,有人整出了一种混合Deferred Rendering的方式[Hybrid Deferred Rendering,Marries van de Hoef],其基本Pipeline如图所示:

其渲染效率对比如图所示:

延迟渲染下的阴影实现方式

在延迟渲染模式下,主要有以下几种实现阴影的方式:

上一篇下一篇

猜你喜欢

热点阅读