Unity Shader系列文章:Unity Shader目錄-初級篇
Unity Shader系列文章:Unity Shader目錄-中級篇
效果:
原理:
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
}
}
}