參考:Unity無光照假陰影Shader實現及常見問題總結 - 簡書 (jianshu.com)
遊戲實現陰影的常見處理方式 (動態人或物,非烘焙)
1.實時光照
實時光照屬於真陰影,一般來說效果是最好的,但是開銷也是最大的。 Shadow Map(陰影貼圖)跟Soft Shadows(軟陰影) - JeasonBoy - 部落格園 (cnblogs.com)
2.腳底放置陰影面片模擬陰影
一般是無光照小型遊戲的常見解決方案,開銷較小,表現形式較差,面片是死的,無法根據人物動作變化
3.透過頂點shader變換成面片模擬陰影
如上圖所示
優點 : 表現形式上比方案2強,陰影可跟隨頂點動畫,開銷比實時陰影要少
缺點 : 無法在 "非平面" 使用,比如在斜坡上,會穿幫
4.透過 Projector 或者 Decal 來模擬投射陰影
優點 : 表現效果更近一步,也可以在斜面上進行投影了
缺點 : 開銷也更近一步
方式3實現思路
1.我們透過2個Pass來渲染,第二個Pass正常渲染角色,第一個Pass模擬渲染陰影
2.我們需要將模型的所有 Y 值壓到地面高度,這樣就形成了一個頭頂俯檢視的陰影效果
3.我們再對 XZ 方向進行偏移,偏移量根據模型原先 Y 值高度為參考做插值
4.陰影的方向我們規定在 XZ 平面上 (X=0,Z=1) 為初始預設方向,以這個向量為基準進行旋轉
5.旋轉我們可以透過 二維旋轉矩陣 來計算
1.我們透過2個Pass來渲染,第二個Pass正常渲染角色,第一個Pass模擬渲染陰影
2.我們需要將模型的所有 Y 值壓到地面高度,這樣就形成了一個頭頂俯檢視的陰影效果
3.我們再對 XZ 方向進行偏移,偏移量根據模型原先 Y 值高度為參考做插值
4.陰影的方向我們規定在 XZ 平面上 (X=0,Z=1) 為初始預設方向,以這個向量為基準進行旋轉
5.旋轉我們可以透過 二維旋轉矩陣 來計算
Shader程式碼
Shader "loom/fake_shadow_test_pass_order" { Properties { //材質屬性皮膚 _MainTex ("主貼圖",2D) = "white"{} _GroundY ("地面Y高度 (外部傳入)",float) = 0 _Shadow_Color("影子顏色",Color) = (1,1,1,1) _Shadow_Length("影子長度",float) = 0 _Shadow_Rotated("影子旋轉角度",range(0,360)) = 0 } SubShader { Tags { "Queue" = "Geometry+1" //注意這裡很重要,因為影子是要繪製在地面上,所以地面必須應該先繪製,否則blend混合的時候就是和背後的skybox進行混合了 } pass { Stencil{ Ref 1 //Comp取值依次為 0:Disabled 1:Never 2:Less 3:Equal 4:LessEqual 5:Greater 6:NotEqual 7:GreaterEqual 8:Always Comp Greater //或者改成NotEqual //Pass取值依次為 0:Keep 1:Zero 2:Replace 3:IncrementSaturate 4:DecrementSaturate 5:Invert 6:IncrementWrap 7:DecrementWrap Pass Replace } Blend SrcAlpha oneMinusSrcAlpha //因為和地面重疊所以做個偏移 //也可以不做偏移,將傳入的地面高度抬高一點即可 Offset -2,-2 CGPROGRAM #pragma vertex vert #pragma fragment frag #include "UnityCG.cginc" struct appdata { float4 vertex : POSITION; }; struct v2f { float4 pos : SV_POSITION; //這裡worldPos一定是float4,因為vert()中實際是手動兩次空間變換如果是float3會導致w分量丟失,透視除法會出錯 //如果不參與變換,只是傳到frag()中使用的話,比如進行Blinn-Phong光照計算V向量那麼float3就夠了 float4 worldPos : TEXCOORD0; //做陰影插值和Clip地面以下陰影用 float cacheWorldY : TEXCOORD1; }; half _GroundY; half4 _Shadow_Color; half _Shadow_Length; half _Shadow_Rotated; v2f vert(appdata v) { v2f o = (v2f)0; //獲取世界空間的位置 o.worldPos = mul(unity_ObjectToWorld,v.vertex); //快取世界空間下的y分量,後續兩點作用 //第一點 : 做插值用做計算xz的偏移量的多少 //第二點 : 防止在地面以下 o.cacheWorldY = o.worldPos.y; //設定世界空間下y的值全部都設定為傳入的地面高度值 o.worldPos.y = _GroundY; //根據世界空間下模型y值減去傳入的地面高度值_GroundY //以這個值為傳入 lerp(0,_Shadow_Length) 進行線性插值 //最後獲取到模型y值由低到高的插值lerpVal //這個max()函式 假設腿部在地面以下則裁切掉腿部陰影,後續使用clip後無需Max //half lerpVal = lerp(0,_Shadow_Length,max(0,o.cacheWorldY-_GroundY)); half lerpVal = lerp(0,_Shadow_Length,o.cacheWorldY-_GroundY); //常量PI //const float PI = 3.14159265; //角度轉換成弧度 half radian = _Shadow_Rotated / 180.0 * UNITY_PI; //旋轉矩陣,對(0,1)向量進行旋轉,計算旋轉後的向量,該向量就是陰影方向 //2D旋轉矩陣如下 // [x] [ cosθ , -sinθ ] // [ ] 乘以 // [y] [ sinθ , cosθ ] // x' = xcosθ - ysinθ // y' = xsinθ + ycosθ half2 ratatedAngle = half2((0*cos(radian)-1*sin(radian)),(0*sin(radian)+1*cos(radian))); //用以y軸高度為參考計算的插值 lerpVal 去 乘以一個旋轉後的方向向量,作為陰影的方向 //最終得到偏移後的陰影位置 o.worldPos.xz += lerpVal * ratatedAngle; //變換到裁剪空間 o.pos = mul(UNITY_MATRIX_VP,o.worldPos); return o; } fixed4 frag(v2f i) : SV_TARGET { //剔除低於地面部分的片段 clip(i.cacheWorldY - _GroundY); //用作陰影的Pass直接輸出顏色即可 return _Shadow_Color; } ENDCG } pass { CGPROGRAM #pragma vertex vert #pragma fragment frag #include "UnityCG.cginc" sampler2D _MainTex;half4 _MainTex_ST; struct appdata{ float4 vertex : POSITION; float2 uv0 : TEXCOORD0; }; struct v2f{ float4 pos : SV_POSITION; float2 uv : TEXCOORD0; }; v2f vert(appdata v) { v2f o = (v2f)0; o.pos = UnityObjectToClipPos(v.vertex); o.uv = TRANSFORM_TEX(v.uv0,_MainTex); return o; } fixed4 frag(v2f i) : SV_TARGET { return tex2D(_MainTex,i.uv); } ENDCG } } }