学习参考
【技术美术百人计划】图形 3.4 延迟渲染管线介绍
《Unity Shader 入门精要》
关于渲染路径,我在图形渲染管线1.0中就提过了,但只是初步的了解了渲染路径有前向渲染、延迟渲染、Forward+等等,也了解到了前向渲染做了很多无效渲染,难以支持过多光源;延迟渲染可以支持大量的实时光照,基于缓存,因此对空间的要求也比前向渲染高。
这一篇博客我们再在Unity中了解一下这两种渲染路径里面的门道,同时自己上手看看前向渲染和延迟渲染在Unity中的对比效果。
Unity中可以给当前项目设置一个总体的渲染路径,如Project Settings -> Graphics:
也可以给不同的摄像机设置不同的渲染路径,这个就在每个摄像机的Inspector窗口设置,可以选择与Graphics Settings同步,也可以选择Forward/Deferred等等:
还需要知道,Camera的Rendering Path设置是优先于整个项目的设置的。
在之前的实践中已经尝试过为每个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 | 用于遗留的顶点照明渲染 |
前向渲染是最常用的一种渲染路径,原理也通俗易懂:首先利用深度缓存决定当前渲染对象的每个图元的每个片元是否可见,如果可见再计算光照结果并储存进帧缓存中,如下的伪代码展示了前向渲染的大概流程:
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);
}
}
}
}
我们讨论过,如果一个物体在多个光源的影响范围内,那肯定要执行很多次Pass,每个Pass都计算一个光照结果,最后混合所有的结果得到最终的颜色值。大量的逐像素光照会带来大量的Pass,为了节省开销,引擎通常会限制每个物体的逐像素光照数目。
Unity中,Project Settings -> Quality -> Pixel Light Count对逐像素光照数目做了限制,默认为4,意味着一个物体可以接受除了场景中最亮的平行光外的4个逐像素光照。
渲染一个物体时,Unity会计算哪些光源照亮了它,以及计算这些光源照亮该物体的方式。处理光源照亮物体的方式有3种:逐顶点处理、逐像素处理、球谐函数(Spherical Harmonics, SH)处理。
我们已经知道了有三种处理光照的方式,下一步肯定就是考虑Unity是通过什么判断?答案是——通过设置光源的渲染模式(Render Mode)和类型(Type)来实现的,二者都可以在光源Light的Inspector窗口设置:
那么,Unity是如何判断的?前向渲染中,Unity会根据场景中光源的设置和光源对物体的影响程度对光源做一个重要度排序,影响程度包括:光源距离物体的远近、光源强度等。距离好说,当其他参数都相同时,越近当然越重要;但对于距离、强度和光颜色等到底是如何考虑得到排序的我们并不知道(Unity文档也未告知),仅仅知道这个重要度排序跟这么多参数都有关。
通常Unity会使用以下判断规则:
光照计算都是在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次 |
需要补充的是,
直接盘点一下前向渲染的内置光照变量:
名称 | 类型 | 描述 |
_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个点光源的光照 |
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 light
Pass {
Tags {"LightMode"="ForwardBase"}
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "Lighting.cginc"
//need to add this declaration
#pragma multi_compile_fwdbase
//properties
fixed4 _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 light
fixed atten = 1.0;
return fixed4 (ambient + (diffuse + specular) * atten, 1.0);
}
ENDCG
}
Pass {
Tags {"LightMode"="ForwardAdd"}
Blend One One
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "Lighting.cginc"
#include "Autolight.cginc"
#pragma multi_compile_fwdadd
//properties
fixed4 _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_LIGHT
fixed3 worldLightDir = normalize(UnityWorldSpaceLightDir(i.worldPos));
#else //is pointlight
fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz - i.worldPos.xyz);
#endif
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 light
#ifdef USING_DIRECTIONAL_LIGHT
fixed 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.sample
fixed atten = tex2D(_LightTexture0, dot(lightCoord, lightCoord).rr).UNITY_ATTEN_CHANNEL;
#endif
return fixed4 (ambient + (diffuse + specular) * atten, 1.0);
}
ENDCG
}
}
FallBack "Specular"
}
效果如下:
我们知道前向渲染是每个光源对物体的影响都计算出来,但随着场景中光源数量增多,需要计算的三角面和顶点数量也会增多,那前向渲染的性能会急速下降!这个时候延迟渲染就体现出它的优点了。
延迟渲染是一种比较古老的方法,它分为两个Pass,但和前向渲染的截然不同(前向渲染有可能会有多个Pass,取决于场景中的光源),它的两个Pass分工非常明确:(不再写出伪代码了)
我们会发现,除了前向渲染的颜色缓冲和深度缓冲,延迟渲染还多了一个G缓冲,也叫G-buffer,这个G是Geometry的缩写,G缓冲中储存了各种计算光照需要的表面信息。此外,还会发现!这么说来,延迟渲染的Pass数量就只有2个,也就是说它与场景中的光源数目无关,而与屏幕大小有关。这不难理解吧?屏幕空间越大,当前帧包括在camera视角中的物体不就多了,物体多了G-buffer需要纳入的表面信息也就多了。
我们知道第二个Pass纯粹用于计算光照,可以叫做lightPass,这个Pass在Unity中是使用内置的、默认的standard光照模型。
可以按照路径project settings->Graphics->Built-in Shader Settings里进行自定义。
《入门精要》其实也是小篇幅的介绍延迟渲染。我就简单的写一些第一个Pass的G-buffer有哪几个渲染纹理(Render Texture, RT)
之前的叙述中,或多或少的提到了前向渲染和延迟渲染之间的对比,百人计划里老师也列出了二者的优点与缺点:
我们一条一条说说:
关于优点,
关于缺点,
关于优点,
关于缺点,