【技术美术百人计划】延迟渲染
渲染路径
决定光照的实现方式,简而言之,就是当前渲染目标使用光照的方式
渲染方式

前向渲染
流程

待渲染物体--顶点着色器--片元着色器--渲染目标
在渲染每一帧时,每个顶点/片元都要执行一次片元着色器代码,这时需要将所有的光照信息都传递到片元着色器中。
虽然大部分情况下的光源都趋向于小型化,距离很远,效果较差,但在计算光源时还是会把所有的光源都计算进去。
规则(如何渲染每一帧的)和注意事项
发生在顶点处理阶段,会计算所有顶点的光照。(全平台支持)
规则1:最亮的几个光源会被实现为像素光照
规则2:然后就是,最多四个光源会被实现为顶点光照
规则3:剩下的光源会实现为效率较高的球面调谐光照(Spherical Hamanic),这是一种模拟光照
补充说明
最亮的那盏光一定是像素光照
Light的Render Mode是important的光一定是像素光照
如果前面的两条加起来的像素光照小于Quality Setting里的Pixel Light Count(最大像素光照数量),那么从剩下的光源中找出最亮的那几盏光源,实现为像素光照。
最后剩下的光源,按照规则2或3。
在base pass里执行一盏像素光、所有的顶点光和球面调谐光照,并且进行阴影计算。
其余的像素光每盏一个Additional Pass,并且这些pass里没有阴影计算。
场景中看到的阴影,全是base pass里计算出最亮那盏像素光的阴影,其他像素光是不计算阴影的。
最多的光源数是可以更改的
以Unity中的为例,在project setting中

所以,如果一个物体受到n个光源影响,那么每个片元着色器执行代码时,都必须把n个光源传递给着色器中进行计算。

延迟渲染
一句话概括:先不计算光照,延迟到最后再一起计算
主要解决大量光照的渲染方案
延迟渲染实质上,是先不要做迭代三角形做光照计算,而是先找出你能看到的所有像素,再去迭代光照。直接迭代三角形,会造成极大的浪费。
流程

流程为:待渲染几何体 → 顶点着色器 → MRT → 光照计算 → 渲染目标
过程可以拆分为两个pass
第一个pass:几何处理通路。
首先将场景渲染一次,获取到的待渲染对象的各种几何信息存储到名为G-buffer的缓冲区中,这些缓冲区用来之后进行更复杂的光照计算。
由于有深度测试,所以最终写入G-buffer中的,都是离摄像机最近的片元的集合属性,这就意味着,在G-buffer中的片元必定要进行光照计算。
第二个pass:光照处理通路。
这个pass会遍历所有G-buffer中的位置、颜色、法线等参数,执行一次光照计算。
一些注意事项
G-buffer的概念
G-Buffer,全称Geometric Buffer ,译作几何缓冲区,它主要用于存储每个像素对应的位置(Position),法线(Normal),漫反射颜色(Diffuse Color)以及其他有用材质参数。
根据这些信息,就可以在像空间(二维空间)中对每个像素进行光照处理。
如图为一个典型的G-buffer

UE4默认使用的是延迟管线
我们在视图模式---缓冲显示---总览,就可以看到所有G-buffer的预览
延迟渲染不支持透明物体的渲染
因为没有深度信息
延迟渲染中的透明物体渲染方式和前向渲染相同
伪代码

RT(G-buffer)相当于把整个屏幕的信息绘制到一个图中,每个RT都可以写到一个G-buffer中
G-buffer中的数据都是2D的,所以我们的光照计算就相当于一个2D的光照后处理
渲染优劣

优点
1.支持半透明渲染
2.支持使用多个光照pass
3.支持自定义光照计算方式
(延迟渲染是渲染到Gbuffer,再一起计算光照,所以不支持每一个物体用单独的光照方式计算)
缺点
1.光源数量对计算复杂度影响巨大
2.访问深度等数据需要额外计算(需要再渲染一张深度图)
2、延迟渲染的优点、缺点
优点
1.大量光照场景的情况下,优势明显
2.只渲染可见像素,节省计算量
3.对后处理支持良好(例如深度信息:直接拿G-buffer中的就行)
4.用更少的shader(所有的物体光照模型都一样,很多东西不用再定义了)
缺点
1.对MSAA支持不友好
2.透明物体渲染存在问题(深度问题,只渲染力物体最近的物体,渲染透明度时会出现问题)
3.占用大量的显存带宽
涉及一个clear的操作,如果不清理的话,后边可以继续获取到
每一帧都需要几张rt在显存中传输、清理等,会更耗带宽
4.只能使用同一个光照pass
其他部分
1、渲染路径的设置
Project Setting中进行设置


2、TBDR
有两个TBDR,名字一样,内容不同
第一个:
是SIGGRAPH2010提出的,作为传统Defferred Rendering的另一种主要改进,分块延迟渲染(Tile-Based Deferred Rendering,TBDR)旨在合理分摊开销(amortize overhead),自SIGGRAPH 2010上提出以来逐渐为业界所了解。基于延迟渲染的优化方式,通过分块来降低带宽内存用量(解决带宽和内存问题)

延迟渲染的分块,把整个图像分为很多块,再一块一块的渲染。
第二个:
PowerVR基于手机GPU的TBR框架提出的改进,通过HSR减少Overdraw
TBDR这个架构是PowerVR提出来的对TBR的一次改进,在TBR的基础上再加了一个Deferred。
通过做一些可见性测试来减少Overdraw
涉及手机GPU架构,和延迟渲染没什么关系
这里提到的关于TBR 、PowerVR提出的TBRD,作业部分有整理,详细的也可以看知乎的这篇文章https://zhuanlan.zhihu.com/p/259760974

3、其他渲染路径
延迟光照(Light Pre-Pass / Deferred Lighting)
减少G-buffer占用的过多开销,支持多种光照模型
和延迟渲染的区别:
用更少的buffe信息,着色计算的时候用的是forward,所以第三步开始都是前向渲染(可以对不同的物体进行不同的光照模型)
Forward+(即Tiled Forward Rendering,分块正向渲染)
减少带宽,支持多光源,强制需要一个preZ
通过分块索引的方式,以及深度和法线信息来到需要进行光照计算的片元进行光照计算。
需要法线和深度的后处理需要单独渲染一个rt出来
强制使用了一个preZ(如果没涉及过这个概念的话,可以理解为进行了一个深度预计算)
群组渲染(Clustered Rendering)
带宽相对减少,多光源下效率提升
分为forward和deferred两种
详细补充拓展:https://zhuanlan.zhihu.com/p/54694743
延迟渲染中MSAA存在的问题
前边补充的图里有提到“NO MSAA Possible”,也就是说延迟渲染管线不支持MSAA
MSAA在延迟渲染中存在的问题:像素已经被光栅化了,所以没法用更大的像素来渲染。
不同path下光源shader的编写
详情请看官方文档
PreZ(Zprepass)
实际上就是一个深度计算
和深度图的区别
都是深度信息
PreZ是用一个pass,只算深度
深度图是算成了一张RT(RenderTexture),把深度信息绘制到了一张RT上。
具体用途:
大规模草、透明排序会用到PreZ
early-z 和 PreZ的区别
early-z,自动的,对面数有要求(硬件自动)
PreZ,当early-z失效的时候,或者需要深度图的时候,一种手动代替的方案
后边课程还会细讲
7、一些补充
Unity的urp是不支持延迟渲染的,老管线支持。
UE默认管线就是延迟渲染管线
一般延迟渲染用于主机/大项目
【抄作业】
总结延迟渲染管线的优缺点
优点:
1.大量光照场景优势明显
2.只渲染可见像素,节省计算量
3.对后处理支持良好(例如深度信息:直接拿G-buffer中的就行)
缺点:
1.不支持MSAA
2.透明物体渲染不行
3.占用大量的显存带宽
涉及一个clear的操作,如果不清理的话,后边可以继续获取到
每一帧都需要几张rt在显存中传输、清理等,会更耗带宽
4.延迟渲染只能用同一套lighting pass(光照是一起计算的)
二、延迟渲染管线的优化(移动端)
1、出发点
既然提到优化,那么我们可以从缺点理解:在P1部分may佬提到,延迟渲染在移动端的一个很大的缺点:带宽占用过高。
这里我们就要提到PC端构架IMR和移动端构架TBR了
2、IMR
IMR的全称是Immediate Mode Rendering,我们通常说PC端GPU渲染用的就是下面这种架构

上图中,蓝色部分就是正常的渲染管线,灰色部分对应GPU中的显存(包括几何信息,纹理信息,深度Buffer还有FrameBuffer等等。)
这种渲染架构下信息传出的特点:
对于每一个具体的绘制,渲染管线里的读写操作都是直接在显存和GPU中传输数据的,比如说上图中灰色和蓝色之间交互的箭头,向上的箭头表示读取,向下的箭头表示写回。
缺点:
在这种架构下,每一次渲染完的Color和Depth数据写回到Frame Buffer和 Depth Buffer都会产生很大的带宽消耗。
3、TBR
TBR的由来:
移动端的瓶颈-功耗:因为移动端的硬件在设计最开始想到的最重要的问题就是功耗(功耗意味着发热量,意味着耗电量,意味着芯片大小,所以手机GPU的设计也是把功耗摆在第一位。)
而在GPU渲染过程中,对功耗影响最大的就是带宽,所以为了从设计层面减少带宽消耗,就延伸出了移动端的构架TBR
TBR是什么
TBR全称是Tile-Based Rendering,

上图中,可以看到两个灰色部分,因为手机端是没有显存的,所有最下边一层灰色部分是系统内存。
选择TBR的原因
在移动端,如果是一个1920*1080的屏幕上,每渲染一帧图像,对FrameBuffer的访问量是惊人的(各种test,blend,再算上MSAA, overdraw等等)
通常gpu的onchip memory(也就是SRAM,或者L1 L2 cache)很小,这么大的FrameBuffer要存储在离gpu相对较远的DRAM(显存)上,可以这样理解:
把gpu想象成你家,SRAM想象成小区便利店,DRAM想象成市中心超市,从gpu对framebuffer的访问就相当于一辆货车大量的在你家和市中心之间往返运输,带宽和发热量之巨大是手机上无法接受的。
所以移动端的gpu想到了一种化整为零的方法
把巨大的FrameBuffer分解成很多小块,使得每个小块可以被离gpu更近的那个SRAM可以容纳,块的多少取决于你的硬件的SRAM的大小。这样gpu可以分批的一块块的在SRAM上访问framebuffer,一整块都访问好了后整体转移回DRAM上,这样问题就解决了
使用TBR可以减少带宽的原因
对FrameBuffer现在几乎全部的访问在渲染这一块的时候(test,read,write,blend…)都完全在SRAM上解决,只在最后把这一块整体渲染完了才整体搬回DRAM。这种模式就叫做TBR(tile-based-rendering)
也就是说:对于上面图中TBR和系统内存数据传输部分的箭头,读取只发生在需要几何以及纹理信息的时候,写回也只发生在整批绘制画完的时候,具体的绘制都是在On_Chip Memory上完成的,也就是带宽消耗最大的DepthBuffer 和 ColorBuffer的读写都发生在On_Chip Memory上
TBR和IMR的对比如下图

TBR的过程/内容
对于TBR来讲,整个光栅化和像素处理会被分为一个个Tile进行处理(通常为16×16大小的Tile),先执行所有draw call的vertex shading,拼装成相应的图元后,接着对于每一个tile,各用一张链表(polygon list)来记录位于该tile中的图元;然后对于每一个tile中的每一个图元进行光栅化和fragment shading。
简要概括过程:
1.缓存所有绘制指令直到最后才开始进行真正绘制
2.首先对所有图元执行VS,把相关的所有绘制数据保存起来
3.计算每个tile都包含哪些图元
4.开始绘制一个tile
5.依次绘制tile中的每个图元
6.把绘制好的tile拷贝到framebuffer对应的位置上
TBR的结构通过On-Chip Buffers来储存Tiling后的Depth Buffer和Color buffer。
实际上TBR的实现策略
实际上,在TBR的架构里,并不是来一个绘制就执行一个的,因为任何一个绘制都可能影响到到整个FrameBuffer,如果来一个画一个,那么GPU可能会在每一个绘制上都来回运输所有的Tile,这太慢了。
所以,TBR一般的实现策略是对于Cpu过来的绘制,只对他们做顶点处理,也就是上图中的Geometry Processor部分,产生的结果(Frame Data【2】)暂时写回到物理内存,等到非得刷新整个FrameBuffer的时候【1】,GPU才知道拖不了了,就会将这批绘制做光栅化,做tile-based-rendering。
注【1】:对于非得刷新整个FrameBuffere的时候的解释:比如Swap Back and Front Buffer,glflush,glfinish,glreadpixels,glcopytexiamge,glbitframebuffer,queryingocclusion,unbind the framebuffer.总之是所有gpu觉得不得不把这块fb绘制好的时候
注【2】:FrameData是TBRr特有的在gpu绘制时所需的存储数据。

如图,上面是TBR,下面是IMR ,这里可以看到TBR的管线上在PixelShader之前增多了一个步骤,即vs和gs处理后的数据(这里叫做FrameData)被暂时保存下来排好队,然后后面再对framebuffer分块,然后对每一块,绘制所有影响这个块的pixel。
所以TBR在本质上就是DeferredRendering,因为它对于IMR来说,将Rasterize/PS的处理延后到了处理完所有VS之后,而不是像IMR一样VS后马上Rasterize/PS。
4、TBDR
什么是TBDR
虽然TBR也有一次Defer,但是实际上只有只有PowerVR的GPU是TBDR架构,别的移动GPU是TBR架构。
TBDR这个架构是PowerVR提出来的对TBR的一次改进,在TBR的基础上再加了一个Deferred。
PowerVR的渲染架构中还多了一步Rasterize到PS的延迟,PowerVR为处理这一步延迟的组件起名叫HSR(Hidden Surface Removal)并申请了专利
TBR和TBDR的区别
TBR:VS - Defer - RS - PS
TBDR:VS - Defer1 - RS - Defer2 - PS
通过这第二个Defer,PowerVR的渲染架构真正最大程度上实现了“延后(Defer)PS的执行”,以避免执行不必要的PS计算与相关资源调用的带宽开销,以达到最少的性能消耗和最高的渲染效率。
注意:有些文章会将TBR叫作TBDR,所以看资料的时候,要搞清楚它说的"D"是指第一个Defer还是第二个。
HSR技术
首先回顾一下之前深度测试有学到的Early-Z
具体的Early-Z内容不做整理了,可以回顾之前的笔记,简单的来说就是:绘制前先做深度检测,减少不必要的绘制。
但是Early-Z是不能完全避免OverDraw的,这是因为我们在真正对一个复杂场景去渲染的时候是不可能进行严格的由近到远的绘制的,例如下图

EarlyZ特性对渲染物体进行排序优化对于移动GPU也是同样适用,除了PowerVR(TBRD做的比Early-Z要更好)
PowerVR提出的HSR技术,不需要在软件层面对物体进行排序,直接提供硬件级别的支持来解决OverDraw。
简述原理:
当一个像素通过了EarlyZ准备执行PS进行绘制前,先不画,只记录标记这个像素归哪个图元来画。等到这个Tile上所有的图元都处理完了,最后再真正的开始绘制每个图元中被标记上能绘制的像素点。
这样每个像素上实际只执行了最后通过EarlyZ的那个PS,而且由于TBR的机制,Tile块中所有图元的相关信息都在片上,可以极小代价去获得,最终零Overdraw
至于为什么能够做到,这是利用了TBR的特点,我们知道来了一批Drawcall,在TBR架构下是不会立刻去画的,而是把几何阶段处理后的结果存在FrameData中,等到必须要画整个FrameBuffer的绘制的时候才会去着色,所以HSR发生在着色前,拥有这批绘制需要的所有几何信息,自然能够做到像素级别的深度测试支持,而不需要关心EarlyZ技术里面需要的绘制顺序问题。

5、优化建议
//以下摘自参考资料,详情可点开参考资料细看
记得不使用Framebuffer的时候clear或者discard
在每帧渲染之前尽量clear
不要在一帧里面频繁的切换framebuffer
贴图格式能压缩就压缩
能开mipmap就开mipmap
随机纹理寻址相对于相邻纹理寻址有显著开销
Trilinear/Anisotropic相对于Bilinear有显著的开销
使用LUT(look up texture)很可能是负优化
通道图能合并就合并,减少Shader中贴图采样次数
控制Framebuffer大小
避免大量的drawcall和顶点量
避免gpu上的copy-on-write