上文介绍的PCSS方法,其在平均遮挡物的计算和PCF需要大量的采样加权平均操作,如果半影如果半影区相当大,采样效率将相当低下。如果使用柏松圆盘进行固定步数采样,在较大的半影区情况下会出现明显的阴影分片伪影:
SAVSM则使用了一种高效的手段,避免了多次的范围采样,无论多大的半影区,只需要进行4次采样即可获得平滑过度的阴影,效果如下:
SAVSM
SAVSM是将summed-area-table(SAT)用于variance shadow map(VSM)。在此之前,VSM往往采用硬件生成Mipmap,以插值获取一定区域内的深度平均值。这样的插值往往带来较大的误差,而SAT可以获取准确的深度平均值。PCSS中的遮挡物搜索和PCF都可以用VSM的切比雪夫不等式去代替,避免多次的范围采样,再提升效率的同时,还能获得平滑过度的阴影。SAVSM和PCSS结合的总体流程代码如下:
float calVSMShadow(){
vec3 pixel_texCoord = (screen_space_M * (oLightSpacePos / oLightSpacePos.w)).xyz;
// find block search range (0~1)
float pixel_range = calBlockSearchRange(pixel_texCoord);
// calculate average block using Chebyshev
float block_dep = calBlockDepth(pixel_range, pixel_texCoord);
if (block_dep >= 1.0) {
return 1.0f;
}
// calculate penumbra
float penumbra_pixel = calPenumbra(pixel_texCoord.z, block_dep);
//calculate shadow
float final_shadow = calShadow(pixel_texCoord, penumbra_pixel);
return final_shadow;
}
VSM
vsm核心在于利用切比雪夫不等式估计一定范围内深度大于当前深度的概率。我们的PCF计算的比值正好就是一定区域内,深度大于当前像素的个数与该区域的总像素个数的比。这正好就是切比雪夫不等式所计算得出的比值。切比雪夫不等式如下:
其中是深度值的方差,E(x)是期望(均值)。在使用时,我们直接将该不等式作为等式使用,以获得深度大于当前片段的概率。这里可以用到概率论里经常使用的计算方差的公式:
那么我们只需要深度的期望和深度平方的期望,这可以在生成shadow map的时候多增加一个通道保存深度的平方即可。而计算均值的方式就是使用SAT,无论多大区域的平均都可以直接以4次采样获得。若则表示当前片段受到直接照明。值得注意的是我们必须使用线性深度来保证准确性。由切比雪夫不等式计算的非遮挡比率代码如下:
float ChebyshevUpperBound(vec2 moments, float t) {
// One-tailed inequality valid if(t > Moments.x)
float p = float(t <= moments.x);
// Compute variance.
float variance = moments.y - (moments.x * moments.x);
variance = max(variance, g_MinVariance);
// Compute probabilistic upper bound.
float d = t - moments.x;
float p_max = variance / (variance + d * d);
// Reduce light bleeding
p_max = ReduceLightBleeding(p_max, g_amount);
return max(p, p_max);
}
上段代码中我们定义最小方差g_MinVariance,这可以有效的解决shadow map的自遮挡问题。由于受到直接照明的片段的方差非常小,且的值始终在零的附近,因此容易产生明暗交加的条纹,而如果使用SAT,精度会产生极大的损耗,明暗条纹将变为噪声,未加最小方差约束的效果如下图:
自遮挡
有一个最小方差的代替方法,其将每个像素采样到的深度作为局部的平面分布:
之后计算矩的深度平方项,其中线性的期望算子 E(x) = E(y) = E(xy) = 0:
之后将像素表示为具有半像素标准偏差的对称高斯分布:
因此,更新后的表现形式如下:
计算矩(深度项和深度平方项)的代码如下:
vec2 ComputeMoments(float depth){
vec2 moments;
// First moment is the depth itself. (sub 0.5 for Improve accuracy )
moments.x = depth;
moments.y = depth * depth;
// Compute partial derivatives of depth.
float dx = dFdx(depth);
float dy = dFdy(depth);
// Compute second moment over the pixel extents.
moments.y += 0.25 * (dx * dx + dy * dy);
return moments;
}
Light-Bleeding
VSM一个最大的缺点就是会出现Light-Bleeding,如下图:
这种现象非常容易出现在多个平面重叠的简单场景,因为这种场景本身就不符合使用切比雪夫不等式时假定的分布为高斯分布。这种情况会导致方差极大,从而使非遮挡比率趋近于1。Light-Bleeding出现的区域往往在其上一个遮挡面的半影区附近,如下图:
我们可以使用重映射的方式去除Light-Bleeding,我们可以让非遮挡比率小于一定阈值时,直接判定其完全处于遮挡状态,这通过牺牲一点半影区来去除Light-Bleeding。代码如下:
float linstep(float t_min, float t_max, float v){
return clamp((v - t_min) / (t_max - t_min), 0.0f, 1.0f);
}
float ReduceLightBleeding(float p_max, float Amount){
// Remove the [0, Amount] tail and linearly rescale (Amount, 1].
return linstep(Amount, 1.0f, p_max);
}
SAT生成
如果在CPU创建SAT,则需要O(N*M)的时间复杂度,我们选择使用GPU创建SAT。使用GPU仅需要M+N的复杂度。SAT的表达式如下:
这表示表格内任何一个位置的值为:从初始位置开始到当前位置围成的矩形内所有元素的和。若我们需要计算给定区域的均值,我们只需要知道给定区域的元素和与区域的大小,如下图所示:
若我们需要图中右下角的四个网格的元素和,其计算公式为:
图中为28 - 8 - 6 + 2 = 16。我们glsl采样SAT表的代码如下:
vec2 getEmoment(vec2 texcoords, vec2 pixel_range){
vec2 tex_size = 1.0f / textureSize(sat_tex,0);
vec2 grid = texcoords / tex_size;
vec2 t_range_grid = pixel_range / tex_size;
t_range_grid = min(t_range_grid, max_range_grid);
t_range_grid = max(t_range_grid, min_range_grid);
vec2 left_top = texcoords - (t_range_grid + 1.0f) * tex_size;
vec2 right_down = texcoords + t_range_grid * tex_size;
vec2 E_moment = RecombinePrecision(texture(sat_tex, right_down)) - RecombinePrecision(texture(sat_tex, vec2(left_top.x, right_down.y))) - RecombinePrecision(texture(sat_tex, vec2(right_down.x, left_top.y))) + RecombinePrecision(texture(sat_tex, left_top));
vec2 demon = (right_down - left_top) / tex_size;
E_moment /= (demon.x * demon.y);
return E_moment;
}
数值精度
如果我们计算512*512的SAT。数值精度的损失会成为一个显著的问题。实验结果如下:
由于物体腿部的遮挡深度接近接受面的深度,且其方差趋于零。这在一定的精度损失的情况下,将产生明显的噪声,如下图所示:
我们可以有几种方式增加精度,最直接的方式是让我们的帧缓存保存R通道和G通道都为32位的浮点型。然而只使用这样的方法仍会有噪声存留。我们可以通过在记录矩的时候对其减去0.5,在读取SAT的时候增加0.5。这使我们在生成SAT时,能利用符号位带来的精度提升。
另一种方法是为我们的矩帧缓存添加BA位,使其能够保存4个32位的浮点数。RG位保存整数部分,BA位保存小数部分。代码如下:
vec4 highPrecisionAdd(vec2 moments){
float inv_bit = 1.0f / precise_bit;
vec2 intPart;
vec2 fracPart = modf(moments * precise_bit, intPart);
return vec4(intPart * inv_bit, fracPart);
}
vec2 RecombinePrecision(vec4 value){
vec2 res = value.xy;
float inv_bot = 1.0f / precise_bit;
return res + value.zw * inv_bot;
}
这几种提升精度的方法可以一起使用,使我们的精度损失达到一个可接受的效果。
我们补全利用PCSS计算平均遮挡深度和计算阴影的代码(半影区计算和深度搜索范围计算代码与PCSS一致):
float calBlockDepth(float pixel_range, vec3 pixel_texCoord){
float current_depth = LinearizeDepth(pixel_texCoord.z);
//Chebyshev
vec2 E_moment = getEmoment(pixel_texCoord.xy, vec2(pixel_range));
float P_nonblock = ChebyshevUpperBound(E_moment, current_depth);
float block_depth = (E_moment.x - P_nonblock * (current_depth)) / (1.0f - P_nonblock);
if (block_depth >= current_depth || P_nonblock > 0.99) {
return 1.0f;
}
return block_depth;
}
float calShadow(vec3 pixel_texCoord, float penumbra_pixel){
float linear_depth = LinearizeDepth(pixel_texCoord.z);
vec2 E_moment = getEmoment(pixel_texCoord.xy, vec2(penumbra_pixel));
float P_nonblock = ChebyshevUpperBound(E_moment, linear_depth);
return P_nonblock;
}
最后SAVSM的结果如下图: