Unity Shader 前向渲染光照陰影

Unity Shader系列文章:Unity Shader目錄-初級篇

Unity Shader系列文章:Unity Shader目錄-中級篇

效果:
開啟Cast Shadows正方體可以投射陰影,開啟Receive Shadows平面可以接收陰影.jpg
開啟光源的陰影效果.jpg
Mesh Renderer組件的Cast Shadows和Receive Shadows屬性可以控制該物體是否投射和接收陰影.jpg
原理:

Unity中的陰影使用的是ShadowMap的技術(shù)。它會首先把攝像機的位置放在與光源重合的位置上,那么場景中該光源的陰影區(qū)域就是那些攝像機看不到的地方猬膨。

在前向渲染路徑中辈讶,如果場景中最重要的平行光開啟了陰影舔稀,Unity 就會為該光源計算它的陰影映射紋理(shadowmap)频祝。這張陰影映射紋理本質(zhì)上也是一張深度圖, 它記錄了從該光源的位置出發(fā)歌逢、能看到的場景中距離它最近的表面位置(深度信息)。

Unity使用一個額外的Pass來專門更新光源的陰影映射紋理翘狱,這個Pass就是LightMode標簽被設(shè)置為ShadowCaster的Pass秘案。這個Pass的渲染目標不是幀緩存,而是陰影映射紋理(或深度紋理)潦匈。Unity首先把攝像機放置到光源的位置上踏烙,然后調(diào)用該Pass, 通過對頂點變換后得到光源空間下的位置,并據(jù)此來輸出深度信息到陰影映射紋理中历等。因此讨惩,當開啟了光源的陰影效果后,底層渲染引擎首先會在當前渲染物體的Unity Shader中找到LightMode為ShadowCaster的Pass寒屯,如果沒有荐捻,它就會在Fallback指定的Unity Shader中繼續(xù)尋找黍少,如果仍然沒有找到,該物體就無法向其他物體投射陰影(但它仍然可以接收來自其他物體的陰影)处面。當找到了一個LightMode 為ShadowCaster的Pass后厂置,Unity 會使用該Pass來更新光源的陰影映射紋理。

在傳統(tǒng)的陰影映射紋理的實現(xiàn)中魂角,我們會在正常渲染的Pass 中把頂點位置變換到光源空間下昵济,以得到它在光源空間中的三維位置信息。然后野揪,我們使用xy分量對陰影映射紋理進行采樣访忿,得到陰影映射紋理中該位置的深度信息。如果該深度值小于該頂點的深度值(通常由z分量得到)斯稳,那么說明該點位于陰影中海铆。但Unity使用了不同于這種傳統(tǒng)的陰影采樣技術(shù),即屏幕空間的陰影映射技術(shù)(Screenspace Shadow Map)挣惰。屏幕空間的陰影映射原本是延遲渲染中產(chǎn)生陰影的方法卧斟,所以并不是所有的平臺Unity 都會使用這種技術(shù)。這是因為憎茂,屏幕空間的陰影映射需要顯卡支持MRT,而有些移動平臺不支持這種特性珍语。

當使用了屏幕空間的陰影映射技術(shù)時,Unity 首先會通過調(diào)用LightMode為ShadowCaster的Pass來得到可投射陰影的光源的陰影映射紋理以及攝像機的深度紋理竖幔。然后廊酣,根據(jù)光源的陰影映射紋理和攝像機的深度紋理來得到屏幕空間的陰影圖。如果攝像機的深度圖中記錄的表面深度大于轉(zhuǎn)換到陰影映射紋理中的深度值赏枚,就說明該表面雖然是可見的亡驰,但是卻處于該光源的陰影中。
通過這樣的方式饿幅,陰影圖就包含了屏幕空間中所有有陰影的區(qū)域凡辱。如果我們想要一A 個物體接收來自其他物體的陰影,只需要在Shader中對陰影圖進行采樣栗恩。由于陰影圖是屏幕空間下的透乾,因此,我們首先需要把表面坐標從模型空間變換到屏幕空間中磕秤,然后使用這個坐標對陰影圖進行采樣即可乳乌。

總結(jié)一下,一個物體接收來自其他物體的陰影市咆,以及它向其他物體投射陰影是兩個過程:
  • 如果想要一個物體接收來自其他物體的陰影汉操,就必須在Shader中對陰影映射紋理(包括屏幕空間的陰影圖)進行采樣,把采樣結(jié)果和最后的光照結(jié)果相乘來產(chǎn)生陰影效果蒙兰。
  • 如果想要一個物體向其他物體投射陰影, 就必須把該物體加入到光源的陰影映射紋理的計算中磷瘤,從而讓其他物體在對陰影映射紋理采樣時可以得到該物體的相關(guān)信息芒篷。在Unity中,這個過程是通過為該物體執(zhí)行LightMode為ShadowCaster的Pass來實現(xiàn)的采缚。如果使用了屏幕空間的投影映射技術(shù)针炉,Unity 還會使用這個Pass產(chǎn)生一張攝像機的深度紋理。
Shader中計算陰影步驟:

1扳抽、在頂點著色器輸出體中篡帕,聲明一個宏SHADOW_COORDS用于對陰影紋理采樣的坐標;
2贸呢、在頂點著色器中镰烧,使用宏TRANSFER_SHADOW計算聲明的陰影紋理坐標;
3贮尉、在片元著色器中拌滋,使用宏SHADOW_ATTENUATION計算陰影值朴沿。

SHADOW_COORDS猜谚、TRANSFER_SHADOW和SHADOW_ATTENUATION是計算陰影時的"三劍客"。實際上通常情況下計算陰影時赌渣,直接使用宏UNITY_LIGHT_ATTENUATION進行計算陰影值魏铅,同時此宏還會同時計算光照衰減。

Shader代碼:

// 前向渲染坚芜,陰影
Shader "Unlit/ForwardRenderingShadow"
{
    Properties
    {
        _Diffuse ("Diffuse", Color) = (1, 1, 1, 1) // 漫反射顏色
        _Specular ("Specular", Color) = (1, 1, 1, 1) // 高光反射顏色
        _Gloss ("Gloss", Range(8, 256)) = 20 // 高光區(qū)域大小
    }

    SubShader
    {
        Tags { "RenderType" = "Opaque" }

        // Pass1
        // Shadow Pass 將該物體加入到光源的陰影映射紋理的計算中览芳。通常此Pass不需要寫,
        // 由于這個Pass功能通常是在多個Shader中通用鸿竖,因此一般直接通過FallBack一個Unity內(nèi)置Specular來完成沧竟,
        // 即: FallBack "Specular",因為Specular會繼續(xù)FallBack回調(diào)內(nèi)置的VertexLit
        pass
        {
            Name "ShadowCaster"
            Tags { "LightMode" = "ShadowCaster" }
            
            CGPROGRAM
            
            #pragma vertex vert
            #pragma fragment frag

            #pragma multi_compile_shadowcaster
            
            #include "UnityCG.cginc"

            struct v2f
            {
                V2F_SHADOW_CASTER;
            };

            v2f vert(appdata_base v)
            {
                v2f o;
                TRANSFER_SHADOW_CASTER_NORMALOFFSET(o);
                return o;
            }
            float4 frag(v2f i): SV_Target
            {
                SHADOW_CASTER_FRAGMENT(i);
            }
            ENDCG
            
        }
        
        // Pass2
        // Base Pass計算平行光缚忧、環(huán)境光
        pass
        {
            Tags { "LightMode" = "ForwardBase" }
            
            CGPROGRAM
            
            #pragma multi_compile_fwdbase

            #pragma vertex vert
            #pragma fragment frag

            #include "Lighting.cginc"
            #include "AutoLight.cginc"

            fixed4 _Diffuse;
            fixed4 _Specular;
            float _Gloss;

            // 應(yīng)用傳遞給頂點著色器的數(shù)據(jù)
            struct a2v
            {
                float4 vertex: POSITION; // 語義:頂點坐標
                float3 normal: NORMAL; // 語義:法線
            };

            // 頂點著色器傳遞給片元著色器的數(shù)據(jù)
            struct v2f
            {
                float4 pos: SV_POSITION; // 語義:裁剪空間的頂點坐標
                float3 worldNormal: TEXCOORD0;
                float3 worldPos: TEXCOORD1;
                SHADOW_COORDS(2) // 內(nèi)置宏:聲明一個用于對陰影紋理采樣的坐標 (這個宏參數(shù)需要是下一個可用的插值寄存器的索引值悟泵,這里是2)
            };

            v2f vert(a2v v)
            {
                v2f o;
                // 將頂點坐標從模型空間變換到裁剪空間
                // 等價于o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
                o.pos = UnityObjectToClipPos(v.vertex);

                // 將法線從模型空間變換到世界空間
                // 等價于o.worldNormal = mul(v.normal, (float3x3)unity_WorldToObject);
                o.worldNormal = UnityObjectToWorldNormal(v.normal);

                // 將頂點坐標從模型空間變換到世界空間
                o.worldPos = mul(unity_ObjectToWorld, v.vertex);

                // 內(nèi)置宏:用于計算聲明的陰影紋理坐標
                TRANSFER_SHADOW(o);

                return o;
            }

            // 片元著色器
            fixed4 frag(v2f i): SV_TARGET
            {
                fixed3 worldNormal = normalize(i.worldNormal);

                // 世界空間光向量
                fixed3 worldLightDir = normalize(UnityWorldSpaceLightDir(i.worldPos));

                // 計算漫反射顏色
                // 蘭伯特公式:Id = Ip * Kd * N * L
                // IP:入射光的光顏色;
                // Kd:漫反射顏色闪水;
                // N:單位法向量糕非,L:單位光向量
                fixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb * max(0, dot(worldNormal, worldLightDir));

                // 世界空間觀察向量
                // 等價于fixed3 viewDir = normalize(_WorldSpaceCameraPos.xyz - i.worldPos)
                fixed3 viewDir = UnityWorldSpaceViewDir(i.worldPos.xyz);
                // 半角向量: h = (V + L)/(| V + L |)
                fixed3 halfDir = normalize(worldLightDir + viewDir);
                // 計算高光反射
                // Blinn-Phong高光反射公式:
                // Cspecular=(Clight * Mspecular) * max(0,n.h)^mgloss
                // Clight:入射光顏色;
                // Mspecular:高光反射顏色球榆;
                // n: 單位法向量朽肥;
                // h: 半角向量:光線和視線夾角一半方向上的單位向量 h = (V + L)/(| V + L |)
                // mgloss:反射系數(shù);
                fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(max(0, dot(worldNormal, halfDir)), _Gloss);

                // 環(huán)境光
                fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;

                // 平行光無光照衰減
                fixed atten = 1.0;

                // 計算陰影值
                fixed shadow = SHADOW_ATTENUATION(i);

                return fixed4(ambient + (diffuse + specular) * atten * shadow, 1.0);
            }
            
            ENDCG
            
        }

        // Pass3
        // Add Pass 計算額外的逐像素光源(點光源持钉、聚光燈等), 每個pass對應(yīng)1個光源
        pass
        {
            Tags { "LightMode" = "ForwardAdd" }

            // 開啟混合
            Blend One One
            
            CGPROGRAM
            
            // 若使用fullshadows衡招,則Unity會為這些額外逐像素光源計算陰影
            // #pragma multi_compile_fwdadd_fullshadows
            #pragma multi_compile_fwdadd

            #pragma vertex vert
            #pragma fragment frag

            #include "Lighting.cginc"
            #include "AutoLight.cginc"

            fixed4 _Diffuse;
            fixed4 _Specular;
            float _Gloss;

            // 應(yīng)用傳遞給頂點著色器的數(shù)據(jù)
            struct a2v
            {
                float4 vertex: POSITION; // 語義:頂點坐標
                float3 normal: NORMAL; // 語義:法線
            };

            // 頂點著色器傳遞給片元著色器的數(shù)據(jù)
            struct v2f
            {
                float4 pos: SV_POSITION; // 語義:裁剪空間的頂點坐標
                float3 worldNormal: TEXCOORD0;
                float3 worldPos: TEXCOORD1;
            };

            // 頂點著色器
            v2f vert(a2v v)
            {
                v2f o;

                // 將頂點坐標從模型空間變換到裁剪空間
                // 等價于o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
                o.pos = UnityObjectToClipPos(v.vertex);

                // 將法線從模型空間變換到世界空間
                // 等價于o.worldNormal = mul(v.normal, (float3x3)unity_WorldToObject);
                o.worldNormal = UnityObjectToWorldNormal(v.normal);

                // 將頂點坐標從模型空間變換到世界空間
                o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;

                return o;
            }

            // 片元著色器
            fixed4 frag(v2f i): SV_TARGET
            {
                fixed3 worldNormal = normalize(i.worldNormal);

                // 世界空間光向量
                fixed3 worldLightDir = normalize(UnityWorldSpaceLightDir(i.worldPos));

                // 計算漫反射顏色
                // 蘭伯特公式:Id = Ip * Kd * N * L
                // IP:入射光的光顏色;
                // Kd:漫反射顏色每强;
                // N:單位法向量蚁吝,L:單位光向量
                fixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb * max(0, dot(worldNormal, worldLightDir));

                // 世界空間觀察向量
                // 等價于fixed3 viewDir = normalize(_WorldSpaceCameraPos.xyz - i.worldPos)
                fixed3 viewDir = UnityWorldSpaceViewDir(i.worldPos);
                // 半角向量: h = (V + L)/(| V + L |)
                fixed3 halfDir = normalize(worldLightDir + viewDir);
                // 計算高光反射
                // Blinn-Phong高光反射公式:
                // Cspecular=(Clight * Mspecular) * max(0,n.h)^mgloss
                // Clight:入射光顏色旱爆;
                // Mspecular:高光反射顏色;
                // n: 單位法向量窘茁;
                // h: 半角向量:光線和視線夾角一半方向上的單位向量 h = (V + L)/(| V + L |)
                // mgloss:反射系數(shù)怀伦;
                fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(max(0, dot(worldNormal, halfDir)), _Gloss);

                // 計算光照衰減 (一般都直接用Unity內(nèi)置函數(shù)計算: UNITY_LIGHT_ATTENUATION,會在后續(xù)文章中用到)
                #ifdef USING_DIRECTIONAL_LIGHT // 平行光
                    // 平行光山林,光照衰減不變
                    fixed atten = 1.0;
                #else
                    #if defined(POINT) // 點光源
                        // 把頂點坐標從世界空間變換到點光源坐標空間中
                        // unity_WorldToLight由引擎代碼計算后傳遞到shader中房待,這里包含了對點光源范圍的計算,具體可參考Unity引擎源碼驼抹。
                        // 經(jīng)過unity_WorldToLight變換后桑孩,在點光源中心處lightCoord為(0, 0, 0),在點光源的范圍邊緣處lightCoord模為1框冀。
                        float3 lightCoord = mul(unity_WorldToLight, float4(i.worldPos, 1)).xyz;
                        // 使用點到光源中心距離的平方dot(lightCoord, lightCoord)構(gòu)成二維采樣坐標(r,r)流椒,對衰減紋理_LightTexture0采樣。
                        // UNITY_ATTEN_CHANNEL是衰減值所在的紋理通道明也,可以在內(nèi)置的HLSLSupport.cginc文件中查看宣虾。
                        // 一般PC和主機平臺的話UNITY_ATTEN_CHANNEL是r通道,移動平臺的話是a通道温数。
                        fixed atten = tex2D(_LightTexture0, dot(lightCoord, lightCoord).rr).UNITY_ATTEN_CHANNEL;
                    #elif defined(SPOT) // 聚光燈
                        // 把頂點坐標從世界空間變換到點光源坐標空間中
                        // unity_WorldToLight由引擎代碼計算后傳遞到shader中绣硝,這里面包含了對聚光燈的范圍、角度的計算撑刺,具體可參考Unity引擎源碼鹉胖。
                        // 經(jīng)過unity_WorldToLight變換后,在聚光燈光源中心處或聚光燈范圍外的lightCoord為(0, 0, 0)够傍,在聚光燈光源的范圍邊緣處lightCoord模為1甫菠。
                        float4 lightCoord = mul(unity_WorldToLight, float4(i.worldPos, 1));
                        // 與點光源不同,由于聚光燈有更多的角度等要求冕屯,因此為了得到衰減值寂诱,除了需要對衰減紋理采樣外,還需要對聚光燈的范圍愕撰、張角和方向進行判斷刹衫。
                        // 此時衰減紋理存儲到了_LightTextureB0中,這張紋理和點光源中的_LightTexture0是等價的搞挣。
                        // 聚光燈的_LightTexture0存儲的不再是基于距離的衰減紋理带迟,而是一張基于張角范圍的衰減紋理。在張角中心囱桨,即坐標0.5處衰減值為1仓犬,而在兩側(cè)是接近0的。
                        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

                // 盡管紋理采樣方法可以減少計算衰減時的復(fù)雜度舍肠,有時也可以使用數(shù)學公式計算光照衰減:
                // float distance = length(_WorldSpaceLightPos0.xyz - i.worldPos.xyz);
                // float atten = 1.0 / distance;

                return fixed4((diffuse + specular) * atten, 1.0);
            }
            
            ENDCG
            
        }
    }
}

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末搀继,一起剝皮案震驚了整個濱河市窘面,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌叽躯,老刑警劉巖财边,帶你破解...
    沈念sama閱讀 218,386評論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異点骑,居然都是意外死亡酣难,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,142評論 3 394
  • 文/潘曉璐 我一進店門黑滴,熙熙樓的掌柜王于貴愁眉苦臉地迎上來憨募,“玉大人,你說我怎么就攤上這事袁辈〔艘ィ” “怎么了?”我有些...
    開封第一講書人閱讀 164,704評論 0 353
  • 文/不壞的土叔 我叫張陵晚缩,是天一觀的道長尾膊。 經(jīng)常有香客問我,道長橡羞,這世上最難降的妖魔是什么眯停? 我笑而不...
    開封第一講書人閱讀 58,702評論 1 294
  • 正文 為了忘掉前任济舆,我火速辦了婚禮卿泽,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘滋觉。我一直安慰自己签夭,他們只是感情好,可當我...
    茶點故事閱讀 67,716評論 6 392
  • 文/花漫 我一把揭開白布椎侠。 她就那樣靜靜地躺著第租,像睡著了一般。 火紅的嫁衣襯著肌膚如雪我纪。 梳的紋絲不亂的頭發(fā)上慎宾,一...
    開封第一講書人閱讀 51,573評論 1 305
  • 那天,我揣著相機與錄音浅悉,去河邊找鬼趟据。 笑死,一個胖子當著我的面吹牛术健,可吹牛的內(nèi)容都是我干的汹碱。 我是一名探鬼主播,決...
    沈念sama閱讀 40,314評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼荞估,長吁一口氣:“原來是場噩夢啊……” “哼咳促!你這毒婦竟也來了稚新?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,230評論 0 276
  • 序言:老撾萬榮一對情侶失蹤跪腹,失蹤者是張志新(化名)和其女友劉穎褂删,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體冲茸,經(jīng)...
    沈念sama閱讀 45,680評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡笤妙,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,873評論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了噪裕。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片蹲盘。...
    茶點故事閱讀 39,991評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖膳音,靈堂內(nèi)的尸體忽然破棺而出召衔,到底是詐尸還是另有隱情,我是刑警寧澤祭陷,帶...
    沈念sama閱讀 35,706評論 5 346
  • 正文 年R本政府宣布苍凛,位于F島的核電站,受9級特大地震影響兵志,放射性物質(zhì)發(fā)生泄漏醇蝴。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,329評論 3 330
  • 文/蒙蒙 一想罕、第九天 我趴在偏房一處隱蔽的房頂上張望悠栓。 院中可真熱鬧,春花似錦按价、人聲如沸惭适。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,910評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽癞志。三九已至,卻和暖如春框产,著一層夾襖步出監(jiān)牢的瞬間凄杯,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,038評論 1 270
  • 我被黑心中介騙來泰國打工秉宿, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留戒突,地道東北人。 一個月前我還...
    沈念sama閱讀 48,158評論 3 370
  • 正文 我出身青樓蘸鲸,卻偏偏與公主長得像妖谴,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 44,941評論 2 355

推薦閱讀更多精彩內(nèi)容