UnityShader入門精要——第9章
Unity的渲染路徑
前向渲染路徑、延遲渲染路徑、頂點照明渲染路徑 Always ForwardBase 環境光、最重要的平行光、逐頂點/SH光源、lightmaps ForwardAdd 額外的逐畫素光照,每個pass對應一個光源 Deffered 會渲染G緩衝 ShadowCaster 把物體的深度資訊渲染到陰影對映紋理lightmap或一張深度紋理中 prepassBase 用於遺留的延遲渲染,該pass會渲染法線和高光反射的指數部分 prepassFinal 用於遺留的延遲渲染,合併紋理、光照和自發光 vertex vertexLMRGBM VeretxLM 遺留的頂點光照
1. 前向渲染路徑
每進行一次完整的前向渲染,需要渲染該物件對應的渲染圖元,並計算兩個緩衝區的資訊:顏色緩衝區、深度緩衝區。
多個逐畫素光源就要渲染多次。
場景中最亮的平行光:總是逐畫素
渲染模式為not important的光源:逐頂點或SH
渲染模式為important:逐畫素
BasePass:一個逐畫素的平行光以及所有逐頂點和SH光源 lightmap、環境光、自發光、陰影
AdditionalPass:其他影響該物體的逐畫素光源,每個光源執行一次pass (不支援陰影,但#pragma multi_compile_fwdadd_fullshadows開啟陰影)
一個BasePass僅會執行一次,一個Additional Pass會根據影響該物體的其他逐畫素光源數目被多次呼叫
- 頂點照明渲染路徑
硬體配置要求少、運算效能最高、效果最差,不支援陰影、法線對映、高精度的高光反射等。其實是前向渲染路徑的子集。通常一個pass完成對物體的渲染。 - 延遲渲染路徑
第一個pass計算哪些片元可見,通過深度緩衝實現,可見的片元,將其資訊(diffuse、emission、specular、深度、平滑度、normal、lightmap、反射探針、深度緩衝、模板緩衝等)儲存到G緩衝區中,再儲存到幀快取中。第二個pass利用G緩衝區的各個片元資訊,進行真正的光照計算。僅僅使用2個pass,與光源數目無關。延遲渲染缺點:不支援真正的抗鋸齒;不能處理半透明物體;對顯示卡有要求,要求顯示卡必須支援MRT等。
unity的光源型別
點光源、聚光燈、面光源
1. 光源型別有什麼影響
位置、顏色、衰減。
平行光:位置固定、衰減固定為1
點光源:有位置、衰減由一個函式定義
聚光燈:位置、範圍、衰減
在前向渲染中處理不同的光源型別
`
fixed4 frag(v2f i) : SV_Target {
fixed3 worldNormal = normalize(i.worldNormal);
#ifdef USING_DIRECTIONAL_LIGHT
fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz);
#else
fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz - i.worldPos.xyz);
#endiffixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb * max(0, dot(worldNormal, worldLightDir)); fixed3 viewDir = normalize(_WorldSpaceCameraPos.xyz - i.worldPos.xyz); fixed3 halfDir = normalize(worldLightDir + viewDir); 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(_LightMatrix0, float4(i.worldPos, 1)).xyz; fixed atten = tex2D(_LightTexture0, dot(lightCoord, lightCoord).rr).UNITY_ATTEN_CHANNEL; #elif defined (SPOT) float4 lightCoord = mul(_LightMatrix0, float4(i.worldPos, 1)); 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); }
`
如果場景中一個平行光,兩個點光源(Render Mode)為Auto,則會有3個渲染事件(平行光一個,兩個點光源各自一個);如果把這兩個點光源設為(Not Important),則只有一個渲染事件(兩個點光源的顯示不出來,只有一個平行光的效果)。
這是因為,如果逐畫素光源數目較多的話,該物體的AdditionalPass會被呼叫多次,影響效能。
同時,檢視幀偵錯程式可以看到,unity處理這些點光源的順序是按照他們的重要度排序的(顏色、強度、距離等),最先繪製最重要的光源。
unity的光照衰減
- 用於光照衰減的紋理
使用一張紋理作為查詢表來在片元著色器中計算逐畫素光照的衰減,避免數學公式的複雜性。但也有弊端:
需要預處理得到取樣紋理,而且紋理大小會影響衰減的精度。
不直觀、也不方便,因此一旦把資料儲存到查詢表,就無法用其他公式計算衰減。
但提升效能、得到的效果大部分情況下不錯。所以,unity預設是用這種方式計算衰減的。
float3 lightCoord = mul(_LightMatrix0, float4(i.worldPos, 1)).xyz;
fixed atten = tex2D(_LightTexture0, dot(lightCoord, lightCoord).rr).UNITY_ATTEN_CHANNEL;
- 使用數學公式計算衰減
float distance = length(_WorldSpaceLightPos0.xyz - i.worldPosition.xyz);
atten = 1.0/distance; //linear attenuation
#ifdef USING_DIRECTIONAL_LIGHT
fixed atten = 1.0;
#else
#if defined (POINT)
float3 lightCoord = mul(_LightMatrix0, float4(i.worldPos, 1)).xyz;
fixed atten = tex2D(_LightTexture0, dot(lightCoord, lightCoord).rr).UNITY_ATTEN_CHANNEL;
#elif defined (SPOT)
float4 lightCoord = mul(_LightMatrix0, float4(i.worldPos, 1));
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
unity的陰影
- 陰影是如何實現的
光線遇到不透明物體,無法再繼續照亮其他物體。Shadowmap技術:把攝像機的位置放在與光源重合的位置,那麼光源陰影區域就是攝像機照不到的區域。
在前向渲染路徑中,如果場景中最重要的平行光開啟了陰影,unity就會為該光源計算它的Shadowmap(陰影對映紋理)。這張Shadowmap本質上也是一張深度圖,它記錄了從該光源的位置出發、能看到的場景中距離它最近的表面位置(深度資訊)。
那麼在計算Shadowmap時,我們如何判定距離它最近的表面位置呢?一種方法是,先把攝像機放置在光源的位置,然後按照正常的渲染流程,即呼叫BassPass和Additional Pass來更新深度資訊,得到Shadowmap。這種方法會對效能造成一定的浪費,因為我們實際上僅僅需要深度資訊而已。而basePass和AdditionalPass中往往涉及很多複雜的光照模型計算。所以,unity選擇使用一個額外的pass,專門的pass叫ShadowCaster來更新光源的陰影對映紋理。
傳統的Shadowmap,我們會在正常渲染的pass中把頂點位置變換到光源空間下,以得到它在光源空間中的三維位置資訊。然後,使用xy分量對Shadowmap進行取樣,得到Shadowmap中該位置的深度資訊。如果該深度值小於該點的深度值(通常由z分量得到),那麼說明該點位於引用中。
unity5中使用螢幕空間的陰影對映紋理:unity通過呼叫LightMode為ShadowCaster的pass來得到可投射陰影的光源的陰影對映紋理以及攝像機的深度紋理。然後根據光源的Shadowmap和攝像機的深度紋理來得到螢幕空間的陰影圖。如果攝像機的深度圖中記錄的表面深度大於轉換到Shadowmap中的深度值,就說明該表面雖然是可見的,但卻在光源的陰影中。這樣,陰影圖就包含了螢幕空間中所有有陰影的區域。如果我們想要一個物體接收來自其他物體的陰影,只需要在shader中對陰影圖進行取樣。由於陰影圖是螢幕空間下的,所以,我們首先要把表面座標從模型空間轉換到螢幕空間,然後用這個座標對陰影圖進行取樣即可。
總結下,(1)一個物體接收其他物體的陰影:在shader對陰影對映紋理(Shadowmap,包括螢幕空間的陰影圖)進行取樣,把取樣結果和最後的光照結果相乘來產生陰影效果。
(2)如果我們想要一個物體向其他物體投射陰影,必須把該物體加入到光源的Shadowmap(陰影對映紋理)的計算中,從而讓其他物體在對陰影對映紋理取樣時,可以得到該物體的相關資訊。在unity中,這個過程是通過為該物體執行LightMode為ShadowCaster的Pass來實現的。如果使用了螢幕空間的投影對映技術,unity還會使用這個pass產生一張攝像機的深度紋理。 - 不透明物體的陰影
(1)讓物體投射陰影:在unity中,我們會選擇是否讓一個物體投射或接收陰影。這是通過設定mesh renderer元件中的cast shadows和receive shadows屬性實現的。開啟了cast shadows,unity就會把該物體加入到光源的Shadowmap計算中,正如前面所說,這個過程是通過為該物體執行LightMode為ShadowCaster的pass來實現的。如果不開啟receive shadow,那麼當我們呼叫unity的內建巨集和變數計算陰影時,這些巨集通過判斷該物體沒有開啟接收陰影,內部就不會為我們計算陰影。
我們把立方體的這兩個選項開啟,就會投射陰影。但是現在大家會有疑問:之前不是說unity要執行LightMode為ShadowCaster的pass來渲染Shadowmap和深度圖嗎,但立方體的shader中並沒有這個pass,其實,祕密在於fallback的語義:Fallback “Specular”,即內建的specular,而specular本身也沒有這個pass,但由於它的fallback呼叫了Vertexlit,他會繼續毀掉,並最終呼叫到內建的vertexLit。開啟內建的NormalVertexLit.shader,就可以看到裡面有LightMode為ShadowCaster的pass了。
// Pass to render object as a shadow caster
Pass {
Name "ShadowCaster"
Tags { "LightMode" = "ShadowCaster" }
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#pragma target 2.0
#pragma multi_compile_shadowcaster
#pragma multi_compile_instancing // allow instanced shadow pass for most of the shaders
#include "UnityCG.cginc"
struct v2f {
V2F_SHADOW_CASTER;
//UNITY_VERTEX_OUTPUT_STEREO
};
v2f vert( appdata_base v )
{
v2f o;
//UNITY_SETUP_INSTANCE_ID(v);
//UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(o);
TRANSFER_SHADOW_CASTER_NORMALOFFSET(o)
return o;
}
float4 frag( v2f i ) : SV_Target
{
SHADOW_CASTER_FRAGMENT(i)
}
ENDCG
}
它們的用處就是把深度資訊寫入到渲染目標中。這個pass的渲染目標可以是光源的陰影對映紋理或是攝像機的深度紋理。如果我們把shader中的fallback去掉,立方體就不能投射陰影了。當然,我們也可以不依賴fallback,自行在subshader中定義自己的這個pass。這種自定義的pass可以讓我們更靈活控制陰影的產生。
另外,預設情況下,光源的Shadowmap會剔除掉 物體的背面,所以立方體,右側的平面在光源空間下沒有任何正面,因此就不會新增到Shadowmap中,我們可以將cast shadow設為Two Sided來允許對物體的所有面都計算陰影資訊。
(2)讓物體接收陰影
#include "Lighting.cginc"
#include "AutoLight.cginc"
struct v2f {
float4 pos : SV_POSITION;
float3 worldNormal : TEXCOORD0;
float3 worldPos : TEXCOORD1;
SHADOW_COORDS(2)
};
v2f vert(a2v v) {
TRANSFER_SHADOW(o);
}
fixed4 frag(v2f i) : SV_Target {
fixed shadow = SHADOW_ATTENUATION(i);
return fixed4(ambient + (diffuse + specular) * atten * shadow, 1.0);
}
SHADOW_COORDS(宣告陰影紋理座標變數_ShadowCoord)、TRANSFER_SHADOW(會呼叫內建的ComputeScreenPos來計算_ShadowCoord,如果該平臺不支援螢幕空間的陰影對映技術,就會使用傳統的陰影對映技術。TRANSFER_sHADOW會把頂點座標從模型空間變換到光源空間後儲存到_ShadowCoord中)、SHADOW_ATTENUATION(負責使用_ShadowCoord對相關的紋理進行取樣,得到陰影資訊)是計算陰影時的“三劍客”。
注意,這些巨集需要用到上下文變數來進行計算,例如TRANSFER_SHADOW會使用v.vertex或a.pos來計算座標,因此為了能夠讓這些巨集正確工作,我們需要保證自定義的變數名和這些巨集中使用的變數名相匹配。a2v結構體的頂點座標變數名必須是vertex,頂點著色器的輸出結構體v2f必須命名為vertex,且v2f中的頂點位置變數名為pos。
3. 使用幀偵錯程式檢視陰影繪製過程
這些渲染事件可以分為4個部分:Camera.Render下UpdateDepthTexture(DepthPass.Job),即更新攝像機的深度紋理;Drawing下,Render.OpaqueGeometry下的Shadows.RenderJob,shadows.RenderShadowmap渲染得到平行光的陰影對映紋理。然後,RenderForwardOpaque.CollectShadows即根據深度紋理和陰影對映紋理得到螢幕空間的陰影圖,最後繪製渲染結果。
也就是,一:更新深度紋理;二:平行光 的陰影對映紋理;三:根據深度紋理和陰影對映紋理計算螢幕空間的陰影圖;最後繪製渲染結果。
4. 統一管理光照衰減和陰影
之前講過,在前向渲染路徑的BassPass中,平行光的衰減因子總是等於1,而在AdditionalPass中,我們需要判斷該pass處理的光源型別,再使用內建變數和巨集計算衰減因子。實際上,光照衰減和陰影對物體最終的渲染結果的影響本質上是相同的,都是把光照衰減因子和陰影值及光照結果相乘得到最終的渲染結果。那麼是否有什麼辦法同時計算兩個資訊呢?unity在shader裡提供了這樣的功能,主要是通過內建的UNITY_light_aTTENUATION巨集來實現的。
#include "Lighting.cginc"
#include "AutoLight.cginc"
struct a2v {
float4 vertex : POSITION;
float3 normal : NORMAL;
};
struct v2f {
float4 pos : SV_POSITION;
float3 worldNormal : TEXCOORD0;
float3 worldPos : TEXCOORD1;
SHADOW_COORDS(2)
};
v2f vert(a2v v) {
v2f o;
o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
o.worldNormal = UnityObjectToWorldNormal(v.normal);
o.worldPos = mul(_Object2World, v.vertex).xyz;
// Pass shadow coordinates to pixel shader
TRANSFER_SHADOW(o);
return o;
}
fixed4 frag(v2f i) : SV_Target {
fixed3 worldNormal = normalize(i.worldNormal);
fixed3 worldLightDir = normalize(UnityWorldSpaceLightDir(i.worldPos));
fixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb * max(0, dot(worldNormal, worldLightDir));
fixed3 viewDir = normalize(UnityWorldSpaceViewDir(i.worldPos));
fixed3 halfDir = normalize(worldLightDir + viewDir);
fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(max(0, dot(worldNormal, halfDir)), _Gloss);
// UNITY_LIGHT_ATTENUATION not only compute attenuation, but also shadow infos
UNITY_LIGHT_ATTENUATION(atten, i, i.worldPos);
return fixed4((diffuse + specular) * atten, 1.0);
}
由於使用了UNITY_LIGHT_ATTENUATION,我們的basePass和AdditionalPass的程式碼得以統一,不需要在BasePass中單獨處理陰影,也不需要在AdditionalPass中判斷光源型別來處理光照衰減。如果我們想要在AdditionalPass中新增陰影效果,就需要使用#pragma multi_compile_fwdadd_fullshadows編譯指令來代替AdditionalPass中的#pragma multi_compile_fwdadd指令。這樣一來,unity就會為額外的逐畫素光源計算陰影,並傳遞給shader。
5. 透明度物體的陰影
- 透明物體的實現通常會使用AlphaTest或Blend,我們需要小心設定這些物體的fallback。透明度測試的處理比較簡單,但如果我們直接用VertexLit、Diffuse、Specular等作為回撥,往往無法得到正確的陰影。這是因為透明度測試需要在片元著色器中捨棄某些片元。我們如果像第4節一樣,在AlphaTest裡面加上陰影的,鏤空區域陰影會不正常。這是因為,我們使用的是內建的vertexlit中提供的ShadowCaster來投射陰影,而這個pass中並沒有進行任何透明度測試的計算。因此,它會把整個物體的深度資訊渲染到深度圖和陰影對映紋理中。因此,如果我們想要得到經過透明度測試後的陰影效果,就需要提供一個有透明度測試功能的ShadowCaster Pass。為了既達到效果寫的程式碼又少,將Fallback設定為Transparent/Cutout/VertexLit
,在這個內建檔案中,它的ShadowCaster pass也計算了透明度測試,因此,會把裁剪後的物體深度資訊寫入到深度圖和Shadowmap中,但要注意,這個Shader中計算透明度測試時,使用了名為_Cutoff的屬性來進行透明度測試,因此,這就要求我們的shader中必須提供名為_cutoff的屬性。這樣的話,有個問題,出現了一些不應該透過光的部分,這是因為只考慮了正面,沒有考慮背面。讓正方體的mesh Renderer元件的castShadows屬性設定為Two Sides,強制unity在計算Shadowmap時,計算所有面的深度資訊。
FallBack "Transparent/Cutout/VertexLit"
為使用透明度混合的物體新增陰影是一件比較複雜的事情。由於blend需要關閉深度寫入,由此帶來的問題也影響了陰影的生成。總體來說,要想為這些半透明物體產生正確的陰影,需要在每個光源空間下仍然嚴格按照從後往前的順序進行渲染,這樣,陰影處理會變得很複雜,而且也會影響效能。所以,在unity中,所有blend shader都是不會產生任何陰影的。當然我們可以強制為半透明物體生成陰影,通過
FallBack "Transparent/VertexLit"
實現。效果不一定正確。