随着游戏质量提升,传统硬阴影逐渐的不再被人们接受,各种软阴影算法渐渐的出现在我们的视野当中,最为流行的几种软阴影方法为:Variance Shadow Map(VSM),Convolution Shadow Maps(CSM),Exponential Shadow Maps(ESM),Moments Shadow Map(MSM)以及Percentage Closer Soft Shadows(PCSS)。
VSM,CSM,ESM都可以和PCSS结合使用,因此我们首先学习PCSS。
PCSS
PCSS是具有动态采样半径的PCF方法。其根据光源半径,以及光源,遮挡物与接收面的位置来决定PCF的采样半径。该方法分为三个步骤:
1. 计算遮挡物的平均深度。
2. 计算半影区域。
3. 执行PCF。
1. 计算遮挡物的平均深度
我们可以人为定义一个遮挡物搜索半径,将片段位置转化为光空间,采样搜索半径内的shadow map,累加所有深度小于当前深度的值,对其求平均即得到我们的平均遮挡物深度。下图假设了当前深度为7所找到的遮挡物深度(蓝色标记)。
然而我们可以使用一个更好的方法计算遮挡物的搜索半径。我们可以根据三角形相似的原理,来获得灯光覆盖的像素区域,如下图所示:
片段距离光源越远,则搜索半径越大,我们计算遮挡物半径的代码如下:
#define SHADOW_HEIGHT_WORLD (2.0f * tan_fov * shadow_near)
float LinearizeDepth(float depth){
float z=depth * 2.0f - 1.0f;
float denominator = (shadow_far + shadow_near - (shadow_far - shadow_near) * z);
return (2.0f * shadow_near * shadow_far) / denominator;
}
float calBlockSearchRange(vec3 ndc_texCoord){
float LS_depth = LinearizeDepth(ndc_texCoord.z);
// world space range
float light_range = LightSize / LS_depth * (LS_depth - shadow_near);
// pixel space range
float range_pixel = light_range / SHADOW_HEIGHT_WORLD;
return range_pixel;
}
计算平均遮挡深度只需要对遮挡物搜索半径内的像素进行对比,并累加平均深度小于当前深度值即可。但是,如果计算得出的搜索半径很大,则会严重降低运行效率,因此我们使用柏松圆盘采样,使每次采样个数是一个固定值。代码如下:
float calBlockDepth(float pixel_range, vec3 ndc_texCoord){
float ave_depth = 0.0f;
int block_num = 0;
float current_depth = ndc_texCoord.z;
for (int i = 0; i < SAMPLE_NUMB; i++) {
float t_depth = texture(depth_tex, ndc_texCoord.xy + pixel_range * PoissonDisk[i]).r;
if(t_depth < current_depth - shadow_bias){
ave_depth += LinearizeDepth(t_depth);
block_num++;
}
}
if(block_num < 1)
return -1.0;
else
return ave_depth / float(block_num);
}
2. 计算半影区域
我们假定光源总是平行于渲染的片段,根据三角形相似的原理,计算半影区,原理如下图:
值得注意的是,遮挡物搜索中采用的相似为光源的半径,而这里的相似采用光源的直径。我们还需要保证深度为线性,若shadow map为正交投影得出,则深度就是线性的。若使用透视投影得出,我们需要手动将其转为线性深度。计算半影区代码如下:
float calPenumbra(float depth,float block_dep){
float linear_depth = LinearizeDepth(depth);
float w_penumbra_world = (linear_depth - block_dep) * 2.0 * LightSize / block_dep;
float w_penumbra_pixel = w_penumbra_world * shadow_near / (SHADOW_HEIGHT_WORLD * linear_depth);
return w_penumbra_pixel;
}
3. 执行PCF
计算得到像素空间的半影区将作为PCF的采样半径,我们的代码如下:
float calPCF(float w_penumbra, vec3 ndc_texCoord){
float t_shadow = 0.0f;
float current_depth = ndc_texCoord.z;
for (int i = 0; i < SAMPLE_NUMB; i++) {
float sample_depth = texture(depth_tex, ndc_texCoord.xy + w_penumbra * PoissonDisk[i]).r;
if(current_depth > 1.0f || current_depth - shadow_bias < sample_depth)
t_shadow += 0.0f;
else
t_shadow += 1.0f;
}
return t_shadow / float(SAMPLE_NUMB);
}
同样使用柏松圆盘采样法进行采样。这里我们的柏松圆盘通过预计算的方式,硬编为采样数组。
柏松圆盘采样
柏松圆盘采样点的特点是任意一个采样点,距离其周围最近的采样点的距离始终在(Dmin,Dmax)之内。
其算法步骤如下:
1. 在n维空间中生成均匀网格(本文为2维),网格大小为,r为最小距离Dmin。保证每个网格内最多存在一个采样点。
2. 创建两个队列,一个为操作队列,用来保存待操作的点。另一个为结果队列,保存所有满足条件的点。在空间内随机生成一个种子点,并加入操作队列与结果队列。
3. 从操作队列内pop一个点,如果队列为空,则算法结束。否则在取到点周围的(Dmin,Dmax)圆环半径内随机生成一个检测点b。
4. 判断检测点b的Dmin范围内是否存在其他点,如果不存在,则添加点b进操作队列和结果队列。如果存在其他点,则重新生成检测点b。如果重新生成超过k次,仍然在其周围存在其他点,我们则重新执行步骤3.。
生成点和领居判定如下图所示。
我们柏松圆盘采样代码如下:
void PCSS::calPoission(){
//init grid
int grid_num = cell_num * cell_num;
grid.resize(grid_num, glm::vec2(out_of_size));
// push first point
glm::vec2 firstPoint = glm::vec2(drand48(), drand48());
processList.push(firstPoint);
poissonDisk.push_back(firstPoint);
glm::ivec2 grid_index = imageToGrid(firstPoint);
grid[grid_index.y * cell_num + grid_index.x] = firstPoint;
while (!processList.empty()) {
glm::vec2 process_point = processList.front();
processList.pop();
for (int i = 0; i < pois_k; i++) {
glm::vec2 new_point = calAround(process_point);
//check that point is in the image region and have no neighbor
if(calCheckPoint(new_point)){
processList.push(new_point);
poissonDisk.push_back(new_point);
glm::ivec2 new_grid_id = imageToGrid(new_point);
grid[new_grid_id.y * cell_num + new_grid_id.x] = new_point;
}
}
}
}
根据需要我们用柏松圆盘计算得到固定的采样点硬编至PCSS的片段着色器内:
const vec2 PoissonDisk[SAMPLE_NUMB] = vec2[](
vec2(-0.20707,0.680971),
vec2(-0.568494,0.807044),
vec2(0.0749105,0.436834),
vec2(-0.455151,0.536164),
vec2(0.238882,0.853848),
vec2(-0.224585,0.315561),
vec2(-0.49309,0.212044),
vec2(-0.806367,0.336245),
vec2(-0.96812,0.75486),
vec2(0.373125,0.2635),
vec2(-0.102639,-0.0619002),
vec2(0.524274,0.574806),
vec2(0.634103,0.967932),
vec2(-0.385516,-0.314919),
vec2(-0.714502,-0.160928),
vec2(0.609902,-0.201941),
vec2(0.21492,-0.15106),
vec2(0.699132,0.190124),
vec2(0.846728,0.451957),
vec2(-0.0187435,-0.360953),
vec2(0.923692,0.907769),
vec2(-0.592013,-0.673695),
vec2(0.0229522,-0.68713),
vec2(-0.896051,-0.381052),
vec2(-0.244224,-0.579614),
vec2(-0.898701,-0.665722),
vec2(0.475186,-0.734744),
vec2(0.389989,-0.382208),
vec2(0.946732,0.0353365),
vec2(0.965636,-0.426523),
vec2(-0.768483,-0.933271),
vec2(-0.135119,-0.96675),
vec2(0.156406,-0.959331),
vec2(-0.480685,-0.946449),
vec2(0.647589,-0.507713),
vec2(0.975397,-0.769224),
vec2(0.667647,-0.971078)
);
最后我们得到的PCSS结果如下: