一、ShadowMap的原理
Shadow Mapping是一種基於影象空間的陰影實現方法,其優點是實現簡單,適應於大型動態場景;缺點是由於shadow map的解析度有限,使得陰影邊緣容易出現鋸齒(Aliasing);以投射陰影的燈光為視點渲染得到深度圖紋理,該紋理就叫Shadow Map;第二個pass從攝像機渲染場景,並將畫素點轉化為在燈光座標系中的深度值,並與Shadow Map中的相應深度值進行比較以確定該畫素是否處於陰影區;經過這兩個pass最終就可以為場景打上陰影。
以平行光為例,在自然照射下,可以看出點A、點D在陰影之外,而點B、點C在陰影之中。其判斷的依據就是由於光線直線傳播的特性,一旦遇到遮擋物體,就會產生陰影,也就是說,在光線方向上不可見的點,就是在陰影中的點。
ShadowMap的實現主要分為兩個pass,第一個pass就是在光源位置建立一個深度攝像機,以光源位置作為視點,將每個畫素點的深度值(z-depth)也就是距離光源最近的物件距離記錄在 Z-buffer 中,生成深度圖( Shadow Map)。
第二個pass從正常攝像機渲染場景,將每個fragment 到光源的距離和 Shadow Map 中儲存的深度值進行比較,如果大於後者則說明被其他物體遮擋處於陰影之中。
二、建立ShadowMap相機
這裡我們可以透過指令碼完成光源相機的建立,shadowmap會根據光源的不同而有所差異,直線光適合使用平行投影,點光源和聚光燈帶有位置資訊,適合使用透視投影。
public Camera CreateLightCamera()
{
GameObject goLightCamera = new GameObject(“Shadow Camera”);
Camera LightCamera = goLightCamera。AddComponent
LightCamera。backgroundColor = Color。black;
LightCamera。clearFlags = CameraClearFlags。SolidColor;
LightCamera。orthographic = true;
LightCamera。orthographicSize = 6f;
LightCamera。nearClipPlane = 0。3f;
LightCamera。farClipPlane = 50;
LightCamera。enabled = false;
if (!LightCamera。targetTexture)
LightCamera。targetTexture = CreateTextureFor(LightCamera);
lightDepthTexture = LightCamera。targetTexture;
Shader。SetGlobalTexture(“_LightDepthTexture”, lightDepthTexture);
return LightCamera;
}
private RenderTexture CreateTextureFor(Camera cam)
{
// RenderTexture rt = new RenderTexture(Screen。width * qulity, Screen。height * qulity, 24, RenderTextureFormat。Default);
RenderTexture rt = new RenderTexture(1024 * qulity,1024 * qulity, 16, RenderTextureFormat。Default);
rt。hideFlags = HideFlags。DontSave;
return rt;
}
Update實時更新光源相機資訊,並存儲深度圖和光源方向的變換矩陣。
void Update () {
// FitToScene;
_lightCamera。transform。parent = lightObj。transform;
_lightCamera。transform。localPosition = Vector3。zero;
_lightCamera。transform。localRotation = new UnityEngine。Quaternion();
Matrix4x4 projectionMatrix = GL。GetGPUProjectionMatrix(_lightCamera。projectionMatrix, true);
Shader。SetGlobalMatrix(“_worldToLightClipMat”, projectionMatrix * _lightCamera。worldToCameraMatrix);
_lightCamera。RenderWithShader(depthMat。shader, “”);
}
三
、陰影計算
在接收陰影的物體上透過全域性屬性獲取變數,並將物體模型空間下的點轉化為光源裁剪空間下的點。
float4 worldPos = mul(unity_ObjectToWorld, v。vertex);
o。lightClipPos = mul(_worldToLightClipMat, worldPos);
取得光源座標系下的深度值。
// 取光源座標系下的深度
float4 scrPos = ComputeScreenPos(i。lightClipPos);
float depTexture = tex2Dproj(_LightDepthTexture, scrPos)。r;
float depth = i。lightClipPos。z / i。lightClipPos。w;
#if UNITY_REVERSED_Z
depth = 1 - depth; //(1, 0)——>(0, 1)
#else
depth = depth * 0。5 + 0。5; //(-1, 1)——>(0, 1)
#endif
將攝像機的深度值和shadow map儲存的深度值相比較,如果攝像機的深度值較大,則為陰影部分。
float shadow = (depTexture)> (depth) ? 1 : (1-_ShadowPower);
四、shadow bias
輸出陰影,可以發現地面陰影出現條紋鋸齒狀,這種現象,就被稱為 Shadow Acne 或者Self-Shadowing。
產生Shadow acne的根本原因就是 shadow depth map 的解析度不夠,因此多個 pixel 會對應 map 上的同一個點。
圖中黃色箭頭是照射的光線,黑色長方形是實際物體表面,黃色的波浪線是 shadow map中的對應值的情況。
可以看到,由於map是對場景的離散取樣,所以黃色的線段呈階梯狀的波浪變化,相對於實際場景中的情況,就有一部分比實際場景中的深度要大(對應著黑色線段部分),著部分不會產生陰影(注意圖畫反了);一部分比實際場景中的深度要小(對應著黃色線段部分),這部分會產生陰影,所以就出現了條紋狀的陰影。由於這種情況,是物體的實際深度,與自己的取樣深度,相比較不相等(實際深度大於取樣深度)導致的,所以是自己(取樣的副本)遮擋了自己(實際的物體),所以被稱為 self shadowing。
解決辦法:只有實際深度大於取樣深度的時候會出現這一現象,那麼在計算實際深度的時候,往燈光方向拉一點,讓他減小一點就可以解決。即給 Shadow map 中儲存的深度值增加一個偏移值(Depth bias)
當我們計算深度加入偏移值時,若偏移值過大,會導致物體計算影子時的深度與實際深度差別太大,使底部陰影無法顯示,導致影子和物體產生了分離。
因此,我們需要控制偏移值的大小在合理的範圍內,這裡基於物體表面和光照的夾角的大小來控制bias,計算公式如下,miniBais+maxBais∗SlopeScaleminiBais+maxBais∗SlopeScale , 其中SlopeScaleSlopeScale可以理解為光線方向與表面法線方向夾角的tan值。
float GetShadowBias(float3 lightDir , float3 normal , float maxBias , float baseBias)
{
float cos_val = saturate(dot(lightDir, normal));
float sin_val = sqrt(1 - cos_val*cos_val); // sin(acos(L·N))
float tan_val = sin_val / cos_val; // tan(acos(L·N))
float bias = baseBias + clamp(tan_val,0 , maxBias) ;
return bias ;
}
//獲得偏移量
float bais=GetShadowBias(worldLightDir,worldNormal,0。002,0。0002);
也可以根據法線方向和光線方向計算:
float bias = max(0。05 * (1。0 - dot(normal, lightDir)), 0。005);
最後將shadowmap外的shadow值設為1,使其不受陰影。
float shadow = (depTexture + bais )> (depth) ? 1 : (1-_ShadowPower);
float2 uv = i。lightClipPos。xy / i。lightClipPos。w;
uv = uv * 0。5 + 0。5; // (-1,1)->(0,1)
if (uv。x > 1 || uv。y > 1 || uv。x < 0 || uv。y < 0)
shadow=1;
最終得到正常的陰影影象。
五、PCF
解決完shadow acne後,放大陰影邊緣就會看到這種鋸齒現象,其主要原因還在於shadow map的解析度。物體多個點會採集深度紋理同一個點進行陰影計算。這個問題一般可以透過濾波緊進行處理,比如多重取樣。
Pencentage close Filtering(PCF)
,最簡單的一種處理方式,當前點是否為陰影區域需要考慮周圍頂點的情況,處理中需要對當前點周圍幾個畫素進行採集,而且這個採集單位越大PCF的效果會越好,當然效能也越差。現在的GPU一般支援2*2的PCF濾波, 也就是Unity設定中的Hard Shadow 。
//PCF濾波
float PercentCloaerFilter(float2 xy , float sceneDepth , float bias)
{
float shadow = 0。0;
float2 texelSize = float2(_TexturePixelWidth,_TexturePixelHeight);
texelSize = 1 / texelSize;
for(int x = -_FilterSize; x <= _FilterSize; ++x)
{
for(int y = -_FilterSize; y <= _FilterSize; ++y)
{
float2 uv_offset = float2(x , y) * texelSize;
float depth = DecodeFloatRGBA(tex2D(_LightDepthTex, xy + uv_offset));
shadow += (sceneDepth - bias > depth ? 1。0 : 0。0);
}
}
float total = (_FilterSize * 2 + 1) * (_FilterSize * 2 + 1);
shadow /= total;
return shadow;
}
參考