解讀unity內置的軟陰影處理方式:
參考網址:
https://blog.csdn.net/cgy56191948/article/details/105726682
https://blog.csdn.net/weixin_45776473/article/details/119582218
https://tajourney.games/5482/
上面的博客已經論述了,為何出現鋸齒,并從連續性角度,論述了pcf解決方案,使得陰影能夠軟。
本文主要是針對算法的實現上,剖析其實現的細節。
* PCF tent shadowmap filtering based on a 3x3 kernel (optimized with 4 taps)
*/
half UnitySampleShadowmap_PCF3x3Tent(float4 coord, float3 receiverPlaneDepthBias)
{half shadow = 1;#ifdef SHADOWMAPSAMPLER_AND_TEXELSIZE_DEFINED#ifndef SHADOWS_NATIVE// when we don't have hardware PCF sampling, fallback to a simple 3x3 sampling with averaged results.return UnitySampleShadowmap_PCF3x3NoHardwareSupport(coord, receiverPlaneDepthBias);#endif// tent base is 3x3 base thus covering from 9 to 12 texels, thus we need 4 bilinear PCF fetchesfloat2 tentCenterInTexelSpace = coord.xy * _ShadowMapTexture_TexelSize.zw;float2 centerOfFetchesInTexelSpace = floor(tentCenterInTexelSpace + 0.5);float2 offsetFromTentCenterToCenterOfFetches = tentCenterInTexelSpace - centerOfFetchesInTexelSpace;// find the weight of each texel basedfloat4 texelsWeightsU, texelsWeightsV;_UnityInternalGetWeightPerTexel_3TexelsWideTriangleFilter(offsetFromTentCenterToCenterOfFetches.x, texelsWeightsU);_UnityInternalGetWeightPerTexel_3TexelsWideTriangleFilter(offsetFromTentCenterToCenterOfFetches.y, texelsWeightsV);// each fetch will cover a group of 2x2 texels, the weight of each group is the sum of the weights of the texelsfloat2 fetchesWeightsU = texelsWeightsU.xz + texelsWeightsU.yw;float2 fetchesWeightsV = texelsWeightsV.xz + texelsWeightsV.yw;// move the PCF bilinear fetches to respect texels weightsfloat2 fetchesOffsetsU = texelsWeightsU.yw / fetchesWeightsU.xy + float2(-1.5,0.5);float2 fetchesOffsetsV = texelsWeightsV.yw / fetchesWeightsV.xy + float2(-1.5,0.5);fetchesOffsetsU *= _ShadowMapTexture_TexelSize.xx;fetchesOffsetsV *= _ShadowMapTexture_TexelSize.yy;// fetch !float2 bilinearFetchOrigin = centerOfFetchesInTexelSpace * _ShadowMapTexture_TexelSize.xy;shadow = fetchesWeightsU.x * fetchesWeightsV.x * UNITY_SAMPLE_SHADOW(_ShadowMapTexture, UnityCombineShadowcoordComponents(bilinearFetchOrigin, float2(fetchesOffsetsU.x, fetchesOffsetsV.x), coord.z, receiverPlaneDepthBias));shadow += fetchesWeightsU.y * fetchesWeightsV.x * UNITY_SAMPLE_SHADOW(_ShadowMapTexture, UnityCombineShadowcoordComponents(bilinearFetchOrigin, float2(fetchesOffsetsU.y, fetchesOffsetsV.x), coord.z, receiverPlaneDepthBias));shadow += fetchesWeightsU.x * fetchesWeightsV.y * UNITY_SAMPLE_SHADOW(_ShadowMapTexture, UnityCombineShadowcoordComponents(bilinearFetchOrigin, float2(fetchesOffsetsU.x, fetchesOffsetsV.y), coord.z, receiverPlaneDepthBias));shadow += fetchesWeightsU.y * fetchesWeightsV.y * UNITY_SAMPLE_SHADOW(_ShadowMapTexture, UnityCombineShadowcoordComponents(bilinearFetchOrigin, float2(fetchesOffsetsU.y, fetchesOffsetsV.y), coord.z, receiverPlaneDepthBias));
#endifreturn shadow;
}
傳入的參數:float4 coord是(0,1)陰影貼圖的紋理坐標值。
接下來說明三角形面積的計算方式:
如上圖所示,紅色的p點為陰影處的點。我們要對它進行軟陰影處理。
情況1)三角形偏移為0時,三角形面積被分為4個部分的面積情況。
三角形的底為3,高為1.5,之所以使用這樣的三角形,因為計算簡單。
紅色面積:1/8
橙色面積:1
綠色面積:1
藍色面積:1/8
情況2)三角形偏移為-0.5時,三角形面積被分為4個部分的面積情況。
紅色面積:0.5
橙色面積:1.25
綠色面積:0.5
藍色面積:0
情況3)三角形偏移為0.5時,三角形面積被分為4個部分的面積情況
紅色面積:0
橙色面積:0.5
綠色面積:1.25
藍色面積:0.5
然后推廣到更一般的情況,給定偏移offset,計算三角形被分割的四個部分的面積是多少,公式為:
對應的這段代碼是:
// ------------------------------------------------------------------
// PCF Filtering helpers
// ------------------------------------------------------------------/**
* Assuming a isoceles rectangle triangle of height "triangleHeight" (as drawn below).
* This function return the area of the triangle above the first texel.
*
* |\ <-- 45 degree slop isosceles rectangle triangle
* | \
* ---- <-- length of this side is "triangleHeight"
* _ _ _ _ <-- texels
*/
float _UnityInternalGetAreaAboveFirstTexelUnderAIsocelesRectangleTriangle(float triangleHeight)
{return triangleHeight - 0.5;
}/**
* Assuming a isoceles triangle of 1.5 texels height and 3 texels wide lying on 4 texels.
* This function return the area of the triangle above each of those texels.
* | <-- offset from -0.5 to 0.5, 0 meaning triangle is exactly in the center
* / \ <-- 45 degree slop isosceles triangle (ie tent projected in 2D)
* / \
* _ _ _ _ <-- texels
* X Y Z W <-- result indices (in computedArea.xyzw and computedAreaUncut.xyzw)
*/
void _UnityInternalGetAreaPerTexel_3TexelsWideTriangleFilter(float offset, out float4 computedArea, out float4 computedAreaUncut)
{//Compute the exterior areasfloat offset01SquaredHalved = (offset + 0.5) * (offset + 0.5) * 0.5;computedAreaUncut.x = computedArea.x = offset01SquaredHalved - offset;computedAreaUncut.w = computedArea.w = offset01SquaredHalved;//Compute the middle areas//For Y : We find the area in Y of as if the left section of the isoceles triangle would//intersect the axis between Y and Z (ie where offset = 0).computedAreaUncut.y = _UnityInternalGetAreaAboveFirstTexelUnderAIsocelesRectangleTriangle(1.5 - offset);//This area is superior to the one we are looking for if (offset < 0) thus we need to//subtract the area of the triangle defined by (0,1.5-offset), (0,1.5+offset), (-offset,1.5).float clampedOffsetLeft = min(offset,0);float areaOfSmallLeftTriangle = clampedOffsetLeft * clampedOffsetLeft;computedArea.y = computedAreaUncut.y - areaOfSmallLeftTriangle;//We do the same for the Z but with the right part of the isoceles trianglecomputedAreaUncut.z = _UnityInternalGetAreaAboveFirstTexelUnderAIsocelesRectangleTriangle(1.5 + offset);float clampedOffsetRight = max(offset,0);float areaOfSmallRightTriangle = clampedOffsetRight * clampedOffsetRight;computedArea.z = computedAreaUncut.z - areaOfSmallRightTriangle;
}
這個公式后面會給出詳細的證明。
然后計算四個劃分的面積占總面積的多少,由于等腰直角三角形的底是3,高為1.5,所以面積是9/4,求權重比,對應的代碼是:
/*** Assuming a isoceles triangle of 1.5 texels height and 3 texels wide lying on 4 texels.* This function return the weight of each texels area relative to the full triangle area.*/
void _UnityInternalGetWeightPerTexel_3TexelsWideTriangleFilter(float offset, out float4 computedWeight)
{float4 dummy;_UnityInternalGetAreaPerTexel_3TexelsWideTriangleFilter(offset, computedWeight, dummy);computedWeight *= 0.44444;//0.44 == 1/(the triangle area)
}
此時得到了水平方向的四個權重,然后我們同樣的方法得到垂直方向的四個權重,然后咋辦?
我們可以把x和y兩個看成連續的塊,然后把z和w看成連續的塊,然后對應代碼:
// each fetch will cover a group of 2x2 texels, the weight of each group is the sum of the weights of the texelsfloat2 fetchesWeightsU = texelsWeightsU.xz + texelsWeightsU.yw;
于是fetchesWeightU.x = texelsWeightsU.x + texelsWeightsU.y
fetchesWeightU.y = texelsWeightsU.z + texelsWeightsU.w
然后,就是計算y占(x+y)的百分比,w占(z+w)的百分比
// move the PCF bilinear fetches to respect texels weightsfloat2 fetchesOffsetsU = texelsWeightsU.yw / fetchesWeightsU.xy + float2(-1.5,0.5);
fetchesOffsetsU.x = texelsWeightsU.y / (texelsWeightsU.x + texelsWeightsU.y)
fetchesOffsetsU.y = texelsWeightsU.w / (texelsWeightsU.z + texelsWeightsU.w)
這樣就得到的fetchesOffsetsU,是大于等于0的偏移,所以為了得到相對的偏移得歸一化到起點。
如上圖粉色框框起來的x和y點,其水平起點為三角形的底/2,取負,所以是-1.5
而又因為是相鄰每兩個像素的歸一化,所以綠色框住的起點是-1.5+2=0.5,于是得到上面的偏移:+float2(-1.5,0.5);
同樣的垂直方向上也是類似的計算方式,最終我們得到了:
// move the PCF bilinear fetches to respect texels weightsfloat2 fetchesOffsetsU = texelsWeightsU.yw / fetchesWeightsU.xy + float2(-1.5,0.5);float2 fetchesOffsetsV = texelsWeightsV.yw / fetchesWeightsV.xy + float2(-1.5,0.5);
然后我們將轉換到(0,1)的范圍,直接乘以:
fetchesOffsetsU *= _ShadowMapTexture_TexelSize.xx;fetchesOffsetsV *= _ShadowMapTexture_TexelSize.yy;
這里的_ShadowMapTexture_TexelSize的x、y、z、w分量分別是陰影貼圖的水平像素個數倒數,垂直像素個數倒數,水平像素個數,垂直像素個數。
然后就是去采樣了:
// fetch !float2 bilinearFetchOrigin = centerOfFetchesInTexelSpace * _ShadowMapTexture_TexelSize.xy;shadow = fetchesWeightsU.x * fetchesWeightsV.x * UNITY_SAMPLE_SHADOW(_ShadowMapTexture, UnityCombineShadowcoordComponents(bilinearFetchOrigin, float2(fetchesOffsetsU.x, fetchesOffsetsV.x), coord.z, receiverPlaneDepthBias));shadow += fetchesWeightsU.y * fetchesWeightsV.x * UNITY_SAMPLE_SHADOW(_ShadowMapTexture, UnityCombineShadowcoordComponents(bilinearFetchOrigin, float2(fetchesOffsetsU.y, fetchesOffsetsV.x), coord.z, receiverPlaneDepthBias));shadow += fetchesWeightsU.x * fetchesWeightsV.y * UNITY_SAMPLE_SHADOW(_ShadowMapTexture, UnityCombineShadowcoordComponents(bilinearFetchOrigin, float2(fetchesOffsetsU.x, fetchesOffsetsV.y), coord.z, receiverPlaneDepthBias));shadow += fetchesWeightsU.y * fetchesWeightsV.y * UNITY_SAMPLE_SHADOW(_ShadowMapTexture, UnityCombineShadowcoordComponents(bilinearFetchOrigin, float2(fetchesOffsetsU.y, fetchesOffsetsV.y), coord.z, receiverPlaneDepthBias));
#endif
float2 bilinearFetchOrigin = centerOfFetchesInTexelSpace * _ShadowMapTexture_TexelSize.xy;
是p點,就是要處理的點。
然后就是:
UnityCombineShadowcoordComponents(bilinearFetchOrigin, float2(fetchesOffsetsU.x, fetchesOffsetsV.x), coord.z, receiverPlaneDepthBias)
這個函數
/**
* Combines the different components of a shadow coordinate and returns the final coordinate.
* See UnityGetReceiverPlaneDepthBias
*/
float3 UnityCombineShadowcoordComponents(float2 baseUV, float2 deltaUV, float depth, float3 receiverPlaneDepthBias)
{float3 uv = float3(baseUV + deltaUV, depth + receiverPlaneDepthBias.z);uv.z += dot(deltaUV, receiverPlaneDepthBias.xy);return uv;
}
baseUV + deltaUV,就是進行uv的偏移。然后z值,可以uv和陰影bias點乘,加到z上,最終得到一個三維的采樣坐標。
最終:fetchesWeightsU.x * fetchesWeightsV.x * UNITY_SAMPLE_SHADOW
得到一個點的權重,然后分別做四次即可,得到四個方向上的陰影貢獻值,即可得到最后軟陰影效果了。