实时软阴影PCSS

实时软阴影PCSS

一、PCF(percentage-closer filtering)

1.1 原理

​​​  PCF通过多次采样阴影贴图,计算得到更加平均的深度采样值,这样生成的阴影边缘过渡更加柔和,缺点是耗费GPU资源。不同采样分布函数和采样次数决定了边缘光滑程度,下面是常用的泊松圆盘分布采样(Poisson-Disk Sample),它既有随机性又保持点之间相对均匀地分布,如下图右侧所示。

​​  泊松圆盘分布实现的glsl代码如下所示:

//hash 二维随机
float hash12(vec2 p)
{
    vec3 p3=fract(vec3(p.xyx)*0.1031);
    p3+=dot(p3,p3.yzx+33.33);
    return fract((p3.x+p3.y)*p3.z);
}
// 泊松圆盘分布
void poissonDiskSamples(vec2 randomSeed )
{
  // 初始弧度
  float angle = hash12(randomSeed) * 3.1415*3.1415;
  // 初始半径
  float INV_NUM_SAMPLES = 1.0 / float( NUM_SAMPLES );
  float radius = INV_NUM_SAMPLES;
  // 一步的弧度
  float ANGLE_STEP = 3.883222077450933;
  // 一步的半径
  float radiusStep = radius;

  for( int i = 0; i < NUM_SAMPLES; i ++ ) 
  {
    disk[i] = vec2(cos(angle),sin(angle)) * pow( radius, 0.75 );
    radius += radiusStep;
    angle += ANGLE_STEP;
  }
}

​​  a. 初始化

​​  ​​  • 首点生成:在目标区域(如矩形、圆形)内随机选择一个初始点,并加入“活动列表”。

​​  ​​  • 网格划分:将区域划分为若干网格单元,每个单元的边长与采样半径相关(通常为 \(r/\sqrt{2}\) ),用于加速邻近点检测。

​​  b. 迭代采样

​​​​    • 候选点生成:从活动列表中随机选取一个基准点,在其周围环形区域(半径 r 到 2r )内随机生成若干候选点。

​​​​    • 冲突检测:检查候选点是否与已有点距离过近(即小于 r ),或超出边界。若通过检测,则保留该点并加入活动列表。

​​  ​​  • 终止条件:当活动列表为空或达到预设点数时停止。

​​  c. 优化方法(Bridson算法)

​​  ​​  通过网格加速检测,仅在候选点的邻近网格单元中检查冲突点,大幅减少计算量。例如,每个候选点仅需检查周围 \(5 \times 5\) 网格单元内的点。

1.2 代码实现

void main()
{		
    vec3 baseCol=vec3(0.85);
    vec3 projCoords = vFragPosLightSpace.xyz/vFragPosLightSpace.w;
    projCoords = projCoords * 0.5 + 0.5;
    if(projCoords.x<0.0||projCoords.x>1.0)FragColor=vec4(baseCol,1.0);
	else if(projCoords.y<0.0||projCoords.y>1.0)FragColor=vec4(baseCol,1.0);
    else if(projCoords.z<0.0||projCoords.z>1.0)FragColor=vec4(baseCol,1.0);
    else {
        float bias = 0.0001;			
        float currentDepth = projCoords.z;
        float shadow = 0.0;
        // 采样
        for(int i = 0;i<NUM_SAMPLES;++i){
            vec2 coord=clamp(projCoords.xy + disk[i],0.01,0.99);
            float pcfDepth = texture(depthMap, coord).r; 
            shadow += currentDepth - bias > pcfDepth ? 0.0 : 1.0; 
        }
        shadow /=float(NUM_SAMPLES);
        FragColor = vec4(baseCol*shadow, 1.0);
    }
}

note:需要注意的是,

​​  a.使用 bias避免自遮挡导致的纹路

​​​  b.筛选掉不在depthMap采样范围内的projCoords坐标

二、PCSS(Percentage Closer Soft Shadows)

2.1 原理

​​  现实中光源不是点而是一块区域,阴影离投影物体越远,阴影轮廓越模糊。也就是存在所谓的penumbra半影区域。

​​  PCSS的实现分为三个步骤:

​​  1. Blocker Search,确定当前像素周围遮挡物的平均深度,在光源视角的Shadow Map 中,以当前像素为中心,在半径由光源尺寸和几何关系确定的区域内进行多重采样。若采样点深度小于当前像素深度,则记录其深度值并计算平均值。

float findBlocker(vec2 uv, float zReceiver ) 
{
  float blockNum=0;
  float dBlocker=0.0;
  vec2 texelSize=50.0/textureSize(depthMap,0);	
  float sum = 0.01;	
  for(int i = 0;i<NUM_SAMPLES;++i){
    float depthInShadowmap = texture(depthMap, uv + disk[i] *texelSize).r;
    if(depthInShadowmap+0.0001 < zReceiver){
      dBlocker += depthInShadowmap;
      blockNum += 1.0;
    }
  }
  return dBlocker/blockNum;
}

​​  2. Penumbra Estimation ,基于相似三角形原理计算得到。

\[w_{Penumbra} =(d_{Receiver} −d_{Blocker} )⋅w_{Light} /d_{Blocker} \]

\(w_{Light}\)为光源尺寸,\(d_{Blocker}\)为遮挡物深度,\(d_{Receiver}\)为投影平面深度,\(w_{Penumbra}\)为半影长度。

​​  3. PCF 阴影,根据计算出的半影大小调整PCF的采样范围,对shadow map进行多重采样,通过比较深度值计算阴影可见性的加权平均值,生成软阴影过渡效果。

2.2 实现

void main()
{		
    vec3 baseCol=vec3(0.85);
    vec3 projCoords = vFragPosLightSpace.xyz/vFragPosLightSpace.w;
    projCoords = projCoords * 0.5 + 0.5;
    if(projCoords.x<0.0||projCoords.x>1.0)FragColor=vec4(baseCol,1.0);
	else if(projCoords.y<0.0||projCoords.y>1.0)FragColor=vec4(baseCol,1.0);
    else if(projCoords.z<0.0||projCoords.z>1.0)FragColor=vec4(baseCol,1.0);
    else {
        float bias = 0.0001;			
        float currentDepth = projCoords.z;
        float shadow = 0.0;
        // 初始化泊松分布
        poissonDiskSamples(projCoords.xy);
        // STEP 1: avgblocker depth
        float dBlocker = findBlocker(projCoords.xy,projCoords.z);
        // STEP 2: penumbra size
        const float wLight = 1.0;
        float wPenumbra = (projCoords.z-dBlocker)/dBlocker * wLight;
        // 采样
        for(int i = 0;i<NUM_SAMPLES;++i){
            vec2 coord=clamp(projCoords.xy + disk[i]*wPenumbra,0.01,0.99);
            float pcfDepth = texture(depthMap, coord).r; 
            shadow += currentDepth - bias > pcfDepth ? 0.0 : 1.0; 
        }
        shadow /=float( NUM_SAMPLES);

        FragColor = vec4(baseCol*shadow, 1.0);
    }
}

posted @ 2025-04-20 12:05  王小于的啦  阅读(216)  评论(0)    收藏  举报