Rendering Path其实指的就是渲染场景中光照的方式。由于场景中的光源可能很多,甚至是动态的光源。所以怎么在速度和效果上达到一个最好的结果确实很困难。以当今的显卡发展为契机,人们才衍生出了这么多的Rendering Path来处理各种光照。
正向渲染(Forward Rendering),或称正向着色(Forward Shading),是渲染物体的一种非常直接的方式,在场景中我们根据所有光源照亮一个物体,之后再渲染下一个物体,以此类推。要使用Forward Rendering,一般在Vertex Shader或Fragment Shader阶段对每个顶点或每个像素进行光照计算,并且是对每个光源进行计算产生最终结果。
下面是Forward Rendering的核心伪代码。
For each light:
For each object affected by the light:
framebuffer += object * light
传统的正向渲染思路是,先进行着色,再进行深度测试。其的主要缺点就是光照计算跟场景复杂度和光源个数有很大关系。假设有n个物体,m个光源,且每个每个物体受所有光源的影响,那么复杂度就是O(m*n),所以比较适合户外这种光源较少的场景(一般只有太阳光)。因为如果在 vertex shader 中计算光照,其复杂度将是O(num_geometry_vertexes ∗ num_lights),而如果在 fragment shader 中计算光照,其复杂度为O(num_geometry_fragments ∗ num_lights) 。可见 光源数目和复杂度是成线性增长的。
正向渲染简单直接,也很容易实现,但是同时它对程序性能的影响也很大,因为对每一个需要渲染的物体,程序都要对每个光源下每一个需要渲染的片段进行迭代,如果旧的片段完全被一些新的片段覆盖,最终无需显示出来,那么其着色计算花费的时间就完全浪费掉了。
在Unity中,前向渲染规则如下:一个场景中多光照情形,按光源的重要程度排序,ABCD四盏灯用效果最好的逐像素方式渲染光照,DEFG用逐顶点方式渲染光照,GH则用球谐光照方式计算光照。(关于球谐光的一些知识,可以参考我的这篇文章https://blog.csdn.net/yinfourever/article/details/90205890)
Unity中正向渲染分为一个Base Pass和多个Additional Passes.
Base Pass: 用逐像素的方式渲染一个directional light, 所有球谐光和逐顶点光。在这个pass里也会计算lightmap等,directional light可以计算阴影,要注意如果使用了lightmap照亮的物体不会被球谐光照亮。
Additional Passes:每个其他需要用逐像素渲染的光源将会用一个Addtional pass渲染,默认情况下这些灯光不会有阴影除非使用了 multi_compile_fwdadd_fullshadows variant shortcut
更详细的信息可以参考Unity官方文档中关于Forward Rendering的介绍https://docs.unity3d.com/2019.2/Documentation/Manual/RenderTech-ForwardRendering.html
延迟渲染( Deferred Rendering),即延迟着色(Deferred Shading),顾名思义,是将着色计算延后进行处理的一种渲染方法,在2004年的GDC上被正式提出 http://www.tenacioussoftware.com/gdc_2004_deferred_shading.ppt。
延迟着色给我们优化拥有大量光源的场景提供了很多可能性,因为它能够在渲染拥有成百上千光源的场景的同时还能够保持能让人接受的帧率。下面这张图展示了一个基于延迟着色渲染出的场景,这个场景中包含了1000个点光源,对于目前的硬件设备而言,用传统的正向渲染来实现几乎是不可能的。
基于Deferred Rendering 渲染的含1000个点光源的场景 [J. Andersson, SIGGRAPH 2009 Beyond Programmable shading course talk] @ Frostbite 2引擎可以将延迟渲染( Deferred Rendering)理解为先将所有物体都先绘制到屏幕空间的缓冲(即G-buffer,Geometric Buffer,几何缓冲区)中,再逐光源对该缓冲进行着色的过程,从而避免了因计算被深度测试丢弃的⽚元的着色而产⽣的不必要的开销。也就是说延迟渲染基本思想是,先执行深度测试,再进行着色计算,将本来在物空 间(三维空间)进行光照计算放到了像空间(二维空间)进行处理。
可以将延迟渲染理解为两个Pass的过程:
1、几何处理阶段(Geometry Pass)。这个阶段中,我们获取对象的各种几何信息,并将第二步所需的各种数据储存(也就是渲染)到多个G-buffer中;
2、光照处理阶段(Lighting Pass)。在这个pass中,我们只需渲染出一个屏幕大小的二维矩形,使用第一步在G-buffer中存储的数据对此矩阵的每一个片段计算场景的光照;光照计算的过程还是和正向渲染以前一样,只是现在我们需要从对应的G-buffer而不是顶点着色器(和一些uniform变量)那里获取输入变量了。
延迟着色中一个非常重要的概念就是G-Buffer,下面先聊一下G-Buffer。
G-Buffer,全称Geometric Buffer ,译作几何缓冲区,它主要用于存储每个像素对应的位置(Position),法线(Normal),漫反射颜色(Diffuse Color)以及其他有用材质参数。根据这些信息,就可以在像空间(二维空间)中对每个像素进行光照处理。G-Buffer根据需求可以存储不同的内容,以下为一个典型的G-Buffer layout,对于Unity中的Deferred Rendering,G-Buffer layout就与之下的不同,关于Unity的部分之后会再细说。
下面是一帧中G-buffer中存储的内容:
延迟渲染方法一个很大的好处就是能保证在G-buffer中的片段和在屏幕上呈现的像素所包含的片段信息是一样的,因为深度测试已经最终将这里的片段信息作为最顶层的片段。这样保证了对于在光照处理阶段中处理的每一个像素都只处理一次,所以我们能够省下很多无用的渲染调用。除此之外,延迟渲染还允许我们做更多的优化,从而渲染更多的光源。
在几何处理阶段中填充G-buffer非常高效,因为我们直接储存位置,颜色,法线等对象信息到帧缓冲中,这个过程几乎不消耗处理时间。而在此基础上使用多渲染目标(Multiple Render Targets, MRT)技术,我们可以在一个Pass之内完成所有渲染工作。
总结一下,典型的Deferred Rendering 的渲染流程有两步:
通用版本的延迟着色算法伪代码:
For each object:
Render to multiple targets
For each light:
Apply light as a 2D postprocess
正向渲染:
延迟渲染:
延迟渲染的优点
Deferred Rendering 的最大的优势就是将光源的数目和场景中物体的数目在复杂度层面上完全分开。也就是说场景中不管是一个三角形还是一百万个三角形,最后的复杂度不会随光源数目变化而产生巨大变化。
延迟渲染的缺点
在Unity5之后,使用了延迟渲染(deferred rendering)代替了之前版本的延迟光照(Deferred Lighting light prepass)关于延迟光照,在下一小节会简要介绍。
在Unity中G-buffer layout:
The g-buffer pass renders each GameObject once. Diffuse and specular colors, surface smoothness, world space normal, and emission+ambient+reflections+lightmaps are rendered into g-buffer textures. The g-buffer textures are setup as global shader properties for later access by shaders (CameraGBufferTexture0 ..CameraGBufferTexture3 names).
The lighting pass computes lighting based on g-buffer and depth. Lighting is computed in screen space, so the time it takes to process is independent of Scene complexity. Lighting is added to the emission buffer.
延迟渲染只支持standard lighting model,如果想替换自己的shader效果,可以修改lighting pass shader。
更详细的信息可以参考Unity官方文档 https://docs.unity3d.com/Manual/RenderTech-DeferredShading.html
Light Pre-Pass即Deferred Lighting(延迟光照),旨在减少传统Defferred Rendering使用G-buffer 时占用的过多开销(reduce G-buffer overhead),最早由 Wolfgang Engel于2008年在他的博客(http://diaryofagraphicsprogrammer.blogspot.com/2008/03/light-pre-pass-renderer.html)中提到的。
延迟光照的具体的思路是:
关于延迟着色和延迟光照,经常会被弄混,这边简单区分一下。
Deferred Rendering与Deferred Lighting在思想上的主要异同:
两种方法的上述操作均是只能完成对不透明物体的渲染,而透明或半透明的物体则需额外的传统Pass来完成。
两者流程图的对比:
在Unity5版本之前,其实使用的就是Deferred Lighting技术,现在版本的Unity依然保留了该渲染模式,称为Legacy Deferrred。
Lighting Rendering Path. 分为Base pass,Lighting Pass,Final Pass在这里就不细分析了,主要技术原理和之前讲的基本一致, 详细信息可以查看Unity的官方文档
https://docs.unity3d.com/Manual/RenderTech-DeferredLighting.html
明明浅墨大神的文章说Deferred Lighting是Deferred Shading的优化,那为什么Unity要从Deferred Lighting转换成Deferred Shading?
我个人的理解是:
个人理解有可能不太正确,不负任何责任,还望大佬们指教。
作为传统Defferred Rendering的另一种主要改进,分块延迟渲染(Tile-Based Deferred Rendering,TBDR)旨在合理分摊开销(amortize overhead),自SIGGRAPH 2010上提出以来逐渐为业界所了解。实验数据表明TBDR在大量光源存在的情况下明显优于上文提到的Light Pre-Pass。
我们知道,延迟渲染的瓶颈在于读写 G-buffer,在大量光源下,具体瓶颈将位于每个光源对 G-buffer的读取及与颜色缓冲区(color buffer)混合。这里的问题是,每个光源,即使它们的影响范围在屏幕空间上有重疉,因为每个光源是在不同的绘制中进行,所以会重复读取G-buffer中相同位置的数据,计算后以相加混合方式写入颜色缓冲。光源越多,内存带宽用量越大。
而分块延迟渲染的主要思想则是把屏幕分拆成细小的栅格,例如每 32 × 32 象素作为一个分块(tile)。然后,计算每个分块会受到哪些光源影响,把那些光源的索引储存在分块的光源列表里。最后,逐个分块进行着色,对每像素读取 G-buffer 和光源列表及相关的光源信息。因此,G-buffer的数据只会被读取1次且仅1次,写入 color buffer也是1次且仅1次,大幅降低内存带宽用量。不过,这种方法需要计算光源会影响哪些分块,这个计算又称为光源剔除(light culling),可以在 CPU 或 GPU(通常以 compute shader 实现)中进行。用GPU计算的好处是,GPU 计算这类工作比 CPU 更快,也减少 CPU/GPU 数据传输。而且,可以计算每个分块的深度范围(depth range),作更有效的剔除。
也就是说,TBDR 主要思想就是将屏幕分成一个个小块 tile。然后根据这些 Depth 求得每个 tile 的 bounding box。对每个 tile 的 bounding box 和 light 进行求交,这样就得到了对该 tile 有作用 的 light 的序列。最后根据得到的序列计算所在 tile 的光照效果。
对比 Deferred Rendering,之前是对每个光源求取其作用区域 light volume,然后决定其作用的的 pixel,也就是说每个光源要求取一次。而使用 TBDR,只要遍历每个 pixel,让其所属 tile 与光线求交,来计算作用其上的 light,并利用 G-Buffer 进行 Shading。一方面这样做减少 了所需考虑的光源个数,另一方面与传统的 Deferred Rendering 相比,减少了存取的带宽。
Forward+ == Forward + Light Culling[6]。Forward+很类似Tiled-based Deferred Rendering。其具体做法就是先对输入的场景进行z-prepass,也就是说关闭写入color,只向z-buffer写入z值。注意此步骤是Forward+必须的,而其他渲染方式是可选的。接下来来的步骤和TBDR很类似,都是划分tiles,并计算bounding box。只不过TBDR是在G-Buffer中完成这一步骤的,而Forward+是根据Z-Buffer。最后一步其实使用的是forward方式,即在FS阶段对每个pixel根据其所在tile的light序列计算光照效果。而TBDR使用的是基于G-Buffer的deferred rendering。
实际上,forward+比deferred运行的更快。我们可以看出由于Forward+只要写深度缓存就可以,而Deferred Render除了深度缓存,还要写入法向缓存。而在Light Culling步骤,Forward+只需要计算出哪些light对该tile有影响即可。而Deferred Render还在这一部分把光照处理给做了。而这一部分,Forward+是放在Shading阶段做的。所以Shading阶段Forward+耗费更多时间。但是对目前硬件来说,Shading耗费的时间没有那么多。
周末去上海参加了Unite大会,讲道理确实干货满满。其中有一场技术演讲就是关于Unity最新Render path的,Unity最新的SRP渲染管线中,加入了Fine Pruned Tiled Light Lists和Clustered Shading技术,提供Tiled Forward Rendering和Tile-BasedDeferred Rendering,Fine Pruned Tiled Light Lists和Clustered Shading技术原理可以参照Gpu Pro7中的文章,我暂时也还没有去读,之后会抽时间去学习下。
Unity中通过Fine Pruned Tiled Light Lists技术为deferred和forward两种管线提供更优化的剔除功能,相关的一些知识可以参考我下边拍摄的照片,等Unity更新版本提供这些功能后以及我个人学习更多相关知识后,我会再更新这篇文章。
总结下本文目前已经提到的Rendering Path有:
等有时间和精力,随着新技术的发展会更加完善本篇文章。
本文参考以下几篇大神的文章:
https://blog.csdn.net/poem_qianmo/article/details/77142101
https://www.cnblogs.com/polobymulberry/p/5126892.html