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宏时,表示使用聚光灯生成,如下代码:
#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个偏移采样点。
UNITY_DECLARE_SHADOWMAP宏在HLSLSupport.cginc文件中定义,用来声明一个阴影纹理。除此之外,在文件中还定义了其他对阴影进行操作的宏,如进行采样操作的宏UNITY_SAMPLE_SHADOW、进行投影计算的宏UNITY_SAMPLE_SHADOW_PROJ。这些宏在不同的平台下使用不同版本的着色器编译器,有着不同的实际定义。
如果能使用着色器本身支持的阴影相关的操作函数,即宏SHADOW_NATIVE被启用,就直接使用这些函数,如果不支持,就使用普通2D纹理函数对纹理进行采样。这些函数Unity3D已经用宏包装了一层。
#if defined(SHADER_API_PSP2)
half SAMPLE_DEPTH_TEXTURE(sampler2D s, float4 uv) { return tex2D(s, (float3)uv); }
half SAMPLE_DEPTH_TEXTURE(sampler2D s, float3 uv) { return tex2D(s, uv); }
half SAMPLE_DEPTH_TEXTURE(sampler2D s, float2 uv) { return tex2D(s, uv); }
# define SAMPLE_DEPTH_TEXTURE_PROJ(sampler, uv) (tex2DprojShadow(sampler, uv))
# define SAMPLE_DEPTH_TEXTURE_LOD(sampler, uv) (tex2Dlod(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(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
理解了宏的定义,继续回到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
#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中)。贴图中某一点纹素所存储的深度值,即某处离光源最远且光线能照射到的那个位置的深度值。
下面的代码展示了如何从一个立方体纹理贴图中取得某纹素对应的深度值,通常有一个有着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进行不同精度的采样。
实现了从立方体贴图中进行采样之后就可以根据当前位置进行阴影判断了。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
//启用了光探针代理体才可使用本函数
#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
// 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));
在上述代码段中,宏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也是采样主光照贴图的采样器。
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,可以从客户端向引擎填充此数值。
此函数可以对实时阴影和烘焙阴影进行混合。这个函数主要思想:按平常的做法衰减实时阴影,然后取其和烘焙阴影的最小值:
// ------------------------------------------------------------------
// 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);
}
为了淡化阴影,要定义以下两个函数:
// ------------------------------------------------------------------
// 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);
}
阴影深度贴图中存储的是片元的深度,一般地,贴图的纹素一般只使用一个颜色通道存储深度值,因此可以将阴影深度贴图等同于一张灰度图。如果将贴图中每一个纹素的灰度值看作纹理映射坐标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
解决阴影渗漏最直接方法就是把计算出的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的算法式。
//根据给定的在屏幕空间中的阴影坐标值,计算阴影接受平面的深度偏移值
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)函数需要用到片元的属性,因此只能在片元着色器中使用它们。
//组合一个阴影坐标的不同分量并返回最后一下分量 参数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方法具体步骤是:在片元着色器中,把当前操作的片元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。
当目标平台没有使用硬件实现的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;
}
纹素的采样格式取决于采样内核所执行的区域大小。一个纹素个数为mXm的深度阴影贴图,如果要把所有纹素都用来进行采样,需要n=m的平方采样计算,这显然是很不切实际的。因此,Unity3D引擎使用了一些减少PCF纹素采样次数的优化算法。
对于下图所示的采样内核(也称为滤波器),如果不经算法优化,即便使用直接支持一次取样4个纹素的硬件指令,也依然需要做9次纹理采样操作。而经过优化后只需要做4次就能达到同样的效果。纹理采样次数直接决定了滤波操作的性能效率。经过优化算法后,可以把一个n阶的滤波器(n-1)平方采样降到(n/2)平方次采样,从而大大提升执行效率。
Unity使用了若干个不同规格的等腰直角三角形,在4阶、6阶、8阶采样内核上进行覆盖,以获取不同纹素对阴影的贡献程度,然后遵循n阶采样内核执行(n/2)的平方次采样的规则进行PCF处理,下面的代码就是进行PCF操作的一系列工具函数。
//根据给定的三角形的高triangleHeight,得到以triangleHeight值为高的等腰直角三角形的面积
float _UnityInternalGetAreaAboveFirstTexelUnderAIsocelesRectangleTriangle(float triangleHeight)
{
return triangleHeight - 0.5;
}
上述代码根据一个给定高的等腰直角三角形计算该三角形的面积。该三角形的高以纹素为单位,因此,直接令高减去0.5即可得其面,如下图所示,可知等腰直角三角形的高和面积之间的数值关系。
在下面代码中给定一个高为1.5纹素、底为3纹素的等腰直角三角形,在不同的水平方向偏移值的情况下,对于覆盖在4个连续的纹素方格上的这个三角形,求每个方格上放置的部分的面积。
//本函数假定本等腰三角形的高为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函数获取到部分三角形后再除以总面积。
//本函数假定等腰直角三角形的高为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所示:
//本函数假定一个等腰直角三角形的高为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);
}
获取了在不同纹素下三角形覆盖比例之后,可以根据减少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函数类似。
百分比切近过滤技术是非加权的区域采样方法,可以对采样区域进行加权的方法使得相交区域对片元的亮度的贡献依赖于该区域与片元中心的距离。当直线经过某一个片元时,该片元的亮度F是在两者相交的区域A上,对滤波器函数W(x,y)的积分。
高斯模糊(Gaussian blur)滤波技术就是基于加权区域采样的思想,对待区域进行模糊处理,把高斯模糊应用在硬阴影的边缘时,可以对硬阴影的边缘锯齿现象进行模糊化,产生软阴影的边缘柔化效果。高斯模糊效果的滤波器函数为高斯滤波器,滤波器函数W(x,y)和亮度F如下:
从上面2个式子可知,要求解某片元F处的亮度值计算量非常大,可以采用离散计算的方法去模拟计算。首先将片元均匀分割成n个子片元,则每个子片元的所在面积为1/n;然后计算每个子片元对原片元的亮度贡献,并将其保存在一个二维加权表中;接着求出所有中心落于直线段内的子片元,并计算这些子片元对原片元亮度的贡献的和。假如每个片元可以划分为n=3X3个子片元,则加权表可以设置为:
当n越大的时候,高斯分布曲面就越平滑。高斯模糊是根据高斯公式先计算出周围片元对需要模糊的那个片元的影响程度,即权重值,然后对图像中该像素的颜色值进行卷积计算,最后得到该片元的颜色值。
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等函数实现的:
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);
}