一、ShadowMap的原理

Shadow Mapping是一種基於影象空間的陰影實現方法,其優點是實現簡單,適應於大型動態場景;缺點是由於shadow map的解析度有限,使得陰影邊緣容易出現鋸齒(Aliasing);以投射陰影的燈光為視點渲染得到深度圖紋理,該紋理就叫Shadow Map;第二個pass從攝像機渲染場景,並將畫素點轉化為在燈光座標系中的深度值,並與Shadow Map中的相應深度值進行比較以確定該畫素是否處於陰影區;經過這兩個pass最終就可以為場景打上陰影。

以平行光為例,在自然照射下,可以看出點A、點D在陰影之外,而點B、點C在陰影之中。其判斷的依據就是由於光線直線傳播的特性,一旦遇到遮擋物體,就會產生陰影,也就是說,在光線方向上不可見的點,就是在陰影中的點。

【Unity Shader】自定義陰影ShadowMap(一)

ShadowMap的實現主要分為兩個pass,第一個pass就是在光源位置建立一個深度攝像機,以光源位置作為視點,將每個畫素點的深度值(z-depth)也就是距離光源最近的物件距離記錄在 Z-buffer 中,生成深度圖( Shadow Map)。

第二個pass從正常攝像機渲染場景,將每個fragment 到光源的距離和 Shadow Map 中儲存的深度值進行比較,如果大於後者則說明被其他物體遮擋處於陰影之中。

【Unity Shader】自定義陰影ShadowMap(一)

二、建立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。

【Unity Shader】自定義陰影ShadowMap(一)

產生Shadow acne的根本原因就是 shadow depth map 的解析度不夠,因此多個 pixel 會對應 map 上的同一個點。

【Unity Shader】自定義陰影ShadowMap(一)

圖中黃色箭頭是照射的光線,黑色長方形是實際物體表面,黃色的波浪線是 shadow map中的對應值的情況。

可以看到,由於map是對場景的離散取樣,所以黃色的線段呈階梯狀的波浪變化,相對於實際場景中的情況,就有一部分比實際場景中的深度要大(對應著黑色線段部分),著部分不會產生陰影(注意圖畫反了);一部分比實際場景中的深度要小(對應著黃色線段部分),這部分會產生陰影,所以就出現了條紋狀的陰影。由於這種情況,是物體的實際深度,與自己的取樣深度,相比較不相等(實際深度大於取樣深度)導致的,所以是自己(取樣的副本)遮擋了自己(實際的物體),所以被稱為 self shadowing。

解決辦法:只有實際深度大於取樣深度的時候會出現這一現象,那麼在計算實際深度的時候,往燈光方向拉一點,讓他減小一點就可以解決。即給 Shadow map 中儲存的深度值增加一個偏移值(Depth bias)

【Unity Shader】自定義陰影ShadowMap(一)

當我們計算深度加入偏移值時,若偏移值過大,會導致物體計算影子時的深度與實際深度差別太大,使底部陰影無法顯示,導致影子和物體產生了分離。

【Unity Shader】自定義陰影ShadowMap(一)

因此,我們需要控制偏移值的大小在合理的範圍內,這裡基於物體表面和光照的夾角的大小來控制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;

最終得到正常的陰影影象。

【Unity Shader】自定義陰影ShadowMap(一)

五、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;

}

參考