《Unity Shader入門精要》總結 #第九章 更復雜的光照
1、Unity的渲染路徑
主要有3種:前向渲染路徑、延遲渲染路徑、頂點照明渲染路徑
攝像機單獨設定,若選擇User Player Settings,則攝像機會使用Project Settings的設定;否則覆蓋掉Project Settings的設定
,若當前顯示卡並不支援所選擇的路徑,則Unity會使用更低一級的渲染路徑。
標籤名 | 描述 |
Always | 不管使用哪種渲染路徑,該Pass總是會被渲染,而不計算任何光照 |
ForwardBase | 用於前向渲染,該Pass會計算環境光、最重要的平行光、逐頂點/SH光源和Lightmaps |
ForwardAdd | 用於前向渲染,該Pass會計算額外的逐項素光源,每個Pass對應一個光源 |
Deferred | 用於延遲渲染,該Pass會渲染G緩衝(G-buffer) |
ShadowCaster | 把物體的深度資訊渲染到陰影對映紋理(shadowmap)或一張深度紋理中 |
PrepassBase | 用於遺留的延遲渲染,該Pass會渲染法線和高光反射的指數部分 |
PrepassFinal | 用於遺留的延遲渲染,該Pass通過合併紋理、光照和自發光來渲染得到的最後的顏色 |
Vertex、VertexLMRGBM和VertexLM | 用於遺留的頂點照明渲染 |
1.1 前向渲染路徑
前向渲染的原理
每進行一次完整的前向渲染,我們需要渲染該物件的渲染圖元並計算兩個緩衝區的資訊:顏色緩衝區和深度緩衝區
虛擬碼:
Pass{ for(each primitive in this model) for(each fragment covered by this primitive){ if(failed in depth test){ discard; }else{ fixed4 color = Shading(materialInfo, pos, normal, lightDir, viewDir); writeFrameBuffer(fragment, color); } } }
對於每個逐畫素光源就需要進行上面一次完整的渲染流程。如果一個物體在多個逐畫素光源的影響區域內,該物體需要執行多個Pass,每個Pass計算一個逐畫素光源的光照結果,然後在幀緩衝中把這些光照結果混合起來得到最終顏色值。N個物體M個光源,則需要N*M個Pass,如果有大量逐畫素光照,則計算量大,通常會限制每個物體的逐項素光源數目。
Unity中的前向渲染
3種處理光照方式:逐頂點處理、逐畫素處理、球諧函式處理。決定光源使用哪種處理模式取決於其型別和渲染模式。光源型別指的是該光源是平行光還是其他型別的光源,而光源的渲染模式指的是該光源是否是重要的。在Light元件中設定這些屬性。一定數目的光源按照逐畫素處理,至多4個光源按照逐頂點處理,其餘的按照球諧函式處理
有如下規則:
-場景中最亮的平行光總是按照畫素處理
-渲染模式被設定成Not Important的光源按逐頂點/SH處理
-設定成Important的光源按照逐畫素處理
-如果以上規則得到的逐畫素光源數量小於Quality Setting中的逐畫素光源數量,則會有更多的光源以逐畫素的方式進行渲染
Base Pass和Additional Pass的區別
可實現的光照效果 | 渲染設定 | 光照計算 | |
Base Pass | 光照紋理、環境光、自然光、陰影(平行光的陰影) |
Tags{"LightMode" = "ForwardBase"} #pragma multi_compile_fwdbase |
一個逐畫素的平行光 所有逐頂點和SH光源 |
Additional Pass |
預設情況不支援陰影 (可通過#pragma multi_compile_fwdadd_fullshadows開啟) |
Tags{"LightMode" = "ForwardAdd"} Blend One One #pragma multi_compile_fwdadd |
其他影響該物體的逐畫素光源 每個光源執行一次Pass |
-Base Pass中支援一些光照特性,可以訪問lightmap
-Base Pass中渲染的平行光預設支援陰影(若開啟了光源的陰影功能),而Additional Pass中渲染的光源預設情況下沒有,但可以通過編譯指令開啟點光源和聚光燈陰影效果
-環境光與自發光在Base Pass中計算,若在Addtional Pass計算則會疊加多次環境光和自發光
-在Additional Pass的渲染設定中開啟和設定了混合模式,我們希望每個Additional Pass可以與上一次光照結果在幀快取中進行疊加,從而得到最終的有多個光照的渲染效果。若沒有開啟和設定混合模式,則渲染結果會覆蓋掉之前的渲染結果,看起來只受該光源影響。通常選擇Blend One One混合模式
-通常定義Base Pass以及一個Additional Pass,Base Pass僅執行一次,而Additional Pass會根據影響該物體的其他逐畫素光源的數目被多次呼叫,即每個逐畫素光源會執行一次Additional Pass
內建的光照變數和函式
前向渲染可以使用的內建光照變數
名稱 | 型別 | 描述 |
_LightColor0 | float4 | 該Pass處理的逐畫素光源的顏色 |
_WorldSpaceLightPos0 | float4 | _WorldSpaceLightPos0.xyz是該Pass的逐畫素光源的位置,若該光源是平行光,則_WorldSpaceLightPos0.w是0,其他光源型別w值為1 |
_LightMatrix0 | float4*4 | 世界空間到光源空間的變換矩陣,可用於取樣cookie和光強衰減紋理 |
unity_4LightPosX0, unity_4LightPosY0, unity_4LightPosZ0 | float4 | 僅用於Base Pass,前4個非重要的點光源在世界空間中的位置 |
unity_3LightAtten0 | flaot4 | 僅用於Base Pass,儲存了前四個非重要的點光源的衰減因子 |
unity_LightColor | half4[4] | 僅用於Base Pass,儲存了前四個非重要的點光源的顏色 |
僅用於前向渲染路徑的函式:WorldSpaceLightDir,UnityWorldSpaceLightDir,ObjSpaceLightDir
僅用於前向渲染的函式:
函式名 | 描述 |
float3 WorldSpaceLightDir(float4 v) | 輸入模型空間頂點位置,返回世界空間從該點到光源的光照方向。未被歸一化 |
float3 UnityWorldSpaceLightDir(float4 v) | 輸入世界空間頂點位置,返回世界空間從該點到光源的光照方向。未被歸一化 |
float3 ObjSpaceLightDir(float4 v) | 輸入模型空間頂點位置,返回模型空間從該點到光源的光照方向。未被歸一化 |
float3 Shade4PointLights(....) | 計算四個點光源的光照,其引數是已經打包進向量的光照資料,通常是上表的內建變數。前向渲染使用這個函式計算逐頂點光照 |
1.2 頂點照明渲染路徑
配置要求最少、運算效能最高、效果最差
如果選擇使用頂點照明渲染路徑,則Unity會只填充逐頂點相關的光源變數,意味著不可以使用一些逐畫素光照變數
Unity中的頂點照明渲染
計算所有光源對該物體的照明,按逐頂點處理
可訪問的內建變數和函式
名稱 | 型別 | 描述 |
unity_LightColor | half4[8] | 光源顏色 |
unity_LightPosition | float4[8] | xyz是視角空間中的光源位置,若是平行光,則w為0,其他光源為1 |
unity_LightAtten | half4[8] | 光源衰減因子,聚光燈則x分量為cos(spotAngle/2),y分量為1/cos(spotAngle/4),若是其他型別光源x=-1,y=1,z是衰減的平方,w是光源範圍開根號的結果 |
unity_SpotDirection | float4[8] | 若光源為聚光等的話,值為視角空間的聚光燈位置,若是其他型別則(0,0,1,0) |
內建函式
函式名 | 描述 |
float3 ShaderVertexLights(float4 vertex, float3 normal) | 輸入模型空間中的頂點位置和法線,計算四個逐頂點光源的光照以及環境光。內部實現實際上呼叫了ShaderVertexLightsFull函式 |
float3 ShaderVertexLightsFull(float4 vertex, float3 normal, int lightCount, bool spotLight) | 輸入模型空間中的頂點位置和法線,計算lightCount個光源的光照以及環境光,如果spotLight為true,則光源將被當成聚光燈處理,雖然結果更精確但更耗時,否則按點光源處理 |
1.3 延遲渲染路徑
除了前向渲染中使用的顏色緩衝和深度緩衝外,延遲渲染還會利用額外的緩衝區統稱為G緩衝(儲存了例如法線、位置、用於光照計算的材質屬性等)。
延遲渲染原理
第一個Pass計算哪些片元是可見的,通過深度緩衝技術實現,當發現一個片元課件就將其相關資訊儲存到G緩衝區中;
第二個Pass中利用G緩衝區的各個片元資訊,如表面法線、視角方向、漫反射係數等進行真正的光照計算
Pass 1{
//光照計算需要運用的資訊儲存到G緩衝中
for(each primitive in this model){
for(each fragment covered by this primitive){
if(failed in depth test){
//未通過深度測試,片元不可見
discard;
}else{
//通過,將需要的資訊儲存於G緩衝中
writeGBuffer(materialInfo, pos, normal, lightDir, viewDir);
}
}
}
}
Pass 2{
for(each pixel in the screen){
if(the pixel is valid){
//若該畫素有效,讀取對應G緩衝資訊
readGBuffer(pixel, materialInfo, pos, normal, lightDir, viewDir);
}
//讀取到的資訊進行光照計算
float4 color = Shading(materialInfo, pos, normal, lightDir, viewDir);
//更新幀緩衝
writeFrameBuffer(pixel, color);
}
}
(1)第一個Pass用於渲染G緩衝,在這個Pass中將物體的漫反射顏色、高光反射顏色、平滑度、法線、自發光和深度等資訊渲染到螢幕空間的螢幕空間的G緩衝區中。此Pass只執行一次
(2)第二個Pass用於計算真正的光照模型,此Pass使用上一個Pass中渲染的資料來計算最終的光照顏色,再儲存於幀緩衝中
延遲渲染的效率不依賴於場景的複雜度,而是和我們使用的螢幕空間的大小有關,因為資訊都在緩衝區中,可理解成是2D影象
Unity中的渲染延遲
適合使用在場景中光源數目很多,如果使用前向渲染會造成效能瓶頸的情況下使用。缺點:
-不支援anti-aliasing
-不能處理半透明物體
-對顯示卡有一定要求
預設的G緩衝區包含以下渲染紋理:
格式 | RGB通道儲存 | A通道儲存 | |
RT0 | ARGB32 | 漫反射顏色 | 未使用 |
RT1 | ARGB32 | 高光反射顏色 | 高光反射指數部分 |
RT2 | ARGB2101010 | 法線 | 未使用 |
RT3 | ARGB32(非HDR)/ARGBHalf(HDR) | 自發光+lightmap+反射探針 |
深度緩衝和模板緩衝
第二個Pass中計算光照時預設情況下僅可使用Unity內建的Standard光照模型,若想使用其他的就需替換掉原有的Internal-DeferredShading.shader檔案
可訪問的內建變數和函式
名稱 | 型別 | 描述 |
_LightColor | float4 | 光源顏色 |
_LightMatrix0 | float4*4 | 從世界空間到光源空間的變換矩陣,可用於取樣cookie和光強衰減紋理 |
2、Unity的光源型別
2.1 光源型別有何影響
最常使用的光源屬性有光源的位置、方向、顏色、強度以及衰減5個屬性
平行光
其幾何屬性只有方向,到每一點方向一致,除此之外平行光無具體位置以及衰減的概念
點光源
球體半徑;點光源位置;點光源方向(點光源位置-某點位置),隨著物體逐漸遠離點光源其接收到的光照強度會逐漸減小
球心處光照強度最強,向外遞減到0
聚光燈
錐形區域的半徑由Range定義;張開角度由Spot Angle決定;聚光燈位置;方向屬性(聚光燈位置-某點位置);衰減,需要判定是否在椎體範圍內
2.2 前向渲染中處理不同的光源型別
// Upgrade NOTE: replaced '_LightMatrix0' with 'unity_WorldToLight'
// Upgrade NOTE: replaced '_LightMatrix0' with 'unity_WorldToLight'
Shader "Custom/Chapter9_ForwardRendering" {
Properties {
_Diffuse("Diffuse", Color) = (1,1,1,1)
_Gloss("Gloss", Range(8.0, 256)) = 20
_Specular("Specular", Color) = (1,1,1,1)
}
SubShader {
Pass{
Tags{"LightMode" = "ForwardBase"}
CGPROGRAM
#pragma multi_compile_fwdbase
#pragma vertex vert
#pragma fragment frag
#include "Lighting.cginc"
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 = UnityObjectToWorldDir(v.vertex);
return o;
}
fixed4 frag(v2f i) : SV_Target{
fixed3 worldNormal = normalize(i.worldNormal);
fixed3 worldPos = normalize(i.worldPos);
fixed3 worldLightDir = normalize(UnityWorldSpaceLightDir(i.worldPos));
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;//環境光計算一次即可,自發光也計算一次即可
// fixed3 diffuse = _Diffuse.rgb * _LightColor0.rgb * max(0, dot(worldNormal, worldLightDir));
fixed3 diffuse = _Diffuse.rgb * _LightColor0.rgb * saturate(dot(worldNormal, worldLightDir));
fixed3 viewDir = normalize(UnityWorldSpaceViewDir(i.worldPos));
fixed3 halfDir = normalize(viewDir + worldLightDir);
fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(max(0, dot(worldNormal, halfDir)), _Gloss);
fixed atten = 1.0;
return fixed4(ambient + (diffuse + specular) * atten, 1.0);
}
ENDCG
}
Pass{
Tags{"LightMode" = "ForwardAdd"}
Blend One One
CGPROGRAM
#pragma multi_compile_fwdadd
#pragma vertex vert
#pragma fragment frag
#include "Lighting.cginc"
#include "AutoLight.cginc"
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 = UnityObjectToWorldDir(v.vertex);
return o;
}
fixed4 frag(v2f i) : SV_Target{
fixed3 worldNormal = normalize(i.worldNormal);
fixed3 worldPos = normalize(i.worldPos);
#ifdef USING_DIRECTIONAL_LIGHT
// fixed3 worldLightDir = normalize(UnityWorldSpaceLightDir(i.worldPos));
fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz);
#else
// fixed3 worldLightDir = normalize(UnityWorldSpaceLightDir(i.worldPos).xyz - i.worldPos.xyz);
fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz - i.worldPos.xyz);
#endif
// fixed3 diffuse = _Diffuse.rgb * _LightColor0.rgb * max(0, dot(worldNormal, worldLightDir));
fixed3 diffuse = _Diffuse.rgb * _LightColor0.rgb * saturate(dot(worldNormal, worldLightDir));
fixed3 viewDir = normalize(UnityWorldSpaceViewDir(i.worldPos));
fixed3 halfDir = normalize(viewDir + worldLightDir);
fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(max(0, dot(worldNormal, halfDir)), _Gloss);
#ifdef USING_DIRECTIONAL_LIGHT
fixed atten = 1.0;
#else
#if defined(POINT)
float3 lightCoord = mul(unity_WorldToLight, float4(i.worldPos, 1.0)).xyz;
fixed atten = tex2D(_LightTexture0, dot(lightCoord, lightCoord).rr).UNITY_ATTEN_CHANNEL;
#elif defined(SPOT)
float4 lightCoord = mul(unity_WorldToLight, float4(i.worldPos, 1.0));
fixed atten = (lightCoord.z > 0) * tex2D(_LightTexture0, lightCoord.xy/lightCoord.w + 0.5).w * tex2D(_LightTextureB0, dot(lightCoord, lightCoord).rr).UNITY_ATTEN_CHANNEL;
#else
fixed atten = 1.0;
#endif
#endif
return fixed4((diffuse + specular) * atten, 1.0);
}
ENDCG
}
}
FallBack "Diffuse"
}
第一個Pass:
(1)使用#pragma multi_compile_fwdbase使得在Shader中使用光照衰減等光照變數可以被正確賦值
(2)Base Pass中計算一次環境光,在Additional Pass中不必再計算(物體自發光也只計算一次即可)
(3)Base Pass中處理場景中最重要的平行光。Unity將選擇最亮的平行光傳遞給Base Pass進行逐畫素處理,其他平行光將按照逐頂點或在Additional Pass中按逐畫素方式處理。若沒有任何平行光則Base Pass將當成全黑光源處理,對於Base Pass其處理的逐畫素光源一定是平行光。可以使用_WorldSpaceLightPos0得到這個平行光的方向,_LightColor0是顏色和強度相乘的結果,衰減值無=1.0
第二個Pass:
(1)#pragma multi_compile_fwdadd指令可以保證我們在Additional Pass中訪問到正確的光照變數。還是用Blend開啟設定混合模式,希望Additional Pass計算得到的光照結果可以在幀快取終於之前的光照結果進行疊加,若沒有Blend命令,Additional Pass將直接覆蓋掉之前的光照結果。
(2)修改針對去掉Base Pass中環境光、自發光、逐頂點光照和SH光照的部分,並新增一些對不同光源型別的支援。
平行光的方向可由_WorldSpaceLightPos0.xyz得到,而點、聚光燈_WorldSpaceLightPos0.xyz表示世界空間下的光源位置,想要得到方向就必須用這個位置減去世界空間下的頂點位置
(3)對於不同光源型別衰減值,Unity選擇了使用一張紋理作為查詢表LUT,以在片元著色器中得到光源的衰減。先得到光源空間下的座標,然後該座標對衰減紋理進行取樣得到衰減值
3、Unity的光照衰減
3.1 用於光照衰減的紋理
若對該光源使用cookie,則衰減查詢紋理是_LightTextureB0。(0,0)表明與光源位置重合的點的衰減值,(1,1)表明在光源空間中所關心的距離最遠的點的衰減。
_LightMatrix0是世界空間變換到光源空間的變換矩陣
float3 lightCoord = mul(_LightMatrix0, float4(i.worldPosition, 1)).xyz;
然後利用光源空間中頂點距離的平方對紋理取樣,使用巨集UNITY_ATTEN_CHANNEL得到衰減紋理中衰減值所在的分量
fixed atten = tex2D(_LightTexture0, dot(lightCoord, lightCoord).rr).UNITY_ATTEN_CHANNEL;
3.2 使用數學公式計算衰減
4、Unity陰影
4.1 陰影是如何實現的
shadow map技術,將攝像機放在光源處與光源重合,有陰影的就是攝像機照不到的地方。
Unity需要一個額外的Pass專門更新光源的陰影對映紋理,此Pass就是LightMode = “ShadowCaster”的Pass。開啟光源陰影效果後,底層引擎首先在LightMode中查詢為ShadowCaster的Pass,若沒有就在Fallback指定的UnityShader中繼續尋找,若還沒有就無法向其他物體投射陰影(但仍可接收)。
Unity中使用了螢幕空間的陰影對映技術。需要顯示卡支援MRT。
表面深度>轉換到陰影紋理中的深度值,即說明該表面雖然可見但出於該光源陰影中。
陰影圖是螢幕空間:將該表面座標從模型空間變換到螢幕空間中
step1. 想要接收投影,就需要在Shader中對陰影對映紋理進行取樣,將取樣結果和最後的光照結果相乘產生陰影效果
step2. 要物體向其他物體投射陰影,將該物體加入到光源的陰影對映紋理計算中,從而讓其他物體在對陰影對映紋理取樣時可以得到該物體的相關資訊。該過程通過ShadowCaster實現。
4.2 不透明物體的陰影
SHADOW_COORDS/TRANSFER_SHADOW/SHADOW_ATTENUATION三劍客:
SHADOW_COORDS聲明瞭一個名為——ShadowCoord的陰影紋理座標變數,TRANSFER_SHADOW根據平臺不同有所差異。為了使巨集可以進行相關計算,必須使a2f頂點座標變數名為vertex,v2f必須名為v,v2f頂點位置變數必須命名為pos