前向渲染和延遲渲染
前向渲染和延遲渲染總的來說是我們的兩種主要的渲染方式。
我們在Unity的Project Settings中的Graphic界面能夠找到渲染隊列的設定:
我們也可以在Main Camera這里進行設置:
那這里我們首先介紹一下兩種渲染(Forward Renderring\Deferred Renderring)的基本概念:
渲染就像去餐館吃飯,每一桌就是一個需要渲染的對象,我們在吃飯前要先指指點點(渲染的設置),前向渲染就是比較樸素的餐館運行模式:針對每一桌客人,我們都執行:客人下單,上菜,執行完一桌之后我們才去咨詢下一桌,這樣的話問題:作為餐館的服務人員,我們來回跑了太多次(調用GPU進行渲染的指令),且這樣計算的總開銷時間較長(下一桌客人一定要等到上一桌客人的菜上完才能點菜);延遲渲染就是我們先總的收集所有客人的點餐情況,然后根據總的要求進行上菜,這樣最大的改善就是我們大大減少了咨詢客人點菜情況的時間,且減小了整體的計算時長。
兩種渲染方式在多光源場景下的性能差異尤為明顯:前向渲染的過程中我們要分別每個渲染對象單獨地計算完光照效果之后再計算下一個對象,但是對于延遲渲染來說,整個場景的所有物體只用計算一次光照,本身光照計算就幾乎是開銷最大的部分,這樣可以相當大一部分節省性能。
但是顯然延遲渲染并不是十全十美的,你能統計全餐館的下單情況的前提是你得有足夠大的一個記事本,在計算機里這個記事本叫做:G-BUFFER,我們會把各個對象的諸多信息放入這個緩沖區,然后后續再在緩沖區中統一計算光照。因此,從這個角度來說,我們可以認為延遲渲染是前向渲染的以空間換時間的一種方式。
我們現在展開來說:
通俗地說,針對場景里的多光源,前向渲染會給光源分為三個優先級:最亮的一檔就會采取逐像素渲染,而最不重要的一檔我們會直接使用球諧函數來生成一個結果避免復雜運算,中間的一檔(逐頂點渲染)則不會超過四個。其中每個光源并不是一定處以某個優先級,而是一個兩種優先級的混合疊加態。
上述說的光源的優先級和限制的逐像素渲染光源數都是可以修改的:
這是前向渲染處理多光源的方法,那么對于延遲渲染來說呢?
延遲渲染是不支持抗鋸齒的,這是一個非常致命的問題,同時還不支持半透明:
?
因此,根據不同的場景來選擇不同的渲染策略才是最重要的。?
陰影實現
還是那句話,現在并沒有完美的生成陰影的算法,大多數都是有些缺陷的。
我們直接上代碼來介紹吧:
Shader "Chapter3/chapter3_3_shadow"
{Properties{// 定義主顏色屬性,可在材質面板中調整_MainColor ("Main Color", Color) = (1, 1, 1, 1)}SubShader{// -------- 基礎Pass:處理主要光源的投影 --------Pass{// 標簽定義,用于指定此Pass的光照模式為"ForwardBase"Tags{"LightMode" = "ForwardBase"}CGPROGRAM// 頂點著色器和片段著色器入口#pragma vertex vert#pragma fragment frag#pragma multi_compile_fwdbase // 多重編譯,支持不同光照模型#include "UnityCG.cginc" // 包含Unity常用工具函數和宏#include "Lighting.cginc" // 包含Unity光照計算函數#include "AutoLight.cginc" // 包含Unity自動化光照相關代碼// 定義頂點到片段的數據結構struct v2f{float4 pos : SV_POSITION; // 裁剪空間的頂點位置float3 normal : TEXCOORD0; // 法線,用于光照計算float4 vertex : TEXCOORD1; // 模型空間的頂點位置SHADOW_COORDS(2) // 使用Unity預定義宏存儲陰影坐標};fixed4 _MainColor; // 主顏色變量// 頂點著色器v2f vert (appdata_base v){v2f o;o.pos = UnityObjectToClipPos(v.vertex); // 轉換頂點到裁剪空間o.normal = v.normal; // 傳遞法線信息o.vertex = v.vertex; // 傳遞頂點位置TRANSFER_SHADOW(o) // 計算陰影坐標并存儲return o;}// 片段著色器fixed4 frag (v2f i) : SV_Target{// 計算法線方向float3 n = UnityObjectToWorldNormal(i.normal);n = normalize(n);// 計算世界空間中的光源方向float3 l = WorldSpaceLightDir(i.vertex);l = normalize(l);// 將頂點從模型空間轉換到世界空間float4 worldPos = mul(unity_ObjectToWorld, i.vertex);// Lambert光照模型:計算法線與光線夾角的點積fixed ndotl = saturate(dot(n, l));fixed4 color = _LightColor0 * _MainColor * ndotl;// 疊加4個點光源的光照color.rgb += Shade4PointLights(unity_4LightPosX0, unity_4LightPosY0, unity_4LightPosZ0, // 點光源位置unity_LightColor[0].rgb, unity_LightColor[1].rgb, unity_LightColor[2].rgb, unity_LightColor[3].rgb, // 點光源顏色unity_4LightAtten0, worldPos.rgb, n // 衰減和位置) * _MainColor;// 疊加環境光照color += unity_AmbientSky;// 使用Unity宏計算陰影衰減系數UNITY_LIGHT_ATTENUATION(shadowmask, i, worldPos.rgb)// 將陰影系數與顏色相乘,應用陰影效果color.rgb *= shadowmask;return color; // 返回最終顏色}ENDCG}// -------- 額外的Pass:處理其他逐像素燈光的投影 --------Pass{// 標簽定義,此Pass用于附加光源,模式為"ForwardAdd"Tags{"LightMode" = "ForwardAdd"}// 混合模式:相加混合Blend One OneCGPROGRAM#pragma vertex vert#pragma fragment frag#pragma multi_compile_fwdadd_fullshadows // 支持多重編譯,包含完整陰影#include "UnityCG.cginc"#include "Lighting.cginc"#include "AutoLight.cginc"// 定義頂點到片段的數據結構,與基礎Pass一致struct v2f{float4 pos : SV_POSITION;float3 normal : TEXCOORD0;float4 vertex : TEXCOORD1;SHADOW_COORDS(2)};fixed4 _MainColor;// 頂點著色器v2f vert (appdata_base v){v2f o;o.pos = UnityObjectToClipPos(v.vertex);o.normal = v.normal;o.vertex = v.vertex;TRANSFER_SHADOW(o)return o;}// 片段著色器fixed4 frag (v2f i) : SV_Target{// 計算法線和光照方向float3 n = UnityObjectToWorldNormal(i.normal);n = normalize(n);float3 l = WorldSpaceLightDir(i.vertex);l = normalize(l);// 轉換頂點到世界空間float4 worldPos = mul(unity_ObjectToWorld, i.vertex);// Lambert光照計算fixed ndotl = saturate(dot(n, l));fixed4 color = _LightColor0 * _MainColor * ndotl;// 疊加點光源的光照color.rgb += Shade4PointLights(unity_4LightPosX0, unity_4LightPosY0, unity_4LightPosZ0,unity_LightColor[0].rgb, unity_LightColor[1].rgb,unity_LightColor[2].rgb, unity_LightColor[3].rgb,unity_4LightAtten0, worldPos.rgb, n) * _MainColor;// 使用陰影宏計算陰影系數UNITY_LIGHT_ATTENUATION(shadowmask, i, worldPos.rgb)// 應用陰影到顏色color.rgb *= shadowmask;return color; // 返回最終顏色}ENDCG}}// 回退著色器,定義為DiffuseFallBack "Diffuse"
}
我想我需要首先介紹兩個Tags中的LightMode:ForwardBase和ForwardAdd。
在代碼中我們用:
// 混合模式:相加混合Blend One One
的混合模式把這兩種前向渲染的結果進行結合就可以得到一個完整的生成陰影的方法。
現在我們來看具體代碼:
// -------- 基礎Pass:處理主要光源的投影 --------Pass{// 標簽定義,用于指定此Pass的光照模式為"ForwardBase"Tags{"LightMode" = "ForwardBase"}CGPROGRAM// 頂點著色器和片段著色器入口#pragma vertex vert#pragma fragment frag#pragma multi_compile_fwdbase // 多重編譯,支持不同光照模型#include "UnityCG.cginc" // 包含Unity常用工具函數和宏#include "Lighting.cginc" // 包含Unity光照計算函數#include "AutoLight.cginc" // 包含Unity自動化光照相關代碼// 定義頂點到片段的數據結構struct v2f{float4 pos : SV_POSITION; // 裁剪空間的頂點位置float3 normal : TEXCOORD0; // 法線,用于光照計算float4 vertex : TEXCOORD1; // 模型空間的頂點位置SHADOW_COORDS(2) // 使用Unity預定義宏存儲陰影坐標};fixed4 _MainColor; // 主顏色變量// 頂點著色器v2f vert (appdata_base v){v2f o;o.pos = UnityObjectToClipPos(v.vertex); // 轉換頂點到裁剪空間o.normal = v.normal; // 傳遞法線信息o.vertex = v.vertex; // 傳遞頂點位置TRANSFER_SHADOW(o) // 計算陰影坐標并存儲return o;}// 片段著色器fixed4 frag (v2f i) : SV_Target{// 計算法線方向float3 n = UnityObjectToWorldNormal(i.normal);n = normalize(n);// 計算世界空間中的光源方向float3 l = WorldSpaceLightDir(i.vertex);l = normalize(l);// 將頂點從模型空間轉換到世界空間float4 worldPos = mul(unity_ObjectToWorld, i.vertex);// Lambert光照模型:計算法線與光線夾角的點積fixed ndotl = saturate(dot(n, l));fixed4 color = _LightColor0 * _MainColor * ndotl;// 疊加4個點光源的光照color.rgb += Shade4PointLights(unity_4LightPosX0, unity_4LightPosY0, unity_4LightPosZ0, // 點光源位置unity_LightColor[0].rgb, unity_LightColor[1].rgb, unity_LightColor[2].rgb, unity_LightColor[3].rgb, // 點光源顏色unity_4LightAtten0, worldPos.rgb, n // 衰減和位置) * _MainColor;// 疊加環境光照color += unity_AmbientSky;// 使用Unity宏計算陰影衰減系數UNITY_LIGHT_ATTENUATION(shadowmask, i, worldPos.rgb)// 將陰影系數與顏色相乘,應用陰影效果color.rgb *= shadowmask;return color; // 返回最終顏色}ENDCG}
我們來說新東西:
#pragma multi_compile_fwdbase // 多重編譯,支持不同光照模型
這句指令就是允許我們的前向渲染根據場景中的光照動態地調整一些涉及著色器參數的關鍵字選擇。
ForwardBase中主要分成三個部分:v2f,vert和frag。
v2f中我們可以看到熟悉的頂點坐標(裁剪空間和模型空間),法線和一個陰影的預定義宏,其中:
float4 vertex : TEXCOORD1; // 模型空間的頂點位置SHADOW_COORDS(2) // 使用Unity預定義宏存儲陰影坐標
我們法線模型空間的頂點位置居然使用了TEXCOORD1來存儲,這不是紋理坐標的語義嗎?是的這確實是,但是其實也沒人規定你不可以用,只要合乎語法即可(但是有一種語義不可以用,是的就是我們之前提到過的系統值語義:這種語義存儲的內容是被規定好的特殊階段的特殊數據,如果不符合則整個渲染流程報錯),陰影的預定義宏則是提前為將來生成的陰影坐標分配好了存儲的坐標索引(2)。
TRANSFER_SHADOW(o) // 計算陰影坐標并存儲
這一步就是計算陰影坐標的方法,Unity的著色器語言為我們封裝成了一個函數。
// 將頂點從模型空間轉換到世界空間float4 worldPos = mul(unity_ObjectToWorld, i.vertex);// Lambert光照模型:計算法線與光線夾角的點積fixed ndotl = saturate(dot(n, l));fixed4 color = _LightColor0 * _MainColor * ndotl;// 疊加4個點光源的光照color.rgb += Shade4PointLights(unity_4LightPosX0, unity_4LightPosY0, unity_4LightPosZ0, // 點光源位置unity_LightColor[0].rgb, unity_LightColor[1].rgb, unity_LightColor[2].rgb, unity_LightColor[3].rgb, // 點光源顏色unity_4LightAtten0, worldPos.rgb, n // 衰減和位置) * _MainColor;// 疊加環境光照color += unity_AmbientSky;// 使用Unity宏計算陰影衰減系數UNITY_LIGHT_ATTENUATION(shadowmask, i, worldPos.rgb)// 將陰影系數與顏色相乘,應用陰影效果color.rgb *= shadowmask;return color; // 返回最終顏色
這里為什么我們要采取蘭伯特模型而不是更精準的馮模型呢?
?我們根據蘭伯特模型的公式計算出基本的漫反射光照顏色值之后,再疊加四個點光源的效果,這里我們用了Unity內置的Shade4PointLights函數:
?綜上所述,現在我們的漫反射光照顏色值是蘭伯特光照模型和點光源效果的總和,我們再添加一個unity自帶的:
最后再乘以一個Unity自帶的陰影衰落因子就得到了ForwardBase輸出的結果。
然后是我們的ForwardAdd部分:
?
// -------- 額外的Pass:處理其他逐像素燈光的投影 --------
Pass
{// 標簽定義,此Pass用于附加光源,模式為"ForwardAdd"Tags{"LightMode" = "ForwardAdd"}// 混合模式:相加混合Blend One OneCGPROGRAM#pragma vertex vert#pragma fragment frag#pragma multi_compile_fwdadd_fullshadows // 支持多重編譯,包含完整陰影#include "UnityCG.cginc"#include "Lighting.cginc"#include "AutoLight.cginc"// 定義頂點到片段的數據結構,與基礎Pass一致struct v2f{float4 pos : SV_POSITION;float3 normal : TEXCOORD0;float4 vertex : TEXCOORD1;SHADOW_COORDS(2)};fixed4 _MainColor;// 頂點著色器v2f vert (appdata_base v){v2f o;o.pos = UnityObjectToClipPos(v.vertex);o.normal = v.normal;o.vertex = v.vertex;TRANSFER_SHADOW(o)return o;}// 片段著色器fixed4 frag (v2f i) : SV_Target{// 計算法線和光照方向float3 n = UnityObjectToWorldNormal(i.normal);n = normalize(n);float3 l = WorldSpaceLightDir(i.vertex);l = normalize(l);// 轉換頂點到世界空間float4 worldPos = mul(unity_ObjectToWorld, i.vertex);// Lambert光照計算fixed ndotl = saturate(dot(n, l));fixed4 color = _LightColor0 * _MainColor * ndotl;// 疊加點光源的光照color.rgb += Shade4PointLights(unity_4LightPosX0, unity_4LightPosY0, unity_4LightPosZ0,unity_LightColor[0].rgb, unity_LightColor[1].rgb,unity_LightColor[2].rgb, unity_LightColor[3].rgb,unity_4LightAtten0, worldPos.rgb, n) * _MainColor;// 使用陰影宏計算陰影系數UNITY_LIGHT_ATTENUATION(shadowmask, i, worldPos.rgb)// 應用陰影到顏色color.rgb *= shadowmask;return color; // 返回最終顏色}ENDCG
}
我們首先可以看到Blend One One,這是一種混合模式:
除此之外的ForwardAdd的代碼幾乎與ForwardBase的部分一模一樣,我們都只采用蘭伯特光照模型即可(其實具體的渲染模式和內部的光照模型并沒有直接掛鉤,不同的渲染模式主要是負責的職能不同而光照模型則是定義光照計算的方式不同)。
大體效果如圖: