学习参考
【技术美术百人计划】图形 3.4 延迟渲染管线介绍
《Unity Shader 入门精要》
1 Unity的渲染路径
关于渲染路径,我在图形渲染管线1.0中就提过了,但只是初步的了解了渲染路径有前向渲染、延迟渲染、Forward+等等,也了解到了前向渲染做了很多无效渲染,难以支持过多光源;延迟渲染可以支持大量的实时光照,基于缓存,因此对空间的要求也比前向渲染高。
这一篇博客我们再在Unity中了解一下这两种渲染路径里面的门道,同时自己上手看看前向渲染和延迟渲染在Unity中的对比效果。
1.1 设置渲染路径的2种方法
Unity中可以给当前项目设置一个总体的渲染路径,如Project Settings -> Graphics:
也可以给不同的摄像机设置不同的渲染路径,这个就在每个摄像机的Inspector窗口设置,可以选择与Graphics Settings同步,也可以选择Forward/Deferred等等:
还需要知道,Camera的Rendering Path设置是优先于整个项目的设置的。
1.2 指定Pass的渲染路径
在之前的实践中已经尝试过为每个Pass设置LightMode标签,例如:
Pass {Tags {"LightMode"="ForwardBase"}
}
表示当前Pass使用的是前向渲染路径中的ForwardBase路径。
Pass还可以设置的LightMode标签类型有:
Tags | 解释 |
Always | 当前Pass总是会被渲染,但不会计算人和光照 |
ForwardBase | 前向渲染,该Pass会计算环境光、最重要的平行光、逐顶点/SH光源和Lightmaps |
ForwardAdd | 前向渲染,该Pass会计算额外的逐像素光源,每个Pass对应一个光源 |
Deferred | 延迟渲染,该Pass会渲染G-buffer |
ShadowCaster | 把物体的深度信息传递给阴影映射纹理(shadowmap)或一张深度纹理中 |
PrepassBase | 遗留的延迟渲染,该Pass会渲染法线和高光反射的指数部分 |
PrepassFinal | 用于遗留的延迟渲染,该Pass通过合并纹理、光照和自发光来渲染得到最后的颜色 |
Vertex/VertexLMRGBM/VertexLM | 用于遗留的顶点照明渲染 |
2 前向渲染
2.1 原理
前向渲染是最常用的一种渲染路径,原理也通俗易懂:首先利用深度缓存决定当前渲染对象的每个图元的每个片元是否可见,如果可见再计算光照结果并储存进帧缓存中,如下的伪代码展示了前向渲染的大概流程:
Pass {for (each primitive in this model) {for (each fragment covered by this primitive) {if(failed in depth test) {discard; //没通过深度测试,则该片元不可见} else { //该片元可见,可继续进行光照计算float4 color = Shading(...);//更新帧缓冲,写入到颜色缓冲中writeFrameBuffer(fragment, color);}}}
}
2.2 Unity中如何限制光照数目?
我们讨论过,如果一个物体在多个光源的影响范围内,那肯定要执行很多次Pass,每个Pass都计算一个光照结果,最后混合所有的结果得到最终的颜色值。大量的逐像素光照会带来大量的Pass,为了节省开销,引擎通常会限制每个物体的逐像素光照数目。
Unity中,Project Settings -> Quality -> Pixel Light Count对逐像素光照数目做了限制,默认为4,意味着一个物体可以接受除了场景中最亮的平行光外的4个逐像素光照。
2.3 三种处理光照的方式
渲染一个物体时,Unity会计算哪些光源照亮了它,以及计算这些光源照亮该物体的方式。处理光源照亮物体的方式有3种:逐顶点处理、逐像素处理、球谐函数(Spherical Harmonics, SH)处理。
- 逐顶点和逐像素我在【Unity Shader】实现基础光照模型中就已经尝试了
- 关于球谐函数,它是相对于逐像素和逐顶点效率更高的、一种模拟光照的计算方式。大概去学习了一下,发现跟之前101讲的BRDF的辐射度量学只是有重合的点,准备之后再进行系统的了解,所以这里先挖个坑,不做过多的介绍~
2.3.1 光源的渲染模式和类型
我们已经知道了有三种处理光照的方式,下一步肯定就是考虑Unity是通过什么判断?答案是——通过设置光源的渲染模式(Render Mode)和类型(Type)来实现的,二者都可以在光源Light的Inspector窗口设置:
- RenderMode——有Auto/Important/Not Important三个选项
- Type——即光源类型,spot/point/directional/area四个选项
2.3.2 Unity如何判断?
那么,Unity是如何判断的?前向渲染中,Unity会根据场景中光源的设置和光源对物体的影响程度对光源做一个重要度排序,影响程度包括:光源距离物体的远近、光源强度等。距离好说,当其他参数都相同时,越近当然越重要;但对于距离、强度和光颜色等到底是如何考虑得到排序的我们并不知道(Unity文档也未告知),仅仅知道这个重要度排序跟这么多参数都有关。
通常Unity会使用以下判断规则:
- 最亮的平行光总是按照逐像素考虑的
- Render Mode设置成Not Important的光源,会按逐顶点或SH的方法处理
- Render Mode设置成Important的光源,会按逐像素处理
- 光源数量小于上面提到的Pixel Light Count,会有更多的光源按逐像素处理
2.4 Base Pass和Additonal Pass
光照计算都是在Pass中实现的。而对于前向渲染,Pass的“LightMode”标签有“ForwardBase”和“ForwardAdd”两个选项,也就意味着前向渲染通常都包含两个Pass。
Pass | 可实现的 光照效果 | 渲染设置 | 进行的光照计算 | 执行次数 | ||
标签 | 额外编译指令 | 混合模式 | ||||
Base | 光照纹理/ 环境光/自发光/平行光的阴影 | ForwardBase | #pragma multi_compile_fwdbase | 无 | 一个逐像素的平行光和SH光源 | 仅1次 |
Additonal | 默认不支持阴影 | ForwardAdd | #pragma multi_compile_fwdbadd | Blend One One | 其他逐像素光照-point和spot | 每个光源执行1次 |
需要补充的是,
- 关于渲染设置的额外编译指令:只有设置了编译指令,Unity才会在对应的Pass中获得正确的光照变量,具体有哪些光照变量,后面会进行补充
- 关于阴影:Base Pass支持平行光阴影(取决于光源的Shadow Type是否开启阴影),Additional Pass默认无阴影(即使Shadow Type设置了阴影类型),需要额外使用#pragma multi_compile _fwdadd_fullshadows代替#pragma multi_compile_fwdbadd为point和spot光源设置阴影
- 关于Blend模式:Additional Pass开启设置了混合模式,为什么要开启?因为一个Shader里Base Pass只会执行一次(排除双面渲染的情况),而Additional Pass会被多次调用,这就需要处理每次光照结果在帧缓存中的叠加,于是就需要定义Blend,通常使用的是Blend One One
2.5 内置的光照变量和函数
直接盘点一下前向渲染的内置光照变量:
名称 | 类型 | 描述 |
_lightColor0 | float4 | 该Pass处理的逐像素光源的颜色 |
_WorldSpaceLightPos0 | float4 | _WorldSpaceLightPos0.xyz是内置变量,如果该光源是平行光,则_WorldSpaceLightPos0.w是0,其他光源的值是1 |
_LightMatrix0 | float4X4 | 世界空间->光源空间的变换矩阵 |
unity_4LightPosX0, unity_4LigthPosY0, unity_4LightPosZ0 | float4 | 仅用于Base Pass,前4个非重要点光源在世界空间中的位置 |
unity_4LightAtten0 | float4 | 仅用于Base Pass,储存了前4个非重要的点光源的衰减因子 |
unity_LightColor | half4[4] | 仅用于Base Pass,储存着颜色 |
还有内置光照函数,这些函数都是仅用于前向渲染的:
函数名 | 描述 |
float3 WorldSpaceLightDir(float4 v) | 输入一个模型空间的顶点位置,得到lightDir,但没有被归一化 |
float3 UnityWorldSpaceLightDir(float4 v) | 输入世界空间的顶点位置,返回世界空间中lightDir |
float3 ObjSpaceLightDir(float4 v) | 输入模型空间的顶点位置,返回模型空间中的lightDir |
float3 Shade4PointLights(...) | 计算4个点光源的光照 |
2.6 实现前向渲染
Shader "Unity Shaders Book/Chapter 9/ForwardRendering"
{Properties{_Diffuse ("Diffuse", Color) = (1, 1, 1, 1)_Specular ("Specular", Color) = (1, 1, 1, 1)_Gloss ("Gloss", Range(8, 256)) = 20.0}SubShader {// Pass for ambient light & directional lightPass {Tags {"LightMode"="ForwardBase"}CGPROGRAM#pragma vertex vert#pragma fragment frag#include "Lighting.cginc"//need to add this declaration#pragma multi_compile_fwdbase//propertiesfixed4 _Diffuse;fixed4 _Specular;float _Gloss;struct a2v {float4 vertex : POSITION;float3 normal : NORMAL;};struct v2f {float4 pos : SV_POSITION;float3 worldNormal : TEXCOORD0;float3 worldPos : TEXCOORD1;};v2f vert(a2v v) {v2f o;o.pos = UnityObjectToClipPos(v.vertex);o.worldNormal = UnityObjectToWorldNormal(v.normal);o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;return o;}fixed4 frag(v2f i) : SV_Target {fixed3 worldNormal = normalize(i.worldNormal);fixed3 worldLightDir = normalize(UnityWorldSpaceLightDir(i.worldPos));fixed worldViewDir = normalize(UnityWorldSpaceViewDir(i.worldPos));fixed3 halfDir = normalize(worldViewDir + worldLightDir);fixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb * saturate(dot(worldNormal, worldLightDir));fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(saturate(dot(halfDir, worldNormal)), _Gloss);fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;//attenuation of directional lightfixed atten = 1.0;return fixed4 (ambient + (diffuse + specular) * atten, 1.0);}ENDCG}Pass {Tags {"LightMode"="ForwardAdd"}Blend One OneCGPROGRAM#pragma vertex vert#pragma fragment frag#include "Lighting.cginc"#include "Autolight.cginc"#pragma multi_compile_fwdadd//propertiesfixed4 _Diffuse;fixed4 _Specular;float _Gloss;struct a2v {float4 vertex : POSITION;float3 normal : NORMAL;};struct v2f {float4 pos : SV_POSITION;float3 worldNormal : TEXCOORD0;float3 worldPos : TEXCOORD1;};v2f vert(a2v v) {v2f o;o.pos = UnityObjectToClipPos(v.vertex);o.worldNormal = UnityObjectToWorldNormal(v.normal);o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;return o;}fixed4 frag(v2f i) : SV_Target {fixed3 worldNormal = normalize(i.worldNormal);#ifdef USING_DIRECTIONAL_LIGHTfixed3 worldLightDir = normalize(UnityWorldSpaceLightDir(i.worldPos));#else //is pointlightfixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz - i.worldPos.xyz);#endiffixed worldViewDir = normalize(UnityWorldSpaceViewDir(i.worldPos));fixed3 halfDir = normalize(worldViewDir + worldLightDir);fixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb * saturate(dot(worldNormal, worldLightDir));fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(saturate(dot(halfDir, worldNormal)), _Gloss);fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;//attenuation of directional light#ifdef USING_DIRECTIONAL_LIGHTfixed atten = 1.0;#else//1.Change point from world to lightspace, add-> "Autolight.cginc"float3 lightCoord = mul(unity_WorldToLight, float4(i.worldPos, 1)).xyz;//2.samplefixed atten = tex2D(_LightTexture0, dot(lightCoord, lightCoord).rr).UNITY_ATTEN_CHANNEL;#endifreturn fixed4 (ambient + (diffuse + specular) * atten, 1.0);}ENDCG}}FallBack "Specular"
}
效果如下:
3 延迟渲染
我们知道前向渲染是每个光源对物体的影响都计算出来,但随着场景中光源数量增多,需要计算的三角面和顶点数量也会增多,那前向渲染的性能会急速下降!这个时候延迟渲染就体现出它的优点了。
3.1 原理
延迟渲染是一种比较古老的方法,它分为两个Pass,但和前向渲染的截然不同(前向渲染有可能会有多个Pass,取决于场景中的光源),它的两个Pass分工非常明确:(不再写出伪代码了)
- 第一个Pass——不进行任何的光照计算,仅计算当前可见的片元,片元通过了深度测试?那就把这个片元的信息储存在G缓冲中,这就是第一个Pass干的事情
- 第二个Pass——利用G-buffer中的信息进行真正的光照计算,将计算得到的结果写入帧缓冲中
我们会发现,除了前向渲染的颜色缓冲和深度缓冲,延迟渲染还多了一个G缓冲,也叫G-buffer,这个G是Geometry的缩写,G缓冲中储存了各种计算光照需要的表面信息。此外,还会发现!这么说来,延迟渲染的Pass数量就只有2个,也就是说它与场景中的光源数目无关,而与屏幕大小有关。这不难理解吧?屏幕空间越大,当前帧包括在camera视角中的物体不就多了,物体多了G-buffer需要纳入的表面信息也就多了。
3.2 Unity中如何实现延迟渲染
我们知道第二个Pass纯粹用于计算光照,可以叫做lightPass,这个Pass在Unity中是使用内置的、默认的standard光照模型。
可以按照路径project settings->Graphics->Built-in Shader Settings里进行自定义。
《入门精要》其实也是小篇幅的介绍延迟渲染。我就简单的写一些第一个Pass的G-buffer有哪几个渲染纹理(Render Texture, RT)
- RT0——ARGB32,储存漫反射颜色,A通道不使用
- RT1——ARGB32,储存高光反射颜色,A通道储存高光反射的指数部分,就是那个_GLOSS
- RT2——ARGB2101010,储存法线,A通道不适用
- RT3——储存自发光+lightmap+反射探针(reflection probes)
- 深度缓冲和模板缓冲
4 两种渲染路径的优劣
之前的叙述中,或多或少的提到了前向渲染和延迟渲染之间的对比,百人计划里老师也列出了二者的优点与缺点:
我们一条一条说说:
4.1 前向渲染的优点和缺点
关于优点,
- 支持半透明渲染——之前做的半透明效果的透明度测试和透明度混合都是前向渲染实现的
- 支持多个光照Pass——这里的多个我觉得是指对于不同的光源(平行光、点光源等)可以分别进行处理吧,毕竟大多数时候还是有两个Pass(双面渲染需要两个Base Pass除外)
- 支持自定义光照计算方式
关于缺点,
- 计算复杂度很大——相比延迟渲染,只要影响到物体的光源它都给计算了,这计算量能不大嘛
- 访问深度需要额外的计算——这是因为不同于延迟渲染,直接将信息储存在了一个G-buffer中,只要不clear,每次需要用的时候直接调用即可;前向渲染需要每次用到深度信息的时候都需要额外计算
4.2 延迟渲染的优点和缺点
关于优点,
- 大量光照场景优势明显——当场景中超级多光照的时候,延迟渲染肯定会比前向渲染更节省性能,需要的计算量也大大降低
- 节省计算量——其实跟上面一条的道理一样,只渲染当前屏幕空间大小内的可见的片元
- 对后处理支持良好——这一点还是得益于G-buffer,需要深度信息的后处理操作,直接访问就行!不需要再进行计算,那么关于屏幕后处理后面会再进行学习
关于缺点,
- 占用大量显存宽带——由于需要G-buffer,深度信息一直存在一张图里,那么需要的显存也更多了
- 对MSAA支持不友好——其实就是延迟渲染不支持真正的抗锯齿功能
- 不能渲染半透明物体——所以在延迟渲染中如果遇到半透明材质的物体,会选择最后前向渲染给它实现出来