探索Unity中的陰影渲染
投射一個方向光陰影
接收一個方向光陰影
支持對聚光源和點光源陰影
work in unity 5.6.6f2 unity陰影
1方向光陰影
前面寫的光照shader產生了相當真實的效果,可它假設着來自每個光源的光線最終都會擊中它的片元,但是這只有在那些光線沒有被遮擋才成立。
1-1. 方向光投射陰影的草圖
當一個物體位於光源和另一個物體之間時,它可能會阻止部分或全部光線到達另一個物體。這些光線照亮了第一個物體就不再可能照亮第二個物體。因此,第二個物體有一部不發光,而不發光的區域位於第一個物體的陰影下。我們通常是這樣描述:第一個物體投射了一個陰影到第二個物體。
實際上,在全光照和全陰影的存在過渡區,被稱為半陰影。這是因為所有光源都有一個體積,因此,這些區域只有部分光源是可見的,意味着它是部分陰影。光源遠大,表面距離陰影投射器越遠,半影區域也就越大。
但是Unity不支持半影,只支持軟陰影soft shadow,但它是陰影過濾算法。
1-2. 半陰影/soft shadow in unity
1.1 啟用陰影
先關閉環境光,這樣會更容易看見陰影。
1.2.沒有投射陰影
沒有陰影,物體間的空間視覺感不太強。在QualitySetting可以打開或關閉陰影。
1.3. 陰影參數
同時確保光源開啟投射陰影,分辨率依賴於上面的quality設置
1.4 shadow type
1.5 投射陰影
1.2 陰影貼圖
Unity是如何把陰影添加到屏幕?上面所有物體使用的standard着色器,有一些方法確定光線是否被阻擋。
要搞清楚一個點是否在陰影中,可以通過在場景中從光線到表面片元投射光線,如果光線在到達表面之前擊中某些東西,說明它就被阻擋了。這些事是物理引擎做的,但是要計算每個片元與每個光是不實際的,而且還要把結果傳遞給GPU。
現在有許多支持實時陰影的技術,它們各有優劣。而Unity采用了最常用的技術:Shadow Mapping。這意味着unity把陰影數據存儲至紋理中。現在來看看它是如何工作的。
打開frame Debugger,Window/Frame Debugger。點擊Enable,按順序查看面板信息。注意看看每幀在gameScene視圖中的不同,以及陰影的開啟。
1.6 frame debugger調試
當啟用陰影繪制時,這個繪制過程變得非常復雜:有更多的渲染階段,和更多的draw call。陰影繪制非常昂貴!
1.3 渲染深度紋理
Rendering to the Depth Texture
當方向陰影激活后,Unity在渲染過程開啟一個depth pass通道計算。結果存儲在與屏幕分辨率相匹配的紋理,這個pass通道會渲染整個屏幕,但是只收集每個片元的深度信息。這些信息與GPU用於確定一個片段渲染結束時在先前渲染的片段之上(前)還是之下(后)的信息相同。
這個數據對應在裁剪空間(clip space)坐標的z分量值。而裁剪空間是定義攝像機能看見的區域,深度信息最終存儲為0-1范圍內的值。在debugger查看該紋理時,近裁切面附近的紋理顯示趨近為(白)淺色,遠裁切面附近的紋素texel,顏色趨近黑(暗)色。
1.7 depth texture, 攝像機近裁切面為5
1.8 與屏幕分辨率一致
這些信息實際上與陰影沒有太多直接關系,但Unity在后面的pass通道使用了它。
1.4 渲染陰影貼圖
Rendering to Shadow Maps
這步主要工作:先渲染第一個光源的陰影貼圖,然后就會渲染第二個光源的陰影貼圖。
再一次渲染整個屏幕,並再次把深度信息存儲在紋理中。但是,這此的屏幕渲染是從光源位置角度進行的,實際上是把光源作為攝像機。這意味着用深度值告訴了我們光線擊中物體之前走了多遠距離,這可以用來確定什么東西被遮蔽了!
陰影貼圖記錄了實際的幾何圖形的深度信息。而法線貼圖是為了添加粗糙表面的一種錯覺,陰影貼圖會忽略它們。因此,陰影不受法線貼圖的影響。
由於我們使用方向光,這些光模擬的攝像機是正交投影,沒有透視投影。因此它們模擬的相機的位置精確性就不那么重要。Unity將定位常規相機使其能夠看見視野內所有物體。
1.9 左第一個光源,右第二個光源
事實上,原來Unity渲染整個場景不是每個光只渲染一次,而是每個光要渲染四次!這個陰影紋理被分成四個象限,每個象限從不同的角度呈現。這是因為我們選擇使用Four Cascades(QualitySetting)。如果我們設置為Two Cascades,就是每個光渲染兩次;如果設置沒有,只會渲染一次。我們接下來要探查陰影質量與該項設置的關系。Unity為什么渲染這么多次。
1.5 收集陰影
Collecting Shadows
我們已經從攝像機的角度得到場景的深度信息,也有了從每個光模擬的相機視角得到的深度信息,這些數據存在不同的裁剪空間。但是我們知道這些空間的相對位置和方向,因此能從一個空間轉換到另一個空間。這允許我們從兩個視角比較深度測量。理論上講,我們有兩個向量應該在同一點交會結束,這樣相機和光源都能看見該點,說明它被點亮了。如果光的向量在到達該點之前結束,則光被擋住,這意味着該點被陰影化。
當攝像機看不到一個點時?看不到的這些點被隱藏在距離相機更近的其他點后面。 場景深度紋理僅包含最接近的點。 因此沒有時間浪費在評估隱藏點。
1.20 每個光的屏幕空間陰影
Unity通過渲染一個單獨的覆蓋整個視野的面片來創建這些紋理,它使用了Hidden/Internal-ScreenSpaceShadows shader的通道,每個片元從場景和光源的深度紋理采樣,進行比較,渲染最終陰影值到屏幕空間的陰影紋理。亮的紋素值設為1,陰影紋素值設為0。此時Unity能執行過濾,創建柔和的陰影。
1.21 shader 通道0
為什么Unity在渲染和收集間交替?每個光需要它自己的屏幕空間陰影貼圖,然而從光源位置視野渲染的陰影貼圖能被重復使用。
1.6 采樣陰影貼圖
Sampling the Shadow Maps
最后,Unity完成了陰影渲染。現在屏幕是常規渲染,只有一個更改:光照顏色與它的陰影貼圖的值相乘。這就消除了被遮擋的光線。渲染的每個片元都要采樣陰影貼圖,每個最終隱藏在其他對象之后的片元會最后繪制。因此這些片元最后能接收到最終能遮擋它們的對象的陰影。當在frame debugger步進調試觀察時,您還可以看到陰影在實際投射它的對象之前出現。當然這些錯誤只在渲染幀時很明顯,一旦完成渲染就是正確的了
1.22 部分渲染幀
1.7 陰影質量
雖然場景是從光源的方向進行渲染,但是該方向與場景內攝像機視野方向不匹配。因此陰影貼圖的紋素與最終呈現圖像的紋素是沒有對齊的,會出現鋸齒。陰影貼圖的分辨率也會不同,最終圖像的分辨率是由顯示設置決定的,而陰影貼圖的分辨率由陰影質量設置決定。
當陰影貼圖的紋素最后渲染的比最終圖像大時,將很明顯:陰影的邊緣出現疊加,在使用硬陰影時非常明顯。
1.23 硬陰影 vs 軟陰影
在質量設置面板修改使用hard shadow、lowest resolution、no cascades。就會看見滿屏的鋸齒。
1.24 低質量陰影
“陰影是一張紋理”現在就非常明顯了。但是上圖有些陰影出現在了不該出現的地方。
距離攝像機越近的陰影,它們的紋素變得越大。這是由於陰影貼圖當前覆蓋了場景相機的整個可視區域。在QualitySetting面板通過降低陰影覆蓋的區域,來提升靠近相機區域的陰影質量。如圖1.25
1.25 Shadow Distance降至25,其他參數與1.24一致
通過限制靠近屏幕相機的陰影區域,我們能使用相同的陰影紋理去覆蓋更多小區域。結果是能得到更好的陰影。但是會丟失更遠區域的陰影細節,因為當陰影接近最大距離時會逐漸消失。
理想情況是,既要獲得近距離高質量陰影,同時也要保留遠處的陰影。因為遠處的陰影最后渲染在較小的屏幕區域,就可以用作低分辨率陰影紋理。這就是Shadow Cascades的工作。當啟用該選項,多個陰影貼圖渲染進同一張紋理,每張貼圖對應某些距離來使用。
1.26 fourCascades,100Distance,hardShodw,LowResolution
當使用FourCascades,1.26結果看起來比1.24要好,盡管我們使用了同一張紋理分辨率,我們更有效的使用了紋理。不過缺點就是我們現在至少要渲染場景3次以上。當渲染屏幕空間陰影紋理時,Unity關注從正確Cascade采樣,如圖1.27CascadeSplits:一個cascade結束是下一個的開始。
1.27 Cascade Splits
可以控制cascade的范圍,作為陰影距離的一部分。也能通過改變Shading Mode/Miscellaneous/Shadow Cascades觀察scene視圖的變化。
1.28 Cascade范圍:StableFit
圖1.28顯示的cascade形狀(覆蓋區域)是可以通過Shadow Projection調整,默認是Stable Fit:這個模式cascade條帶選擇的區域基於距離攝像機位置的遠近。其他模式是Close Fit:使用相機的深度信息替代,在相機可視方向產生一條規則的條帶。
1.29 Close Fit
Close Fit模式可以更高效的利用陰影紋理,繪制更高質量的陰影。然而,該陰影投射模式(ShadowProjection)取決於陰影產生后位置和方向以及相機參數。結果是,當移動或旋轉相機,陰影貼圖也會跟着移動。這就是著名的陰影抖動。所以Stale Fit是引擎默認的選項。
1.30 Close Fit: swimming
Stable Fit模式下,在相機位置改變時Unity能夠對齊紋理,紋素看起來好像沒動。實際上cascade移動了,只是在cascade相互過渡時陰影會發生改變。如果沒有注意到cascade改變,就不容易察覺到。
1.31 Stable Fit: edge transition
1.8 陰影“痤瘡”(0!什么鬼)
當我們使用低質量的硬陰影時,我們看見一些陰影出現在不正確的地方。不幸的是,不管如何設置Quality Setting都會發生。
Shadow Acne:陰影貼圖中每個紋素表示光線擊中表面的點。然而,這些紋素不是單獨點。它們最后要覆蓋很大的區域並且與光的方向對齊,而不是與表面一致。結果時,它們會像黑色瓦片最終黏在、穿過、伸出表面;當陰影紋理的一部分從投射出陰影的表面伸出時,表面看起來也會產生陰影。
1.32 凸起
陰影凸起的另一個來源是數字精度的限制,當使用非常小的距離時這些限制會導致不正確的結果。默認是0.05.
1.33 light組件中設置沒有biases
避免該問題的一個方法是:當渲染陰影紋理時增加深度偏移。這個偏差系數目的是增加‘光投射到表面距離’,把陰影‘推進’表面內。
1.34 Biases系數控制粉刺
較低的Bias系數會產生粉刺,而較高的偏差系數就會有另一個問題:當投射陰影的對象逐漸遠離光源時,陰影也會逐漸飄離原對象。使用較小的值問題還可接受,但太大的值會導致物體與該物體的陰影不再相連接了,好像飛起來了。
1.35 太大的Bias導致陰影飄移
除了距離bias偏差,還有法線偏差。該系數輔助調整陰影投射:沿着法線,將投射的陰影頂點向內‘推’。該值也會改善“陰影粉刺”,但是越大的值越會使陰影變得更小並且有可能使陰影中間出現洞。
best bias settings?沒有最優的默認值,必須不停的實驗調整 。
1.9 抗鋸齒
Anti-Aliasing:圖形邊緣鋸齒緩和。在Unity開啟了4倍抗鋸齒,感覺並沒有達到想要的抗鋸齒效果。
Unity采用的多重采樣抗鋸齒方案:MSAA,通過沿三角形邊緣執行超級采樣以消除邊緣鋸齒,更重要的是Unity渲染屏幕空間陰影時,它使用了一個單獨四方面片覆蓋整個可視區域。結果是,這就沒有了三角形邊緣,因此MSAA對屏幕空間陰影紋理采樣就沒有效果了。MSAA對最終圖像有效,但陰影值是取之屏幕空間陰影紋理,當亮表面緊挨着暗表面被陰影覆蓋時就非常明顯。明暗之間的邊緣是反鋸齒的,而陰影邊緣則不是。
1.36 no AA
1.37 4倍MSAA
當然也有FXAA,是屏幕后處理抗鋸齒,效果挺好!
2投射陰影
通過上面我們知道了Unity如何創建方向光陰影,是時候寫自己的Shader來支持陰影了。當前光照shader既不支持投射陰影也不支持接收陰影。
首先來處理投射陰影:我們知道對於方向光陰影Unity會渲染多次屏幕。對每個陰影紋理一次是深度pass渲染,一次是每個光源渲染。而屏幕空間陰影紋理是屏幕效果暫時與我們無關。陰影渲染Pass標簽是ShadowCaster。因為我們只對深度值感興趣,它與別的Pass相比應該會簡單。增加一個pass
Pass{ Tags{"LightMode" = "ShadowCaster"} CGPROGRAM #pragma target 3.0 #pragma vertex MyVertexProgram #pragma fragment MyFragmentProgram #include "MyShadow.cginc" ENDCG }
創建一個MyShadow.cginc文件
#if !defined(MY_SHADOW_INCLUDE) #define MY_SHADOW_INCLUDE #include “UnityCG.cginc” struct InputData { float4 position : POSITION; }; float4 MyVertexProgram(InputData i) : SV_POSITION{ return UnityObjectToClipPos(i.position); } half4 MyFragmentProgram() : SV_TARGET{ return 0; } #endif
上面寫完就嫩產生方向光陰影了。下面開始用代碼調優陰影質量。
2.1 偏差-Bias
我們要支持陰影的偏移。在渲染深度Pass時該值是0,但當渲染陰影紋理時,偏差值取光照組件設置。我們要做的就是:在頂點函數中在裁切空間下,對頂點坐標應用深度偏差。UnityCG函數UnityApplyLinerShadowBias:
float4 MyVertexProgram(InputData i) : SV_POSITION{ float4 position = UnityObjectToClipPos(i.position); return UnityApplyLinearShadowBias(position); }
在裁剪空間增加Z分量,復雜的是在其次坐標空間下,必須補償透視投影,這樣偏移不會隨着與相機距離改變而改變,也必須確保結果不會越界。
float4 UnityApplyLinearShadowBias(float4 clipPos) { #if defined(UNITY_REVERSED_Z) // We use max/min instead of clamp to ensure proper handling of the rare case // where both numerator and denominator are zero and the fraction becomes NaN. clipPos.z += max(-1, min(unity_LightShadowBias.x / clipPos.w, 0)); float clamped = min(clipPos.z, clipPos.w*UNITY_NEAR_CLIP_VALUE); #else clipPos.z += saturate(unity_LightShadowBias.x/clipPos.w); float clamped = max(clipPos.z, clipPos.w*UNITY_NEAR_CLIP_VALUE); #endif clipPos.z = lerp(clipPos.z, clamped, unity_LightShadowBias.y); return clipPos; }
同時支持Normal Bias,必須根據法向量移動頂點坐標。因此,添加一個normal變量。然后可以使用UnityCG定義的UnityClipSpaceShadowCasterPos函數
float4 MyVertexProgram(InputData i) : SV_POSITION{ //float4 position = UnityObjectToClipPos(i.position); float4 position = UnityClipSpaceShadowCasterPos(i.position, i.normal); return UnityApplyLinearShadowBias(position); }
先將頂點坐標轉換到世界空間,然后轉換到裁剪空間。計算光的方向,計算法線和光的角度,取正弦值,最后轉與觀察投影矩陣相乘轉到裁剪空間。
float4 UnityClipSpaceShadowCasterPos(float4 vertex, float3 normal) { float4 wPos = mul(unity_ObjectToWorld, vertex); if (unity_LightShadowBias.z != 0.0) { float3 wNormal = UnityObjectToWorldNormal(normal); float3 wLight = normalize(UnityWorldSpaceLightDir(wPos.xyz)); // apply normal offset bias (inset position along the normal) // bias needs to be scaled by sine between normal and light direction // (http://the-witness.net/news/2013/09/shadow-mapping-summary-part-1/) // // unity_LightShadowBias.z contains user-specified normal offset amount // scaled by world space texel size. float shadowCos = dot(wNormal, wLight); float shadowSine = sqrt(1-shadowCos*shadowCos); float normalBias = unity_LightShadowBias.z * shadowSine; wPos.xyz -= wNormal * normalBias; } return mul(UNITY_MATRIX_VP, wPos); }
寫完就具備了完全的陰影投射
3接收陰影
First,我們先關注主方向光的陰影,因為該光源屬於BasePass,必須要先適配。當主方向光投射陰影,Unity會找一個啟用了SHADOWS_SCREEN關鍵字的shader變體。所以我們要在Base Pass創建兩個變體,同之前使用頂點光關鍵字類似:一個無,一個是該關鍵字。
#pragma multi_compile _ VERTEXLIGHT_ON#pragma multi_compile _ SHADOWS_SCREEN
該basePass有兩個multi_compile指令,每個都是單關鍵字。因此編譯后這里會有4個變體:
// Total snippets: 3 // ----------------------------------------- // Snippet #0 platforms ffffffff: SHADOWS_SCREEN VERTEXLIGHT_ON 4 keyword variants used in scene: <no keywords defined> SHADOWS_SCREEN VERTEXLIGHT_ON SHADOWS_SCREEN VERTEXLIGHT_ON
(老版本Unity有可能出現)當增加了multi_compile指令后,shader編譯器會提示關於_ShadowCoord不存在。這是因為UNITY_LIGHT_ATTENUATION宏在使用陰影時的行為不同導致。在MyLighting_shadow.cginc頂點函數快速修復
#else
UNITY_LIGHT_ATTENUATION(attenuation, 0, i.worldPos);
#endif
3.1 采樣陰影
Secend,采樣屏幕空間陰影紋理。
Third,需要獲取屏幕空間紋理坐標,從頂點函數傳遞給片元函數。在插值器Interpolator添加一個float4 變量以支持傳遞陰影紋理坐標。從裁剪空間開始(裁剪空間頂點坐標)。
struct Interpolator{
#if defined(SHADOWS_SCREEN) float4 shadowCoordinate : TEXCOORD6; #endif } Interpolators MyVertexProgram(VertexData v) { //。。。 #if defined(SHADOWS_SCREE) i.shadowCoordinate = i.position; #endif //。。。 }
3.1 錯誤的紋理坐標映射
AutoLignt.cginc定義了Sampler2D _ShadowMapTexture,可以通過它訪問屏幕陰影紋理。但是要覆蓋整個屏幕,就需要屏幕空間坐標。在裁剪空間,XY坐標范圍是[-1, 1],而屏幕空間下是[0,1];然后偏移坐標與屏幕左小腳等於0對齊。因為我們處理的使透視變換,偏移坐標值取決於距離,這里的偏移值等於加上齊次坐標的w分量之后的一半。
#if defined(SHADOWS_SCREEN)
i.shadowCoordinate.xy = (i.position.xy + i.position.w) * 0.5;
i.shadowCoordinate.zw = i.position.zw;
#endif
3.2 錯誤的左下角映射
圖3.2的投影錯誤,還需要通過x和y除以齊次坐標進一步轉換
3.3 錯誤投影
3.3 結果仍然是錯誤的,影子被拉伸了。這是由於在頂點函數計算導致,不應該在傳遞給片元函數時提前修改原始數據,需要保持它們的獨立性。在片元函數再次除以w.
3.3顛倒的投影
此時,影子是上下顛倒的。如果它們被翻轉,這意味着你的圖形Direct3D屏幕空間Y坐標從0向下到1,而不是向上。要與此同步,翻轉頂點的Y坐標。
#if defined(SHADOWS_SCREEN) i.shadowCoordinate.xy = (float2(i.pos.x, -i.pos.y) + i.pos.w);// (i.pos.xy + i.pos.w) * 0.5; i.shadowCoordinate.zw = i.pos.zw; #endif
3.4 繼續錯誤
3.2 內置函數使用
SHADOW_COORDS宏定義紋理坐標
TRANSFRE_SHADOW宏獲取陰影紋理坐標(轉換)
#define TRANSFER_SHADOW(a) a._ShadowCoord = ComputeScreenPos(a.pos);
SHADOW_ATTENUATION宏陰影紋理明暗衰減
#defineSHADOW_COORDS
(idx1) unityShadowCoord4 _ShadowCoord : TEXCOORD##idx1; #define SHADOW_ATTENUATION(a) unitySampleShadow(a._ShadowCoord)
UNITY_LIGHT_ATTENUATION宏包含了SHADOW_ATTENUATION宏使用,可替換之
當啟用SHADOWS_SCREEN指令時,會自動計算,不啟用不計算,沒有任何損失。
struct Interpolators { … // #if defined(SHADOWS_SCREEN) // float4 shadowCoordinates : TEXCOORD5; // #endif SHADOW_COORDS(5) … };
Interpolators MyVertexProgram (VertexData v) { …// #if defined(SHADOWS_SCREEN)// i.shadowCoordinates = i.position;// #endifTRANSFER_SHADOW(i); … }
UnityLight CreateLight (Interpolators i) {
…
#if defined(SHADOWS_SCREEN)
float attenuation = SHADOW_ATTENUATION(i);
#else
UNITY_LIGHT_ATTENUATION(attenuation, 0, i.worldPos);
#endif
UNITY_LIGHT_ATTENUATION(attenuation, i, i.worldPos); … }
3.5 正確了
ComputeScreenPos函數
inline float4 ComputeNonStereoScreenPos(float4 pos) { float4 o = pos * 0.5f; o.xy = float2(o.x, o.y*_ProjectionParams.x) + o.w; o.zw = pos.zw; return o; } inline float4 ComputeScreenPos(float4 pos) { float4 o = ComputeNonStereoScreenPos(pos); #if defined(UNITY_SINGLE_PASS_STEREO) o.xy = TransformStereoScreenSpaceTex(o.xy, pos.w); #endif return o; }
4聚光燈陰影
關閉方向光,增加聚光燈后,竟然直接有陰影了。這是Unity宏帶來的便利。
4.1 點光源陰影
再看幀調試器
4.2 SpotLight Debugger
- 圖4.2對於聚光燈源陰影的渲染工作量很少,不同之處:
1沒有方向光獨立的深度pass和屏幕空間陰影pass,而是直接渲染陰影紋理;
2與方向光渲染陰影還有很大的差別之處:聚光燈光線不是平行的,因此用光的位置模擬相機視角會得到一個透視視角,結果就是不支持陰影分段渲染(cascades);
3normal bias(法線偏差)只支持方向光陰影,對於其他光源類型簡單的置為0;
4采樣代碼不同。 - 相同之處:投射陰影的這段代碼通用。
4.1采樣陰影紋理
由於聚光燈不使用屏幕空間的陰影,這段采樣紋理代碼就有點不一樣。因此,如果我們想要使用軟陰影,我們必須在fragment程序中進行過濾。而Unity宏已經做了過濾計算UnitySampleShadowmap。
//陰影坐標把頂點坐標從模型空間轉到世界空間再轉到光的陰影空間得到。
// ---- Spot light shadows #if defined (SHADOWS_DEPTH) && defined (SPOT) #define SHADOW_COORDS(idx1) unityShadowCoord4 _ShadowCoord : TEXCOORD##idx1; #define TRANSFER_SHADOW(a) a._ShadowCoord = mul (unity_WorldToShadow[0], mul(unity_ObjectToWorld,v.vertex)); #define SHADOW_ATTENUATION(a) UnitySampleShadowmap(a._ShadowCoord) #endif
然后SHADOW_ATTENUATION
宏使用UnitySampleShadowmap
函數采樣陰影映射。這個函數定義在UnityShadowLibrary,AutoLight文件引用了它。當使用硬陰影時,該函數對陰影紋理采樣一次。當使用軟陰影時,它對紋理采樣四次並對結果取平均值。這個結果沒有用於屏幕空間陰影的過濾效果好,但是速度快得多。
4.3 hard vs. soft spotLight Shadow
// Spot light shadows inline fixedUnitySampleShadowmap
(float4 shadowCoord) { // DX11 feature level 9.x shader compiler (d3dcompiler_47 at least) // has a bug where trying to do more than one shadowmap sample fails compilation // with "inconsistent sampler usage". Until that is fixed, just never compile // multi-tap shadow variant on d3d11_9x. #if defined (SHADOWS_SOFT) && !defined (SHADER_API_D3D11_9X) // 4-tap shadows #if defined (SHADOWS_NATIVE) #if defined (SHADER_API_D3D9) // HLSL for D3D9, when modifying the shadow UV coordinate, really wants to do // some funky swizzles, assuming that Z coordinate is unused in texture sampling. // So force it to do projective texture reads here, with .w being one. float4 coord = shadowCoord / shadowCoord.w; half4 shadows; shadows.x = UNITY_SAMPLE_SHADOW_PROJ(_ShadowMapTexture, coord + _ShadowOffsets[0]); shadows.y = UNITY_SAMPLE_SHADOW_PROJ(_ShadowMapTexture, coord + _ShadowOffsets[1]); shadows.z = UNITY_SAMPLE_SHADOW_PROJ(_ShadowMapTexture, coord + _ShadowOffsets[2]); shadows.w = UNITY_SAMPLE_SHADOW_PROJ(_ShadowMapTexture, coord + _ShadowOffsets[3]); shadows = _LightShadowData.rrrr + shadows * (1-_LightShadowData.rrrr); #else // On other platforms, no need to do projective texture reads. float3 coord = shadowCoord.xyz / shadowCoord.w; half4 shadows; shadows.x = UNITY_SAMPLE_SHADOW(_ShadowMapTexture, coord + _ShadowOffsets[0]); shadows.y = UNITY_SAMPLE_SHADOW(_ShadowMapTexture, coord + _ShadowOffsets[1]); shadows.z = UNITY_SAMPLE_SHADOW(_ShadowMapTexture, coord + _ShadowOffsets[2]); shadows.w = UNITY_SAMPLE_SHADOW(_ShadowMapTexture, coord + _ShadowOffsets[3]); shadows = _LightShadowData.rrrr + shadows * (1-_LightShadowData.rrrr); #endif #else float3 coord = shadowCoord.xyz / shadowCoord.w; float4 shadowVals; shadowVals.x = SAMPLE_DEPTH_TEXTURE (_ShadowMapTexture, coord + _ShadowOffsets[0].xy); shadowVals.y = SAMPLE_DEPTH_TEXTURE (_ShadowMapTexture, coord + _ShadowOffsets[1].xy); shadowVals.z = SAMPLE_DEPTH_TEXTURE (_ShadowMapTexture, coord + _ShadowOffsets[2].xy); shadowVals.w = SAMPLE_DEPTH_TEXTURE (_ShadowMapTexture, coord + _ShadowOffsets[3].xy); half4 shadows = (shadowVals < coord.zzzz) ? _LightShadowData.rrrr : 1.0f; #endif // average-4 PCF half shadow = dot (shadows, 0.25f); #else // 1-tap shadows #if defined (SHADOWS_NATIVE) half shadow = UNITY_SAMPLE_SHADOW_PROJ(_ShadowMapTexture, shadowCoord); shadow = _LightShadowData.r + shadow * (1-_LightShadowData.r); #else half shadow = SAMPLE_DEPTH_TEXTURE_PROJ(_ShadowMapTexture, UNITY_PROJ_COORD(shadowCoord)) < (shadowCoord.z / shadowCoord.w) ? _LightShadowData.r : 1.0; #endif #endif return shadow; }
5點光源陰影
如果直接使用點光源,會有編譯報錯:undeclared identifier 'UnityDecodeCubeShadowDepth'。該函數在UnityCG.cginc文件。
5.1 UnityPBSLighting文件引用;AutoLight文件引用
所以根據引用結構,需要把UnityPBSLighing文件放在第一位引用。就不會報錯了。
5.2 左:render six times per light
5.1 投射陰影
從幀調試器查看,左邊一個光要渲染6次,兩盞光就是12次了。有很多個RenderJobPoint渲染了。結果是,點光源的陰影紋理是一個立方體貼圖,而立方體貼圖是通過相機在6個不同方向觀察場景,每個方向渲染一面組成六面體,前面1.4講過把光源模擬相機對屏幕渲染。所以點光源陰影計算很費,尤其是實時點光源陰影。
5.3 錯誤的陰影紋理
當渲染點光源陰影紋理時,Unity引擎會找shader變體關鍵字SHADOWS_CUBE.而
SHADOWS_DEPTH
關鍵字只適用於方向光和聚光燈。為了支持點光源陰影,Unity提供了一個特殊編譯指令
#pragma multi_compile_shadowcaster
// ----------------------------------------- // Snippet #2 platforms ffffffff: SHADOWS_CUBE SHADOWS_DEPTH 2 keyword variants used in scene:SHADOWS_DEPTH SHADOWS_CUBE
所以,需要創建一個獨立的處理程序。這里首先要計算光到表面的距離,但得知道光到表面的方向。在頂點函數先轉換頂點坐標所在世界空間,再計算光的方向。然后在片元函數計算該方向向量長度再與bias偏差相加。然后再除以點光源的范圍映射到[0.1]再與長度相乘,最后解碼。而_LightPositionRange.w = 1/range已經計算好了隱射范圍,直接用。
#if defined(SHADOWS_CUBE) struct Interplotars { float4 position : SV_POSITION; float3 lightVec : TEXCOORD0; }; Interplotars MyVertexProgram(InputData v){ Interplotars i; i.position = UnityObjectToClipPos(v.position); i.lightVec = mul(unity_ObjectToWorld, v.position).xyz - _LightPositionRange.xyz; //float4 position = UnityClipSpaceShadowCasterPos(i.position, i.normal);//方向光源:簡單的裁剪空間頂點坐標 return i; } half4 MyFragmentProgram(Interplotars i) : SV_TARGET{ float depth = length(i.lightVec) + unity_LightShadowBias.x; depth *= _LightPositionRange.w; return UnityEncodeCubeShadowDepth(depth); } #else float4 MyVertexProgram(InputData i) : SV_POSITION{ //float4 position = UnityObjectToClipPos(i.position); float4 position = UnityClipSpaceShadowCasterPos(i.position, i.normal); return UnityApplyLinearShadowBias(position); } half4 MyFragmentProgram() : SV_TARGET{ return 0; } #endif
5.4 正確的陰影紋理
UnityEncodeCubeShadowDepth函數:
// Shadow caster pass helpers float4 UnityEncodeCubeShadowDepth (float z) { #ifdef UNITY_USE_RGBA_FOR_POINT_SHADOWS return EncodeFloatRGBA (min(z, 0.999)); #else return z; #endif }
// 使用浮點類型cube——map,存儲再8位RGBA紋理 inline float4 EncodeFloatRGBA( float v ) { float4 kEncodeMul = float4(1.0, 255.0, 65025.0, 16581375.0); float kEncodeBit = 1.0/255.0; float4 enc = kEncodeMul * v; enc = frac (enc);//返回小數部分 enc -= enc.yzww * kEncodeBit; return enc; }
5.2 采樣陰影紋理
在additional pass的編譯指令,Unity宏已經做了。
//同樣計算光的方向,然后采樣cubeMap。區別是float3類型而不是float4,不需要齊次坐標。 // ---- Point light shadows #if defined (SHADOWS_CUBE) #define SHADOW_COORDS(idx1) unityShadowCoord3 _ShadowCoord : TEXCOORD##idx1; #define TRANSFER_SHADOW(a) a._ShadowCoord = mul(unity_ObjectToWorld, v.vertex).xyz - _LightPositionRange.xyz; #define SHADOW_ATTENUATION(a) UnitySampleShadowmap(a._ShadowCoord) #endif // ------------------------------------------------------------------ // Point light shadows //在這種情況下,UnitySampleShadowmap采樣一個立方體地圖,而不是2D紋理。 #if defined (SHADOWS_CUBE) samplerCUBE_float _ShadowMapTexture; inline float SampleCubeDistance (float3 vec) { #ifdef UNITY_FAST_COHERENT_DYNAMIC_BRANCHING return UnityDecodeCubeShadowDepth(texCUBElod(_ShadowMapTexture, float4(vec, 0))); #else return UnityDecodeCubeShadowDepth(texCUBE(_ShadowMapTexture, vec)); #endif } inline half UnitySampleShadowmap (float3 vec) { float mydist = length(vec) * _LightPositionRange.w; mydist *= 0.97; // bias #if defined (SHADOWS_SOFT) float z = 1.0/128.0; float4 shadowVals; shadowVals.x = SampleCubeDistance (vec+float3( z, z, z)); shadowVals.y = SampleCubeDistance (vec+float3(-z,-z, z)); shadowVals.z = SampleCubeDistance (vec+float3(-z, z,-z)); shadowVals.w = SampleCubeDistance (vec+float3( z,-z,-z)); half4 shadows = (shadowVals < mydist.xxxx) ? _LightShadowData.rrrr : 1.0f; return dot(shadows,0.25); #else float dist = SampleCubeDistance (vec); return dist < mydist ? _LightShadowData.r : 1.0; #endif } #endif // #if defined (SHADOWS_CUBE)
同樣,如果使用軟陰影會采樣四次並取平均值,硬陰影采樣一次。同時沒有進行過濾計算,計算昂貴且效果很粗糙!
5.5 hard vs soft pointLight Shadows
對於點光源陰影實在不能用於手機平台, 替代方式就是用無陰影點光+cookie投射,模擬陰影。或者用較少的聚光燈陰影代替。
6原文
贊原作者!