假陰影,低開銷的陰影實現方式

JeasonBoy發表於2024-07-16

參考: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.旋轉我們可以透過 二維旋轉矩陣 來計算
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
        }
    }
}

相關文章