1. 渲染路径
在Unity 里,渲染路径 (RenderingPath) 决定了光照是如何应用到 Unity Shader 中的, 我们需要为每个Pass指定它使用的渲染路径。
Pass
{
Tags{ "LightMode" = "ForwardBase" }
...
}
Unity 支持多种类型的渲染路径。前向渲染路径(Forward Rendering Path)、延迟渲染路径 (Deferred Rendering Path) 和顶点照明渲染路径 (Vertex Lit Rendering Path)。在 Unity 5.0 版本以后,顶点照明渲染路径已经被抛弃。
前向渲染(左)与延迟渲染(右)对比
1.1. 前向渲染路径
前向渲染路径是传统的渲染方式,也是我们最常用的一种渲染路径。
1.1.1. 原理
思路:每进行一次完整的前向渲染,我们需要渲染该对象的渲染图元,并计算两个缓冲区的信息:一个是颜色缓冲区,一个是深度缓冲区。我们利用深度缓冲来决定一个片元是否可见,如果可见就更新颜色缓冲区中的颜色值。
如图所示,渲染流程为:待渲染几何体 → 顶点着色器 → 片元着色器 → 渲染目标
在渲染每一帧的时,每一个顶点/片元都要执行一次片元着色器代码,这时需要将所有的光照信息传到片元着色器中。虽然大部分情况下的光照都趋向于小型化,而且照亮区域也不大,但即便是离这个像素所对应的世界空间的位置很远的光源,光照计算还是会把所有的光源考虑进去的。简单来说就是不管光源的影响大不大,计算的时候都会把所有光源计算进去,这样就会造成一个很大的浪费。
对于每一个像素,都需要执行一次Pass计算。假设,场景中有N个物体,每个物体受M个光源的影响,那么要渲染整个场景一共需要N*M个Pass。可以看出,如果有大量逐像素光照, 那么需要执行的Pass数目也会很大。因此,渲染引擎通常会限制每个物体的逐像素光照的数目。
1.1.2. Unity 中的前向渲染
1.1.2.1. 处理光照方式及其选择规则
在 Unity 中,前向渲染路径有3 种处理光照(即照亮物体)的方式:
- 逐顶点处理
- 逐像素处理
- 球谐函数 (Spherical Harmonics, SH) 处理
而决定一个光源使用哪种处理模式取决于它的类型和渲染模式。
- 光源类型指的是该光源是平行光还是其他类型的光源
- 而光源的渲染模式指的 是该光源是否是重要的 (Important)。
其中,一定数目的光源会按逐像素,最多4个光源按逐顶点(可更改,如下图),剩下的光源可以按SH。
Unity使用的判断规则如下。
- 最亮的平行光---逐像素
- Not Important---逐顶点或者SH
- Important---逐像素处理。
- 最后,如果
逐像素光源数批
<Quality Setting中的逐像素光源数批(Pixel Light Count)
,会有更多的光源以逐像素的方式进行渲染
1.1.2.2. 两种Pass
前向渲染有两种Pass: Base Pass和AdditionalPass
1.1.3. 内置的光照变量和函数
总结:前向渲染的每一个物体对每个光照都计算一次,为了节约性能Unity会使用不同处理光照的方式。
1.2. 顶点照明渲染路径
顶点照明渲染路径是对硬件配罚要求最少、运算性能最高,但同时也是得到的效果最差的一种类型,它不支持那些逐像素才能得到的效果,只是使用了逐顶点的方式来计算光照。
1.3. 延迟渲染路径
1.3.1. 原理
流程为:待渲染几何体 → 顶点着色器 → MRT 多渲染目标→ 光照计算 → 渲染目标
延迟渲染主要包含了两个Pass。
- 在第一个Pass 中,我们不进行任何光照计算,而是仅仅计算哪些片元是可见的,这主要是通过深度缓冲技术来实现,当发现一个片元是可见的,我们就把它的相关信息存储到G缓冲区G-Buffer(G 缓冲区存储了我们所关心的表面的其他信息,例如该表面的法线、位置、用于光 照计算的材质属性等)中。 这个Pass 仅会执行一次。
- 然后,在第二个Pass 中,我们利用G缓冲区的各个片元信息,进行真正的光照计算 。 默认的G缓冲区包含了以下几个渲染纹理(RenderTexture, RT)。
-
- 漫反射颜色
- 高光反射颜色
- 法线
- 自发光+lightmap+反射探针(reflection probes)
- 深度缓冲和模板缓冲
延迟渲染跟场景中包含的光源数目是没有关系的,而是和屏幕空间的大小有关。这是因为,我们需要的信息都存储在缓冲区中,而这些缓冲区可以理解成是一张张 2D 图像。它最适合在场景中光源数目很多、如果使用前向渲染会造成性能瓶颈的情况下使用。
可以自定义延迟渲染路径来满足项目的需求:
总结:延迟渲染就是分两次渲染,第一次计算哪些片元可见并将信息存到G-Buffer,第二次进行光照计算。适合多光源场景。
2. 案例
对于这样一个场景,包含两个点光源,一个Direction Light,和两个物体。
性能对比
Frame Debugger中渲染过程对比:
前向渲染
延迟渲染
3. 比较
3.1. 特性对比
1、后处理方式不同(如何需要深度信息进行后处理的话)
- 前向渲染需要单独渲染出一张深度图
- 延迟渲染直接用G-buffer中的深度信息计算即可
2、shader计算不同
- 延迟渲染,因为是最后统一计算光照的,所以只能算一个光照模型,如果需要其他光照模型,只能切换pass
3、抗锯齿方式不同
3.2. 优缺点对比
前向渲染的优缺点
优点
- 1.支持半透明渲染
- 2.支持使用多个光照pass
- 3.支持自定义光照计算方式
-
- (延迟渲染是渲染到Gbuffer,再一起计算光照,所以不支持每一个物体用单独的光照方式计算)
缺点
- 1.光源数量对计算复杂度影响巨大
- 2.访问深度等数据需要额外计算(需要再渲染一张深度图)
延迟渲染的优缺点
优点
- 1.大量光照场景的情况下,优势明显
- 2.只渲染可见像素,节省计算量
- 3.对后处理支持良好(例如深度信息:直接拿G-buffer中的就行)
- 4.用更少的shader(所有的物体光照模型都一样,很多东西不用再定义了)
缺点
- 1.对MSAA(Multisample anti aliasing,多重采样抗锯齿)支持不友好
- 2.透明物体渲染存在问题(深度问题,只渲染离物体最近的物体,渲染透明度时会出现问题)
- 3.占用大量的显存带宽
-
- 涉及一个clear的操作,如果不清理的话,后边可以继续获取到
- 每一帧都需要几张RT在显存中传输、清理等,会更耗带宽
- 4.只能使用同一个光照pass
4. 补充
Unity的渲染路径设置:
4.1. 移动端优化
见补充
4.2. 其他渲染路径
4.2.1. 延迟光照(Light Pre-Pass / Deferred Lighting)
减少G-buffer占用的过多开销,支持多种光照模型。和延迟渲染的区别:用更少的buffer信息,着色计算的时候用的是forward,所以第三步开始都是前向渲染(可以对不同的物体进行不同的光照模型)。
这个算法跟Deferred Shading差不多,Cryengine3早期版本中使用过该技术,目前基本上没有引擎再使用这个方法。
4.2.2. 分块正向渲染(Forward plus/ Tiled Forward Rendering)
减少带宽,支持多光源,强制需要一个preZ
- 通过分块索引的方式,以及深度和法线信息来到需要进行光照计算的片元进行光照计算。
- 需要法线和深度的后处理需要单独渲染一个rt出来
- 强制使用了一个preZ(如果没涉及过这个概念的话,可以理解为进行了一个深度预计算)
4.2.3. 群组渲染(Clustered Rendering)
带宽相对减少,多光源下效率提升。分为forward和deferred两种。
4.3. MSAA
计算机图形学四:抗锯齿SSAA及MSAA算法和遮挡剔除Z-Buffer算法_抗锯齿算法-CSDN博客
4.3.1. 走样
在介绍MSAA原理之前,首先了解走样(Aliasing),走样指的是因为对函数(信号)的采样频率不足,或者说信号的变化速率远大于采样的频率,使得出现不连续的现象称为走样。 走样分为三种:
- 几何体走样(几何物体的边缘有锯齿),几何走样由于对几何边缘采样不足导致。因此需要采用抗锯齿技术。
- 着色走样,由于对着色器中着色公式(渲染方程)采样不足导致。例如高光闪烁。
- 时间走样,主要是对高速运动的物体采样不足导致。比如游戏中播放的动画发生跳变等。
4.3.2. 超采样反走样SSAA
超采样技术就是以一个更大的分辨率来渲染场景,然后再把相邻像素值做一个过滤(比如平均等)得到最终的图像(Resolve)。因为这个技术提高了采样率,所以它对于解决上面几何走样和着色走样都是有效果的。如下图所示,首先经对每个像素取n个子采样点,然后针对每个子像素点进行着色计算。最后根据每个子像素的值来合成最终的图像。如下图将每个像素点细分成了4个采样点进行抗锯齿处理(可以用更多采样点,越多效果越好,性能耗费越大)。
4.3.3. MSAA
MSAA是对SSAA的一个改进。MSAA是一种抗锯齿技术,即多重采样抗锯齿(MultiSampling Anti-Aliasing,简称MSAA),这是一种在OpenGL中的特殊的超级采样抗锯齿(SSAA),MSAA主要是对 Z-Buffer 和 Stencil Buffer(模板缓冲)进行SSAA处理,其原理是通过提取像素界面周围的颜色信息,通过混合颜色信息来消除高对比界面所产生的锯齿。只对多边形的边缘进行抗锯齿处理。
缺点:资源耗费,画质上有些不如一般的SSAA。
MSAA依然同样会分采样点,但是只会去计算究竟有几个采样点会被三角形覆盖,计算颜色的时候只会利用像素中心坐标计算一次颜色(即所有的信息都会被插值到像素中心然后取计算颜色),如下图:
但是Direct X9完全不支持MSAA。
延迟渲染管线不支持MSAA。
MSAA在延迟渲染中存在的问题:延迟渲染依赖于每个片元存储的数据,这是通过纹理RT来完成的。这与多重采样抗锯齿不兼容,因为抗锯齿技术依赖于子像素数据。
4.4. PreZ(Z-Prepass)
在几何阶段(顶点着色器)后光栅化阶段(片元着色器)前再添加一次深度测试,将不通过测试的片元舍弃掉,不参加后续的计算,这一步操作被称为EarlyZ(详见3.1深度测试与模板测试):
应用阶段(CPU)->几何阶段(顶点着色器)->early-z(提前深度测试)->光栅化阶段(片元着色器)->各种测试(深度测试,透明度测试,模板测试等)->颜色缓冲区(buffer)
但是要注意,下面几种情况会使得EarlyZ失效:
- 开启AlphaTest或clip/discard等舍弃片元的操作
-
- 正常来说开启Alpha Test后,A的黑色部分片元被舍弃,能够看到后面B C两个不透明物体;但是开启EarlyZ后,首先记录下了A的深度信息,到了AlphaTest理应舍弃A物体黑色部分,渲染B C,然而因为B C无法通过深度测试被舍弃而出现错误渲染结果。
- 修改深度值,同理。
- 开启Alpha blend,一般会关闭深度写入(ZWriteOff),这就导致EarlyZ无法写入深度信息。
为了解决上述的问题,我们可以用Z-PrePass。我们使用两个Pass:
- PrePass:第一个Pass,仅开启深度写入,不输出任何颜色信息,先渲染不透明物体(Opaque),再渲染透明物体(也可以称为Transparent Mask),渲染透明物体的时候做Clip。
- BasePass:第二个Pass,关闭深度写入,并且将深度比较函数设置为Equal,正常渲染输出颜色信息,先渲染不透明物体再渲染透明物体,但是这时不开启Clip。
但是这样显然多了一个pass的消耗,会出现两倍的drawcall。而如果PixelShader非常复杂的话,这么也是值得的,所以还是要看具体的项目才能确定技术方案。
简单来说EarlyZ是底层硬件写好无法修改且在某些情况下会失效,而Z-PrePass则是在EarlyZ失效情况下的一种手动替代方案,目的都是为了减少overdraw节省性能所做的操作。
5. 作业
移动端如何优化延迟渲染管线?
移动端采用了TBDR架构,优化的建议有:
如果何时不再需要 RenderTexture 的当前内容,清除当前缓冲区可以提高性能。
RenderTexture.DiscardContents()
在每帧渲染之前尽量clear
不要在一帧里面频繁的切换framebuffer
压缩贴图格式
尽量打开mipmap
随机纹理寻址相对于相邻纹理寻址有显著开销(?)
纹理寻址模式 - UWP applications | Microsoft Learn
Filer Mode减少Trilinear/Anisotropic使用,使用Bilinear/point
- 减少使用LUT(look up texture)(滤镜)
合并贴图通道,减少Shader中贴图采样次数
unity编辑器拓展十一——将两张RGB图合并成一张_unity 合并图片-CSDN博客
【Untiy】在Unity中拼合贴图到通道工具_哔哩哔哩_bilibili
控制Framebuffer大小
避免gpu上的copy-on-write,不要在同一帧内多次改变提交给gpu的资源,这会迅速把framedata撑大到装不下的状态。
其他见《Unity Shader入门精要》第十六章-Unity中的渲染优化技术