一、陰影與全局照明系統的關係
Unity3D引擎可以根據宏SHADOWS_SCREEN和LIGHTMAP_ON是否啓用決定是否在全局照明系統下對陰影進行混合處理。如果這兩個宏同時啓用,則HANDLE_SHADOWS_BLENDING_IN_GI定義爲1,即宣告在全局照明下也對陰影進行處理。宏SHADOWS_SCREEN本質上是一個着色器多樣體,表示是否在屏幕空間中處理陰影計算,如下所示:
#if defined( SHADOWS_SCREEN ) && defined( LIGHTMAP_ON )
#define HANDLE_SHADOWS_BLENDING_IN_GI 1
#endif
二、聚光燈光源生成的陰影
Unity3D引擎會根據不同類型的光源,用不同的計算方式對應計算光源所產生的引擎。引擎提供的陰影計算庫中,有用聚光燈光源生成和用點光源生成的陰影。當啓用SPOT宏時,表示使用聚光燈生成,如下代碼:
2.1啓用SPOT宏
#if defined (SHADOWS_DEPTH) && defined (SPOT)
// declare shadowmap
//如果沒有聲明shadowmap,則聲明一個陰影貼圖紋理ShadowMapTexture
#if !defined(SHADOWMAPSAMPLER_DEFINED)
UNITY_DECLARE_SHADOWMAP(_ShadowMapTexture);
#define SHADOWMAPSAMPLER_DEFINED
#endif
// shadow sampling offsets and texel size
//陰影貼圖紋理的偏移量和紋素的大小
#if defined (SHADOWS_SOFT)
float4 _ShadowOffsets[4];
float4 _ShadowMapTexture_TexelSize;
#define SHADOWMAPSAMPLER_AND_TEXELSIZE_DEFINED
#endif
如果啓用了SHADOWS_SOFT,即啓用了軟陰影效果,就還需要其他採樣點去進行陰影的柔化操作。變量_ShadowOffsets[4]取出了陰影某個採樣點的4個偏移採樣點。
2.2 UNITY_DECLARE_SHADOWMAP宏的定義
UNITY_DECLARE_SHADOWMAP宏在HLSLSupport.cginc文件中定義,用來聲明一個陰影紋理。除此之外,在文件中還定義了其他對陰影進行操作的宏,如進行採樣操作的宏UNITY_SAMPLE_SHADOW、進行投影計算的宏UNITY_SAMPLE_SHADOW_PROJ。這些宏在不同的平臺下使用不同版本的着色器編譯器,有着不同的實際定義。
2.3 SAMPLE_DEPTH_TEXTURE 及類似的宏
如果能使用着色器本身支持的陰影相關的操作函數,即宏SHADOW_NATIVE被啓用,就直接使用這些函數,如果不支持,就使用普通2D紋理函數對紋理進行採樣。這些函數Unity3D已經用宏包裝了一層。
#if defined(SHADER_API_PSP2)
half SAMPLE_DEPTH_TEXTURE(sampler2D s, float4 uv) { return tex2D<float>(s, (float3)uv); }
half SAMPLE_DEPTH_TEXTURE(sampler2D s, float3 uv) { return tex2D<float>(s, uv); }
half SAMPLE_DEPTH_TEXTURE(sampler2D s, float2 uv) { return tex2D<float>(s, uv); }
# define SAMPLE_DEPTH_TEXTURE_PROJ(sampler, uv) (tex2DprojShadow(sampler, uv))
# define SAMPLE_DEPTH_TEXTURE_LOD(sampler, uv) (tex2Dlod<float>(sampler, uv))
# define SAMPLE_RAW_DEPTH_TEXTURE(sampler, uv) SAMPLE_DEPTH_TEXTURE(sampler, uv)
# define SAMPLE_RAW_DEPTH_TEXTURE_PROJ(sampler, uv) SAMPLE_DEPTH_TEXTURE_PROJ(sampler, uv)
# define SAMPLE_RAW_DEPTH_TEXTURE_LOD(sampler, uv) SAMPLE_DEPTH_TEXTURE_LOD(sampler, uv)
half SAMPLE_DEPTH_CUBE_TEXTURE(samplerCUBE s, float3 uv) { return texCUBE<float>(s, uv); }
#else
// Sample depth, just the red component.
//只採樣深度值,所以只需要用到depth texture的texel的red分量即可
# define SAMPLE_DEPTH_TEXTURE(sampler, uv) (tex2D(sampler, uv).r)
# define SAMPLE_DEPTH_TEXTURE_PROJ(sampler, uv) (tex2Dproj(sampler, uv).r)
# define SAMPLE_DEPTH_TEXTURE_LOD(sampler, uv) (tex2Dlod(sampler, uv).r)
// Sample depth, all components.
//需要用到depth texture的texel的所有分量
# define SAMPLE_RAW_DEPTH_TEXTURE(sampler, uv) (tex2D(sampler, uv))
# define SAMPLE_RAW_DEPTH_TEXTURE_PROJ(sampler, uv) (tex2Dproj(sampler, uv))
# define SAMPLE_RAW_DEPTH_TEXTURE_LOD(sampler, uv) (tex2Dlod(sampler, uv))
# define SAMPLE_DEPTH_CUBE_TEXTURE(sampler, uv) (texCUBE(sampler, uv).r)
#endif
2.4 UnitySampleShadowMap函數版本 1
理解了宏的定義,繼續回到UnityShadowLibrary.cginc的代碼分析UnitySampleShadowmap函數,該函數的功能就是根據給定的陰影座標,在陰影深度貼圖中進行採樣,獲取陰影座標對應的貼圖紋素的深度值,如下:
//float4 shadowCoord是場景中某個空間位置點,該位置點已經變換到產生陰影的那個光源的光源空間。判斷shadowCoord是否在陰影下,以及判斷該陰影的濃度
inline fixed UnitySampleShadowmap (float4 shadowCoord)
{//如果使用軟陰影
#if defined (SHADOWS_SOFT)
half shadow = 1;
// No hardware comparison sampler (ie some mobile + xbox360) : simple 4 tap PCF
//如果着色器不支持原生的陰影操作函數
#if !defined (SHADOWS_NATIVE)
//除以w,進行透視除法,把座標轉換到一個NDC座標上執行操作
float3 coord = shadowCoord.xyz / shadowCoord.w;
float4 shadowVals;
//獲取到本採樣點四周的四個偏移採樣點的深度值,然後存儲到shadowVals變量中
shadowVals.x = SAMPLE_DEPTH_TEXTURE(_ShadowMapTexture, coord + _ShadowOffsets[0].xy);
shadowVals.y = SAMPLE_DEPTH_TEXTURE(_ShadowMapTexture, coord + _ShadowOffsets[1].xy);
shadowVals.z = SAMPLE_DEPTH_TEXTURE(_ShadowMapTexture, coord + _ShadowOffsets[2].xy);
shadowVals.w = SAMPLE_DEPTH_TEXTURE(_ShadowMapTexture, coord + _ShadowOffsets[3].xy);
//如果本採樣點四周的4個採樣點的z值都小於陰影貼圖採樣點的z值,就表示該點不處於陰影區域。_LightShadowData的r分量,即x分量表示陰影的強度值
half4 shadows = (shadowVals < coord.zzzz) ? _LightShadowData.rrrr : 1.0f;
//陰影值爲本採樣點四周的4個採樣點的陰影值的平均值
shadow = dot(shadows, 0.25f);
#else
// Mobile with comparison sampler : 4-tap linear comparison filter
//如果是在移動平臺上,使用tex2D函數進行採樣
#if defined(SHADER_API_MOBILE)
float3 coord = shadowCoord.xyz / shadowCoord.w;
half4 shadows;
shadows.x = UNITY_SAMPLE_SHADOW(_ShadowMapTexture, coord + _ShadowOffsets[0]);
shadows.y = UNITY_SAMPLE_SHADOW(_ShadowMapTexture, coord + _ShadowOffsets[1]);
shadows.z = UNITY_SAMPLE_SHADOW(_ShadowMapTexture, coord + _ShadowOffsets[2]);
shadows.w = UNITY_SAMPLE_SHADOW(_ShadowMapTexture, coord + _ShadowOffsets[3]);
shadow = dot(shadows, 0.25f);
// Everything else
//其他未特別聲明的平臺
#else
float3 coord = shadowCoord.xyz / shadowCoord.w;
float3 receiverPlaneDepthBias = UnityGetReceiverPlaneDepthBias(coord, 1.0f);
shadow = UnitySampleShadowmap_PCF3x3(float4(coord, 1), receiverPlaneDepthBias);
#endif
shadow = lerp(_LightShadowData.r, 1.0f, shadow);
#endif
2.5 unitySampleShadowmap函數版本 2
#else
// 1-tap shadows
//如果不用軟陰影,就不需要對陰影進行柔化
//使用着色器支持的陰影操作函數shadow2proj來進行紋理投影,在這裏宏UNITY_SAMPLE_SHADOW_PROJ即是shadow2Dproj
#if defined (SHADOWS_NATIVE)
half shadow = UNITY_SAMPLE_SHADOW_PROJ(_ShadowMapTexture, shadowCoord);
//進行線性插值,讓陰影值落在當前陰影強度0和1之間
shadow = lerp(_LightShadowData.r, 1.0f, shadow);
#else
//如果沒有着色器內建的陰影操作函數,就直接比較當前判斷點的z值和陰影圖中對應點的z值,然後返回
half shadow = SAMPLE_DEPTH_TEXTURE_PROJ(_ShadowMapTexture, UNITY_PROJ_COORD(shadowCoord)) < (shadowCoord.z / shadowCoord.w) ? _LightShadowData.r : 1.0;
#endif
#endif
三、點光源生成的陰影
當啓用SHADOWS_CUBE 宏時,將使用點光源生成陰影。和聚光燈光源不同的是,點光源生成的陰影,其陰影深度貼圖存儲在一個立方體紋理(cube texture中)。貼圖中某一點紋素所存儲的深度值,即某處離光源最遠且光線能照射到的那個位置的深度值。
3.1 從立方體紋理貼圖中取得紋素對應的深度值
下面的代碼展示瞭如何從一個立方體紋理貼圖中取得某紋素對應的深度值,通常有一個有着RGBA通道的紋理,如果僅用來存儲深度值,一般只用到其中一個通道,就是Red通道。
代碼段 SampleCubeDistance
Unity3D引擎充分利用了R、G、B、A這4個通道的存儲空間,把一個浮點數編碼進4個通道中,以提高深度值的精度:
#if defined (SHADOWS_CUBE)
#if defined(SHADOWS_CUBE_IN_DEPTH_TEX)
UNITY_DECLARE_TEXCUBE_SHADOWMAP(_ShadowMapTexture);
#else
//如果使用立方體紋理映射貼圖作爲一個陰影深度紋理
UNITY_DECLARE_TEXCUBE(_ShadowMapTexture);
//vec是從原點發出,指向立方體上某點位置的連線向量,也是立方體紋理的貼圖座標
inline float SampleCubeDistance (float3 vec)
{
return UnityDecodeCubeShadowDepth(UNITY_SAMPLE_TEXCUBE_LOD(_ShadowMapTexture, vec, 0));
}
#endif
上述代碼中調用了UnityDecodeCubeShadowDepth函數。texCUBE函數和texCUBElod函數的區別在於:前者如果使用兩個參數版本,是不帶mipmap採樣的;而後者則會依據不同的mipmap進行不同精度的採樣。
3.2 對採樣值進行混合計算
實現了從立方體貼圖中進行採樣之後就可以根據當前位置進行陰影判斷了。UnitySampleShadowmap函數接受一個float3類型的值,此值是當前待判斷是否在陰影中的偏移在光源空間中的座標。計算出該座標到光源之間的距離,並且歸一化,然後在該座標位置點的右上前方、左下前方、左上後方、右下後方偏移,各取得對應紋素的深度值:
inline half UnitySampleShadowmap (float3 vec)
{
#if defined(SHADOWS_CUBE_IN_DEPTH_TEX)
float3 absVec = abs(vec);
float dominantAxis = max(max(absVec.x, absVec.y), absVec.z); // TODO use max3() instead
dominantAxis = max(0.00001, dominantAxis - _LightProjectionParams.z); // shadow bias from point light is apllied here.
dominantAxis *= _LightProjectionParams.w; // bias
float mydist = -_LightProjectionParams.x + _LightProjectionParams.y/dominantAxis; // project to shadow map clip space [0; 1]
#if defined(UNITY_REVERSED_Z)
mydist = 1.0 - mydist; // depth buffers are reversed! Additionally we can move this to CPP code!
#endif
#else
float mydist = length(vec) * _LightPositionRange.w;
mydist *= _LightProjectionParams.w; // bias 稍微做一點偏移
#endif
#if defined (SHADOWS_SOFT)
float z = 1.0/128.0;
float4 shadowVals; //取得四個採樣點的深度值
// No hardware comparison sampler (ie some mobile + xbox360) : simple 4 tap PCF
#if defined (SHADOWS_CUBE_IN_DEPTH_TEX)
shadowVals.x = UNITY_SAMPLE_TEXCUBE_SHADOW(_ShadowMapTexture, float4(vec+float3( z, z, z), mydist));
shadowVals.y = UNITY_SAMPLE_TEXCUBE_SHADOW(_ShadowMapTexture, float4(vec+float3(-z,-z, z), mydist));
shadowVals.z = UNITY_SAMPLE_TEXCUBE_SHADOW(_ShadowMapTexture, float4(vec+float3(-z, z,-z), mydist));
shadowVals.w = UNITY_SAMPLE_TEXCUBE_SHADOW(_ShadowMapTexture, float4(vec+float3( z,-z,-z), mydist));
half shadow = dot(shadowVals, 0.25);
return lerp(_LightShadowData.r, 1.0, shadow);
#else
shadowVals.x = SampleCubeDistance (vec+float3( z, z, z));
shadowVals.y = SampleCubeDistance (vec+float3(-z,-z, z));
shadowVals.z = SampleCubeDistance (vec+float3(-z, z,-z));
shadowVals.w = SampleCubeDistance (vec+float3( z,-z,-z));
half4 shadows = (shadowVals < mydist.xxxx) ? _LightShadowData.rrrr : 1.0f;
return dot(shadows, 0.25);
#endif
#else
#if defined (SHADOWS_CUBE_IN_DEPTH_TEX)
half shadow = UNITY_SAMPLE_TEXCUBE_SHADOW(_ShadowMapTexture, float4(vec, mydist));
return lerp(_LightShadowData.r, 1.0, shadow);
#else
half shadowVal = UnityDecodeCubeShadowDepth(UNITY_SAMPLE_TEXCUBE(_ShadowMapTexture, vec));
half shadow = shadowVal < mydist ? _LightShadowData.r : 1.0;
return shadow;
#endif
#endif
}
下圖展示瞭如何取得4個採樣點的深度,4個小方塊表示從光源發出並穿過4個採樣點(小圓圈表示)的射線落在立方體紋理的某一面所採得的紋素:
取得4個採樣點後,判斷它們的深度值是否小於當前片元的深度值,如果是則表示當前片元在陰影中。此時從_LightShadowData變量中取出表示陰影強度的r分量;否則,就直接返回1。最終把4個分量的值累加後除以4得到最終結果:
//如果深度圖紋理中的4個採樣點對應深度值都小於當前片元到光源的距離值
//表示當前片元在陰影中
half4 shadows = (shadowVals < mydist.xxxx) ? _LightShadowData.rrrr : 1.0f;
return dot(shadows, 0.25);
#else
四、預烘焙的陰影
4.1 LPPV_SampleProbeOcclusion函數
//啓用了光探針代理體纔可使用本函數
#if UNITY_LIGHT_PROBE_PROXY_VOLUME
half4 LPPV_SampleProbeOcclusion(float3 worldPos)
{
const float transformToLocal = unity_ProbeVolumeParams.y;
//U方向中紋素的個數
const float texelSizeX = unity_ProbeVolumeParams.z;
//把球諧函數的3階係數及光探針遮蔽信息打包到一個紋素中
//The SH coefficients textures and probe occlusion are packed into 1 atlas.
//-------------------------
//| ShR | ShG | ShB | Occ |
//-------------------------
//判斷在世界空間還是在局部空間中進行計算
float3 position = (transformToLocal == 1.0f) ? mul(unity_ProbeVolumeWorldToObject, float4(worldPos, 1.0)).xyz : worldPos;
//Get a tex coord between 0 and 1
//unity_ProbeVolumeSizeInv.xyz分別表示光探針代理體的長寬高方向上的紋素個數然後獲得本位置點對應的紋理映射座標
float3 texCoord = (position - unity_ProbeVolumeMin.xyz) * unity_ProbeVolumeSizeInv.xyz;
// Sample fourth texture in the atlas
// We need to compute proper U coordinate to sample.
// Clamp the coordinate otherwize we'll have leaking between ShB coefficients and Probe Occlusion(Occ) info
texCoord.x = max(texCoord.x * 0.25f + 0.75f, 0.75f + 0.5f * texelSizeX);
return UNITY_SAMPLE_TEX3D_SAMPLER(unity_ProbeVolumeSH, unity_ProbeVolumeSH, texCoord);
}
#endif //#if UNITY_LIGHT_PROBE_PROXY_VOLUME
4.2 UnitySampleBakedOcclusion函數版本1
// Used by the forward rendering path
//參數爲光照貼圖的UV座標,以及待處理的片元在世界座標系下的位置點
fixed UnitySampleBakedOcclusion (float2 lightmapUV, float3 worldPos)
{
//如果啓用了陰影蒙版
#if defined (SHADOWS_SHADOWMASK)
#if defined(LIGHTMAP_ON)
//如果啓用了光照貼圖,則從光照貼圖中提取遮蔽蒙版信息
fixed4 rawOcclusionMask = UNITY_SAMPLE_TEX2D(unity_ShadowMask, lightmapUV.xy);
#else
fixed4 rawOcclusionMask = fixed4(1.0, 1.0, 1.0, 1.0);
#if UNITY_LIGHT_PROBE_PROXY_VOLUME
if (unity_ProbeVolumeParams.x == 1.0)
//如果開啓了光探針代理體,從位置點worldPos所處的光探針代理體處取得此處的原始遮蔽信息
rawOcclusionMask = LPPV_SampleProbeOcclusion(worldPos);
else
//否則就仍從陰影蒙版貼圖中取得遮蔽信息
rawOcclusionMask = UNITY_SAMPLE_TEX2D(unity_ShadowMask, lightmapUV.xy);
#else
rawOcclusionMask = UNITY_SAMPLE_TEX2D(unity_ShadowMask, lightmapUV.xy);
#endif
#endif
return saturate(dot(rawOcclusionMask, unity_OcclusionMaskSelector));
4.3 UNITY_SAMPLE_TEX2D_SAMPLER宏的定義
在上述代碼段中,宏UNITY_SAMPLE_TEX2D_SAMPLER和宏UNITY_SAMPLE_TEX2D封裝了在不同平臺下對一個2D紋理的採樣操作指令,如下所示:
#define UNITY_SAMPLE_TEX2D(tex,coord) tex.Sample (sampler##tex,coord)
#define UNITY_SAMPLE_TEX2D_SAMPLER(tex,samplertex,coord) tex.Sample (sampler##samplertex,coord)
主光照貼圖的採樣器可用於爲多個紋理所使用,陰影蒙版貼圖unity_ShadowMask也是採樣主光照貼圖的採樣器。
4.5 UnityGetRawBakedOcclusions 函數
UnityGetRawBakedOcclusions函數的功能和UnitySampleBakedOcclusion函數相似,不同之處在於它沒有使用unity_OcclusionMaskSelector變量選擇其中的通道,如下代碼:
// Used by the deferred rendering path (in the gbuffer pass)
fixed4 UnityGetRawBakedOcclusions(float2 lightmapUV, float3 worldPos)
{
#if defined (SHADOWS_SHADOWMASK)
#if defined(LIGHTMAP_ON)
return UNITY_SAMPLE_TEX2D(unity_ShadowMask, lightmapUV.xy);
#else
half4 probeOcclusion = unity_ProbesOcclusion;
#if UNITY_LIGHT_PROBE_PROXY_VOLUME
if (unity_ProbeVolumeParams.x == 1.0)
probeOcclusion = LPPV_SampleProbeOcclusion(worldPos);
#endif
return probeOcclusion;
#endif
#else
return fixed4(1.0, 1.0, 1.0, 1.0);
#endif
}
變量unity_ProbesOcclusion在UnityShaderVariables.cginc文件中定義,它是一個fixed4類型的變量。通過調用引擎C#層提供的API方法:MaterialPropertyBlock.CopyProbeOcculusionArrayFrom,可以從客戶端向引擎填充此數值。
4.6 UnityMixRealtimeAndBakedShadows函數
此函數可以對實時陰影和烘焙陰影進行混合。這個函數主要思想:按平常的做法衰減實時陰影,然後取其和烘焙陰影的最小值:
// ------------------------------------------------------------------
// Used by both the forward and the deferred rendering path
half UnityMixRealtimeAndBakedShadows(half realtimeShadowAttenuation, half bakedShadowAttenuation, half fade)
{
//如果基於深度貼圖的陰影,基於屏幕空間的陰影、基於立方體紋理的陰影這三者都沒有打開
#if !defined(SHADOWS_DEPTH) && !defined(SHADOWS_SCREEN) && !defined(SHADOWS_CUBE)
//如果沒有使用蒙版陰影
#if defined(LIGHTMAP_ON) && defined (LIGHTMAP_SHADOW_MIXING) && !defined (SHADOWS_SHADOWMASK)
//In subtractive mode when there is no shadow we kill the light contribution as direct as been baked in the lightmap.
//在subtractive模式下,沒有陰影存在
return 0.0;
#else
//使用了陰影蒙版,直接返回預烘焙的衰減值
return bakedShadowAttenuation;
#endif
#endif
#if (SHADER_TARGET <= 20) || UNITY_STANDARD_SIMPLE
//no fading nor blending on SM 2.0 because of instruction count limit.
//如果shade model小於2.0,且啓用了陰影蒙版,由於SM2.0的指令條數有限制,不進行陰影淡出和混合的計算,直接比較兩者的衰減值,返回小者即可。
#if defined(SHADOWS_SHADOWMASK) || defined(LIGHTMAP_SHADOW_MIXING)
return min(realtimeShadowAttenuation, bakedShadowAttenuation);
#else
//直接返回實時陰影的衰減值
return realtimeShadowAttenuation;
#endif
#endif
//如果啓用了陰影蒙版值
#if defined(LIGHTMAP_SHADOW_MIXING)
//Subtractive or shadowmask mode
//實時陰影加上淡化參數後,將它限制在[0,1]範圍內,然後將它和預烘焙陰影衰減值比較,返回較小者
realtimeShadowAttenuation = saturate(realtimeShadowAttenuation + fade);
return min(realtimeShadowAttenuation, bakedShadowAttenuation);
#endif
//否則就根據淡化參數在實時陰影衰減值和預烘焙陰影衰減值之間進行線性插值
//In distance shadowmask or realtime shadow fadeout we lerp toward the baked shadows (bakedShadowAttenuation will be 1 if no baked shadows)
return lerp(realtimeShadowAttenuation, bakedShadowAttenuation, fade);
}
五、陰影的淡化處理
5.1 UnityComputeShadowFadeDistance函數和UnityComputeShadowFade函數
爲了淡化陰影,要定義以下兩個函數:
// ------------------------------------------------------------------
// Shadow fade
// ------------------------------------------------------------------
//根據當前片元到攝像機的距離值,計算陰影的淡化程度。參數1:待計算的片元在世界座標系下的位置座標值。參數2:待計算的片元在世界座標系下到當前攝像機的距離
float UnityComputeShadowFadeDistance(float3 wpos, float z)
{
//計算當前點到unity_ShadowFadeCenterAndType點的距離
float sphereDist = distance(wpos, unity_ShadowFadeCenterAndType.xyz);
return lerp(z, sphereDist, unity_ShadowFadeCenterAndType.w);
}
// ------------------------------------------------------------------
half UnityComputeShadowFade(float fadeDist)
{
return saturate(fadeDist * _LightShadowData.z + _LightShadowData.w);
}
5.2 梯度計算
陰影深度貼圖中存儲的是片元的深度,一般地,貼圖的紋素一般只使用一個顏色通道存儲深度值,因此可以將陰影深度貼圖等同於一張灰度圖。如果將貼圖中每一個紋素的灰度值看作紋理映射座標u,v的一個二元函數,則有灰度函數
g=gray(u,v)
有了灰度函數,則可以用偏微分描述貼圖的灰度變化。因爲gray(u,v)函數在某點處有多個方向,所以該點處的方向倒數也不唯一。要想找到灰度變化的最大方向,即沿着此方向灰度函數進行求導時,導數的值最大的那個方向就需要用到梯度。
梯度是一個向量值,有大小和方向。它在某點處的方向,就是gray(u,v)函數沿着該方向求導時得到的導數值最大的方向。而這個最大的導數值,即梯度向量的向量長度稱爲梯度值。gray(u,v)函數的自變量u和v在它們的定義域範圍中,一一對應的梯度值就組成了關於u和v的梯度函數。
求梯度首先需要求出水平和垂直方向的偏導數。由於陰影深度貼圖的灰度是離散值,無法直接使用基於連續的微分運算,因此要使用差分運算。
六、計算深度陰影的偏移值
在使用陰影貼圖技術實現陰影時,如果不對陰影效果進行微調,往往會出現交錯條紋狀陰影的情況,這種現象通常被形象的比喻成“痤瘡(acne)”,稱爲陰影滲漏(shadow acne)。未處理和已處理陰影滲漏問題的效果圖對比如圖:
產生陰影滲漏的主要原因是陰影深度貼圖分辨率的問題,即因爲陰影深度貼圖的分辨率較小,導致在場景中多個片元在計算陰影時對應上了同一個陰影深度貼圖的紋素,因而導致判斷該片元到底在不在光線可到達的片元之前或者之後出現了問題。
如下圖,片元A、B、C、D都對應於一個陰影貼圖中的採樣判斷點p,La、Lb、Lc、Ld分別對應於光源到片元A、B、C、D的距離,L對應於光源到採樣判定點p的距離。
因爲陰影深度貼圖分辨率不夠大,導致A、B、C、D這4個在光源空間中處於不同位置座標的片元對應在同一個陰影深度貼圖的位置點p上,並且p所對應的深度值爲L,即光源到這一被照亮的位置點的距離爲L。如果該位置點p所對應的位置與光源的距離不大於L,該片元被照亮。大於L就被遮蓋住。
上圖中,因爲4個片元A、B、C、D都沒有被其他物體遮蓋住,所以無論虛線La、Lb、Lc、Ld所表示的長度是多少,都應該能被光源照亮,但在實際計算中,因爲陰影深度貼圖的分辨率,4個片元都只能使用L作爲進行判斷照亮與否的距離。所以最終La<L,Lc<L,片元A和C被照亮。Lb>L,Ld>L,片元B和D被遮擋,導致出現了交錯條紋狀陰影。
解決陰影滲漏最直接方法就是把計算出的La、Lb、Lc、Ld的長度,沿着這些線的反方向“往回拉一拉”,即減去一個微小的偏移值,使得最終La、Lb、Lc、Ld的長度都小於L,這樣原本應該能被照亮的地方就確實就被照明瞭,這種方法稱爲調整陰影偏差(shadow bias)。
使用調整陰影偏差有一個很大問題,就是較難定量地針對當前被照明物體的表面凹凸程度設置準確的偏差值。如果偏移值過小,依然還會有些應被照亮的片元沒被照亮;而如果偏移值過大,就會導致影物飄離(Peter Panning),即原本某些應該被遮住不被照亮的片元反被照亮,顯得物體和它的影子分開了似的:
可以看到,設置陰影偏差值確實比較困難,要找到一個剛好能夠消除陰影滲漏的值是需要一定的技巧和算法的。目前Unity3D引擎着色器採用的陰影偏差值的計算方法是基於物體斜度(slope)的,稱爲"基於斜度比例的深度偏差值"算法。
大部分改善對陰影深度貼圖採樣誤差的算法,其核心思想是分析待繪製場景中各部分內容對採樣誤差的影響程度。
首先約定陰影深度貼圖的採樣座標(u,v)的取值範圍是[0,1]x[0,1],分辨率爲水平方向上Ru個紋素、垂直方向上Rv個紋素;接着約定當前的視口分辨率爲水平方向上Ri個像素、垂直方向上Rj個像素。那麼可以建立某片元的屏幕空間座標(i,j)與陰影深度貼圖採樣座標(u,v)的映射關係,如下所示:
式子中,z爲觀察空間中待渲染物體的某可見片元的z座標值;函數U和V爲已知i、j、z,求得u、v的算法式。
6.1 UnityGetReceiverPlaneDepthBias函數
//根據給定的在屏幕空間中的陰影座標值,計算陰影接受平面的深度偏移值
float3 UnityGetReceiverPlaneDepthBias(float3 shadowCoord, float biasMultiply)
{
// Should receiver plane bias be used? This estimates receiver slope using derivatives,
// and tries to tilt the PCF kernel along it. However, when doing it in screenspace from the depth texture
// (ie all light in deferred and directional light in both forward and deferred), the derivatives are wrong
// on edges or intersections of objects, leading to shadow artifacts. Thus it is disabled by default.
float3 biasUVZ = 0;
#if defined(UNITY_USE_RECEIVER_PLANE_BIAS) && defined(SHADOWMAPSAMPLER_AND_TEXELSIZE_DEFINED)
//得到當前紋理座標點與水平方向的鄰居座標點
float3 dx = ddx(shadowCoord);
float3 dy = ddy(shadowCoord);
biasUVZ.x = dy.y * dx.z - dx.y * dy.z;
biasUVZ.y = dx.x * dy.z - dy.x * dx.z;
biasUVZ.xy *= biasMultiply / ((dx.x * dy.y) - (dx.y * dy.x));
// Static depth biasing to make up for incorrect fractional sampling on the shadow map grid.
const float UNITY_RECEIVER_PLANE_MIN_FRACTIONAL_ERROR = 0.01f;
float fractionalSamplingError = dot(_ShadowMapTexture_TexelSize.xy, abs(biasUVZ.xy));
biasUVZ.z = -min(fractionalSamplingError, UNITY_RECEIVER_PLANE_MIN_FRACTIONAL_ERROR);
#if defined(UNITY_REVERSED_Z)
biasUVZ.z *= -1;
#endif
#endif
return biasUVZ;
}
現在GPU爲了提高效率,會同時對至少4個片元進行並行處理。而且這4個片元一般以2X2的方式組織排列。在實際計算中,計算某一片元與它水平(或垂直)方向上的鄰接片元的屬性(如它的紋理座標)的一階差分值,便可以近似等於該片元在水平(或垂直)方向上的導數。這個計算水平(或垂直)一階差分值(導數值),在Cg/HLSL平臺上用ddx(或ddy)函數計算,在GLSL平臺上用dFdx(或dFdy)函數計算。因爲ddx/ddy(或dFdx/dFdy)函數需要用到片元的屬性,因此只能在片元着色器中使用它們。
6.2 UnityCombineShadowcoordComponets函數
//組合一個陰影座標的不同分量並返回最後一下分量 參數1:本採樣點對應的陰影貼圖uv座標,參數2:本採樣點對應的uv座標的偏移量,參數3:本採樣點存儲的深度值,參數4:接受陰影投射的平面的深度偏差值
float3 UnityCombineShadowcoordComponents(float2 baseUV, float2 deltaUV, float depth, float3 receiverPlaneDepthBias)
{
//陰影貼圖的uv採樣座標,還有對應的深度值都加上偏移值
float3 uv = float3(baseUV + deltaUV, depth + receiverPlaneDepthBias.z);
uv.z += dot(deltaUV, receiverPlaneDepthBias.xy);
return uv;
}
七、PCF陰影過濾的相關函數
陰影會產生鋸齒效果,是因爲在對某片元“判斷它是否在陰影之內”而進行深度測試時,要把該片元從當前攝像機的觀察空間轉換到光源空間中。因爲轉換矩陣不一樣,且陰影深度貼圖分辨率不夠大,導致在觀察空間中多個片元對應於陰影深度貼圖中的同一個紋素,例如兩個黑色鋸齒空間的空白部分,本來這部分應該也是處於黑色陰影中的,但因爲採樣到的陰影深度貼圖中的紋素“剛好不是黑色的”,即那個紋素剛好不在黑色陰影下,所以就導致產生鋸齒。
要解決鋸齒最直接也最簡單的方式是提高陰影深度貼圖的分辨率,但提高貼圖的分辨率會帶來內存佔用過大的問題,而且這種方法也只能是減輕而無法從算法程度上解決鋸齒現象。在實際實現中,通常採用適當分辨率的陰影深度貼圖加上區域採樣方法改善鋸齒現象。
因爲陰影深度貼圖的紋素中存儲的不是一般紋理的顏色信息,而是存儲的深度信息,對深度值取平均值會產生不正確的深度結果,所以鋸齒現象不能通過對某紋素周邊鄰接的紋素取值然後求平均值來消除。百分比切近濾波(PCF)方法,是對陰影比較測試後的值進行濾波,可以使生成的陰影邊緣平滑柔和。PCF方法具體步驟是:在片元着色器中,把當前操作的片元f先變到光源空間,然後經投影和視口變換到陰影深度貼圖空間中,假設變換後深度值爲z,對應的貼圖座標爲(u,v),該座標對應的紋素深度值爲z0。進行到這一步,如果不使用PCF方法,那麼直接就根據z和z0的大小判斷該片元要麼在陰影中全黑,要麼不在陰影中不黑。而PCF方法則是對貼圖座標(u,v)處的周邊紋素也進行採樣獲取其深度值,再和當前片元的深度值z比較,如果在陰影中標識爲1,不在陰影中標識爲0,並把這些01值每項累加求得平均值,在這些平均值落在[0,1]中,這樣陰影就有濃淡之分而不像未使用PCF方法之前的非明即暗,從而達到柔化邊緣,減少鋸齒的效果,下圖演示了使用3X3的PCF方法採樣效果:
如圖,如果令最終陰影值爲shadow,待採樣的陰影紋理貼圖depthTexture,僞代碼格式的數學表達式爲:
式子中,Weight爲各個採樣點的權重值,即對陰影的貢獻值。上圖Wright是一個常數,爲1/9。
7.1 UnitySampleShadowmap_PCF3x3NoHardwareSupport函數
當目標平臺沒有使用硬件實現的PCF功能時,引擎提供UnitySampleShadowmap_PCF3x3NoHardwareSupport函數,用着色器代碼實現3x3 PCF採樣的功能,代碼如下:
half UnitySampleShadowmap_PCF3x3NoHardwareSupport(float4 coord, float3 receiverPlaneDepthBias)
{
half shadow = 1;
#ifdef SHADOWMAPSAMPLER_AND_TEXELSIZE_DEFINED
// when we don't have hardware PCF sampling, then the above 5x5 optimized PCF really does not work.
// Fallback to a simple 3x3 sampling with averaged results.
float2 base_uv = coord.xy;
float2 ts = _ShadowMapTexture_TexelSize.xy;
shadow = 0;
//取得本採樣點紋素的左上方紋素 1
shadow += UNITY_SAMPLE_SHADOW(_ShadowMapTexture, UnityCombineShadowcoordComponents(base_uv, float2(-ts.x, -ts.y), coord.z, receiverPlaneDepthBias));
//取得本採樣點紋素的正上方紋素 2
shadow += UNITY_SAMPLE_SHADOW(_ShadowMapTexture, UnityCombineShadowcoordComponents(base_uv, float2(0, -ts.y), coord.z, receiverPlaneDepthBias));
//取得本採樣點紋素的右上方紋素 3
shadow += UNITY_SAMPLE_SHADOW(_ShadowMapTexture, UnityCombineShadowcoordComponents(base_uv, float2(ts.x, -ts.y), coord.z, receiverPlaneDepthBias));
//取得本採樣點紋素的正左方紋素 4
shadow += UNITY_SAMPLE_SHADOW(_ShadowMapTexture, UnityCombineShadowcoordComponents(base_uv, float2(-ts.x, 0), coord.z, receiverPlaneDepthBias));
//本採樣點紋素 5
shadow += UNITY_SAMPLE_SHADOW(_ShadowMapTexture, UnityCombineShadowcoordComponents(base_uv, float2(0, 0), coord.z, receiverPlaneDepthBias));
//取得本採樣點紋素的正右方紋素 6
shadow += UNITY_SAMPLE_SHADOW(_ShadowMapTexture, UnityCombineShadowcoordComponents(base_uv, float2(ts.x, 0), coord.z, receiverPlaneDepthBias));
//取得本採樣點紋素的左下方紋素 7
shadow += UNITY_SAMPLE_SHADOW(_ShadowMapTexture, UnityCombineShadowcoordComponents(base_uv, float2(-ts.x, ts.y), coord.z, receiverPlaneDepthBias));
//取得本採樣點紋素的正上方紋素 8
shadow += UNITY_SAMPLE_SHADOW(_ShadowMapTexture, UnityCombineShadowcoordComponents(base_uv, float2(0, ts.y), coord.z, receiverPlaneDepthBias));
//取得本採樣點紋素的右下方紋素 9
shadow += UNITY_SAMPLE_SHADOW(_ShadowMapTexture, UnityCombineShadowcoordComponents(base_uv, float2(ts.x, ts.y), coord.z, receiverPlaneDepthBias));
shadow /= 9.0;
#endif
return shadow;
}
7.2 用於進行PCF過濾的輔助函數
紋素的採樣格式取決於採樣內核所執行的區域大小。一個紋素個數爲mXm的深度陰影貼圖,如果要把所有紋素都用來進行採樣,需要n=m的平方採樣計算,這顯然是很不切實際的。因此,Unity3D引擎使用了一些減少PCF紋素採樣次數的優化算法。
對於下圖所示的採樣內核(也稱爲濾波器),如果不經算法優化,即便使用直接支持一次取樣4個紋素的硬件指令,也依然需要做9次紋理採樣操作。而經過優化後只需要做4次就能達到同樣的效果。紋理採樣次數直接決定了濾波操作的性能效率。經過優化算法後,可以把一個n階的濾波器(n-1)平方採樣降到(n/2)平方次採樣,從而大大提升執行效率。
Unity使用了若干個不同規格的等腰直角三角形,在4階、6階、8階採樣內核上進行覆蓋,以獲取不同紋素對陰影的貢獻程度,然後遵循n階採樣內核執行(n/2)的平方次採樣的規則進行PCF處理,下面的代碼就是進行PCF操作的一系列工具函數。
7.2.1 _UnityInternalGetAreaAboveFirstTexelUnderAIsocelesRectangleTriangle函數
//根據給定的三角形的高triangleHeight,得到以triangleHeight值爲高的等腰直角三角形的面積
float _UnityInternalGetAreaAboveFirstTexelUnderAIsocelesRectangleTriangle(float triangleHeight)
{
return triangleHeight - 0.5;
}
上述代碼根據一個給定高的等腰直角三角形計算該三角形的面積。該三角形的高以紋素爲單位,因此,直接令高減去0.5即可得其面,如下圖所示,可知等腰直角三角形的高和麪積之間的數值關係。
在下面代碼中給定一個高爲1.5紋素、底爲3紋素的等腰直角三角形,在不同的水平方向偏移值的情況下,對於覆蓋在4個連續的紋素方格上的這個三角形,求每個方格上放置的部分的面積。
7.2.2 _UnityInternalGetAreaPerTexel_3TexelsWideTriangleFilter函數
//本函數假定本等腰三角形的高爲1.5紋素,底爲3紋素,共佔據了4個紋素點,本函數返回本三角形分別被這4個紋素點分割了多少面積,offset取值範圍是[-0.5,0.5],爲0時表示三角形居中
void _UnityInternalGetAreaPerTexel_3TexelsWideTriangleFilter(float offset, out float4 computedArea, out float4 computedAreaUncut)
{
//Compute the exterior areas
//假設offset爲0,則offset01SquareHalved爲0.125
//computedAreaUncut.x和computedArea.x爲0.125
//computedAreaUncut.w和computedArea.w也爲0.125
float 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).
//當offset等於0時,computedAreaUncut.y爲1
//爲offset等於0.5時,computedAreaUncut.y爲0.5,computedArea.y爲0.5
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;
//當offset爲0時,computedAreaUncut.y和computedArea.y都爲1
//We do the same for the Z but with the right part of the isoceles triangle
computedAreaUncut.z = _UnityInternalGetAreaAboveFirstTexelUnderAIsocelesRectangleTriangle(1.5 + offset);
float clampedOffsetRight = max(offset,0);
float areaOfSmallRightTriangle = clampedOffsetRight * clampedOffsetRight;
computedArea.z = computedAreaUncut.z - areaOfSmallRightTriangle;
}
假設傳遞進來的offset分別爲0、-0.5、0.5,對應的computedAreaUncut和computed的值如下圖:
有時並不需要知道每個紋素上面覆蓋着的部分三角形的面積,而只需要知道這些部分三角形的面積佔總面積的百分比。函數_UnityInternalGetWeightPerTexel_3TexelsWideTriangleFilter就可實現這個功能,本質上該函數就是轉調_UnityInternalGetAreaPerTexel_3TexelsWideTriangleFilter函數獲取到部分三角形後再除以總面積。
7.2.3 _UnityInternalGetWeightPerTexel_3TexelsWideTriangleFilter函數
//本函數假定等腰直角三角形的高爲1.5紋素,底爲3紋素,該三角形覆蓋在4個紋素點上
//本函數將求出每個墊在紋素點上面的那部分三角形的面積,並求出各部分面積佔總面積的比例
void _UnityInternalGetWeightPerTexel_3TexelsWideTriangleFilter(float offset, out float4 computedWeight)
{
float4 dummy;
//獲取每個紋素上面的部分三角形的面積
_UnityInternalGetAreaPerTexel_3TexelsWideTriangleFilter(offset, computedWeight, dummy);
computedWeight *= 0.44444;//0.44444是總面積2.25的倒數
}
接下來的函數則處理一個高爲2.5紋素、底爲5紋素的等腰直角三角形,求得在不同的水平方向偏移值的情況下,覆蓋在6個連續的紋素方格上的三角形每個方格上放置的部分的面積分別是多少。其計算方式和“高爲1.5紋素底爲3紋素”的較小三角形相同,並且由於對稱性,可以直接調用計算較小三角形的 _UnityInternalGetAreaPerTexel_3TexelsWideTriangleFilter函數得到結果後,對應補加上多出來的面積片即可,如圖8-9所示:
7.2.4 _UnityInternalGetWeightPerTexel_5TexelsWideTriangleFilter函數
//本函數假定一個等腰直角三角形的高爲2.5紋素,底爲5紋素,該三角形覆蓋在6個紋素點上
//本函數將求出每個墊在紋素點上面的那部分三角形的面積,並求出各部分面積佔總面積的比例並返回
//參數2和3:每個墊在紋素點上面的那部分三角形的面積佔總面積的比例。
void _UnityInternalGetWeightPerTexel_5TexelsWideTriangleFilter(float offset, out float3 texelsWeightsA, out float3 texelsWeightsB)
{
//See _UnityInternalGetAreaPerTexel_3TexelTriangleFilter for details.
float4 computedArea_From3texelTriangle;
float4 computedAreaUncut_From3texelTriangle;
//按高爲1.5紋素,底爲3紋素,覆蓋在4個紋素點上的方法先算出其中4個紋素點
//剩餘的兩個重用_UnityInternalGetWeightPerTexel_3TexelsWideTriangleFilter的計算結果來計算最終大小
_UnityInternalGetAreaPerTexel_3TexelsWideTriangleFilter(offset, computedArea_From3texelTriangle, computedAreaUncut_From3texelTriangle);
//Triangle slop is 45 degree thus we can almost reuse the result of the 3 texel wide computation.
//the 5 texel wide triangle can be seen as the 3 texel wide one but shifted up by one unit/texel.
//0.16 is 1/(the triangle area)
//0.16是三角形總面積6.25的倒數,求得各部分面積佔總面積的比例
texelsWeightsA.x = 0.16 * (computedArea_From3texelTriangle.x);
texelsWeightsA.y = 0.16 * (computedAreaUncut_From3texelTriangle.y);
texelsWeightsA.z = 0.16 * (computedArea_From3texelTriangle.y + 1);
texelsWeightsB.x = 0.16 * (computedArea_From3texelTriangle.z + 1);
texelsWeightsB.y = 0.16 * (computedAreaUncut_From3texelTriangle.z);
texelsWeightsB.z = 0.16 * (computedArea_From3texelTriangle.w);
}
最後的輔助函數是處理一個高爲3.5紋素、底爲7紋素的等腰直角三角形,求得在不同的水平方向偏移值的情況下,覆蓋在8個連續紋素方格上的這個三角形每個方格上放置的部分的面積,算法和上一個三角形類型,如下。
//本函數假定一個等腰直角三角形的高爲3.5紋素,底爲7紋素,該三角形覆蓋在8個紋素點上
//本函數將求出每個墊在紋素點上面的那部分三角形的面積,並求出各部分面積佔總面積的比例並返回
//參數2和3:每個墊在紋素點上面的那部分三角形的面積佔總面積的比例。
void _UnityInternalGetWeightPerTexel_7TexelsWideTriangleFilter(float offset, out float4 texelsWeightsA, out float4 texelsWeightsB)
{
//See _UnityInternalGetAreaPerTexel_3TexelTriangleFilter for details.
float4 computedArea_From3texelTriangle;
float4 computedAreaUncut_From3texelTriangle;
//按高爲1.5紋素,底爲3紋素,覆蓋在4個紋素點上的方法先算出其中4個紋素點
//剩餘的兩個重用_UnityInternalGetWeightPerTexel_3TexelsWideTriangleFilter的計算結果來計算最終大小
_UnityInternalGetAreaPerTexel_3TexelsWideTriangleFilter(offset, computedArea_From3texelTriangle, computedAreaUncut_From3texelTriangle);
//Triangle slop is 45 degree thus we can almost reuse the result of the 3 texel wide computation.
//the 7 texel wide triangle can be seen as the 3 texel wide one but shifted up by two unit/texel.
//0.081632 is 1/(the triangle area)
//0.081632是三角形總面積12.25的倒數,求得各部分面積佔總面積的比例
texelsWeightsA.x = 0.081632 * (computedArea_From3texelTriangle.x);
texelsWeightsA.y = 0.081632 * (computedAreaUncut_From3texelTriangle.y);
texelsWeightsA.z = 0.081632 * (computedAreaUncut_From3texelTriangle.y + 1);
texelsWeightsA.w = 0.081632 * (computedArea_From3texelTriangle.y + 2);
texelsWeightsB.x = 0.081632 * (computedArea_From3texelTriangle.z + 2);
texelsWeightsB.y = 0.081632 * (computedAreaUncut_From3texelTriangle.z + 1);
texelsWeightsB.z = 0.081632 * (computedAreaUncut_From3texelTriangle.z);
texelsWeightsB.w = 0.081632 * (computedArea_From3texelTriangle.w);
}
7.3 執行PCF過濾操作的函數
獲取了在不同紋素下三角形覆蓋比例之後,可以根據減少PCF採樣次數的原則進行陰影採樣操作。UnitySampleShadowmap_PCF3X3Tent函數是對四階的採樣內核進行操作,因此可以將PCF採樣次數精簡到4次,如以下代碼:
/**
* 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
//如果沒有硬件支持,不支持使用硬件指令實現PCF採樣,則調用軟件方法實現3X3的採樣
#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 fetches
//把單位化紋理映射座標轉爲紋素座標,_ShadowMapTexture_TexelSize.zw爲陰影貼圖的長和寬方向各自的紋素個數
float2 tentCenterInTexelSpace = coord.xy * _ShadowMapTexture_TexelSize.zw;
//floor 函數向下取整
float2 centerOfFetchesInTexelSpace = floor(tentCenterInTexelSpace + 0.5);
//計算tent中點到fetch點中點的偏移值
float2 offsetFromTentCenterToCenterOfFetches = tentCenterInTexelSpace - centerOfFetchesInTexelSpace;
//爲了便於理解,假定tentCenterInTexelSpace爲(4,6),則centerOfFetchesInTexelSpace=(4,6),offsetFromTentCenterToCenterOfFetches=(0,0)
// find the weight of each texel based
//求出基於每個紋素的權重,判斷每個紋素所佔有的部分三角形的權重,根據上面給定的數值,可以得到fetchesWeightsU=(0.0556,0.4444,0.4444,0.0556),fetchesWeightsV=(0.0556,0.4444,0.4444,0.0556)
float4 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 texels
//每次提取會覆蓋一組的2X2的紋素
//該組的權重是紋素權重的和
//根據上面的數組,得到fetchesWeightsU=(0.5,0.5),fetchesWeightsV=(0.5,0.5)
float2 fetchesWeightsU = texelsWeightsU.xz + texelsWeightsU.yw;
float2 fetchesWeightsV = texelsWeightsV.xz + texelsWeightsV.yw;
//根據上面的值,可以得到 texelsWeightsU=(0.0556,0.4444,0.4444,0.0556),texelsWeightsV=(0.0556,0.4444,0.4444,0.0556)
//經計算之後,fetchesOffsetsU=(-0.006112,0.1112),fetchesOffsetsV=(-0.6112,0.1112)
//假定texelSize.xy都爲0.01,則最終fetchesOffsetsU=(-0.006112,0.001112),fetchesOffsetsV=(-0.006112,0.001112)
// move the PCF bilinear fetches to respect texels weights
float2 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;
//fetchesWeightsU.x對應於x0,fetchesWeightsU.y對應於x1
//fetchesWeightsV.x對應於y0,fetchesWeightsV.y對應於y1
//雙線性過濾
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
return shadow;
}
計算採樣的陰影值採用的是雙線性插值方式,上述代碼中舉例的具體數值代入後就是那種可得到4個採樣點,原始陰影採樣點和4個PCF採樣點的位置關係如下圖:
UnitySampleShadowmap_PCF5X5Tent函數和UnitySampleShadowmap_PCF7X7Tent函數的算法思想和UnitySampleShadowmap_PCF3X3Tent函數類似。
7.4 基於3X3內核高斯模糊的PCF過濾操作
百分比切近過濾技術是非加權的區域採樣方法,可以對採樣區域進行加權的方法使得相交區域對片元的亮度的貢獻依賴於該區域與片元中心的距離。當直線經過某一個片元時,該片元的亮度F是在兩者相交的區域A上,對濾波器函數W(x,y)的積分。
高斯模糊(Gaussian blur)濾波技術就是基於加權區域採樣的思想,對待區域進行模糊處理,把高斯模糊應用在硬陰影的邊緣時,可以對硬陰影的邊緣鋸齒現象進行模糊化,產生軟陰影的邊緣柔化效果。高斯模糊效果的濾波器函數爲高斯濾波器,濾波器函數W(x,y)和亮度F如下:
從上面2個式子可知,要求解某片元F處的亮度值計算量非常大,可以採用離散計算的方法去模擬計算。首先將片元均勻分割成n個子片元,則每個子片元的所在面積爲1/n;然後計算每個子片元對原片元的亮度貢獻,並將其保存在一個二維加權表中;接着求出所有中心落於直線段內的子片元,並計算這些子片元對原片元亮度的貢獻的和。假如每個片元可以劃分爲n=3X3個子片元,則加權表可以設置爲:
當n越大的時候,高斯分佈曲面就越平滑。高斯模糊是根據高斯公式先計算出周圍片元對需要模糊的那個片元的影響程度,即權重值,然後對圖像中該像素的顏色值進行卷積計算,最後得到該片元的顏色值。
7.4.1 UnitySampleShadowmap_PCF3X3Gaussian函數
Unity3D引擎提供了UnitySampleShadowmap_PCF3X3Gaussian函數,在PCF採樣的基礎上,用高斯模糊算法重建了各採樣點的權重值,如下代碼:
half UnitySampleShadowmap_PCF3x3Gaussian(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
//求得每個採樣點的權重
const float2 offset = float2(0.5, 0.5);
float2 uv = (coord.xy * _ShadowMapTexture_TexelSize.zw) + offset;
float2 base_uv = (floor(uv) - offset) * _ShadowMapTexture_TexelSize.xy;
float2 st = frac(uv);
float2 uw = float2(3 - 2 * st.x, 1 + 2 * st.x);
float2 u = float2((2 - st.x) / uw.x - 1, (st.x) / uw.y + 1);
u *= _ShadowMapTexture_TexelSize.x;
float2 vw = float2(3 - 2 * st.y, 1 + 2 * st.y);
float2 v = float2((2 - st.y) / vw.x - 1, (st.y) / vw.y + 1);
v *= _ShadowMapTexture_TexelSize.y;
half sum = 0;
sum += uw[0] * vw[0] * UNITY_SAMPLE_SHADOW(_ShadowMapTexture, UnityCombineShadowcoordComponents(base_uv, float2(u[0], v[0]), coord.z, receiverPlaneDepthBias));
sum += uw[1] * vw[0] * UNITY_SAMPLE_SHADOW(_ShadowMapTexture, UnityCombineShadowcoordComponents(base_uv, float2(u[1], v[0]), coord.z, receiverPlaneDepthBias));
sum += uw[0] * vw[1] * UNITY_SAMPLE_SHADOW(_ShadowMapTexture, UnityCombineShadowcoordComponents(base_uv, float2(u[0], v[1]), coord.z, receiverPlaneDepthBias));
sum += uw[1] * vw[1] * UNITY_SAMPLE_SHADOW(_ShadowMapTexture, UnityCombineShadowcoordComponents(base_uv, float2(u[1], v[1]), coord.z, receiverPlaneDepthBias));
shadow = sum / 16.0f;
#endif
return shadow;
}
UnitySampleShadowmap_PCF5x5Gaussian函數和UnitySampleShadowmap_PCF3x3Gaussian函數算法思想類似。而UnitySampleShadowmap_PCF3x3等各個PCF函數則是轉調用UnitySampleShadowmap_PCF3x3Tent等函數實現的:
7.4.2 UnitySampleShadowmap_PCF3x3等函數
half UnitySampleShadowmap_PCF3x3(float4 coord, float3 receiverPlaneDepthBias)
{
return UnitySampleShadowmap_PCF3x3Tent(coord, receiverPlaneDepthBias);
}
half UnitySampleShadowmap_PCF5x5(float4 coord, float3 receiverPlaneDepthBias)
{
return UnitySampleShadowmap_PCF5x5Tent(coord, receiverPlaneDepthBias);
}
half UnitySampleShadowmap_PCF7x7(float4 coord, float3 receiverPlaneDepthBias)
{
return UnitySampleShadowmap_PCF7x7Tent(coord, receiverPlaneDepthBias);
}