OpenGL阴影映射技术介绍

OpenGL阴影映射技术介绍

SM

概述: Shadow Mapping,最基本的阴影算法之一,通过使用光源的视角渲染场景的深度信息来生成阴影,其核心思想时将光源的深度信息存储在一个深度贴图中,在进行渲染时,利用该深度信息来判断每个片段是否在阴影中

步骤:

  1. 生成深度贴图:对场景进行一次光源视角的渲染,生成一个深度贴图,该贴图存储了光源照射到场景中的每个像素的深度值
    • 在光源的视角中,进行正交或透视投影渲染
    • 将深度值存储在深度贴图中
  2. 使用深度贴图进行阴影测试:在场景的主渲染阶段,首先将每个片段(像素)的世界坐标转换到光源的视角空间,然后在光源的深度贴图查询该位置的深度值,如果当前片段的深度值大于深度贴图中的值,说明该片段在阴影中

算法实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
float SM(vec4 fragPosLightSpace,vec3 normal,vec3 lightDir,sampler2D shadowMap){
// 转换为标准齐次坐标 z[-1, 1]
vec3 projCoords=fragPosLightSpace.xyz/fragPosLightSpace.w;
// xyz: [-1, 1] -> [0, 1]
projCoords=projCoords*.5+.5;
// 只要投影向量的z坐标大于1.0或小于0.0,就把shadow设置为1.0(即超出了光源视锥体的最远处,这样最远处也不会处在阴影中,导致采样过多不真实)
if(projCoords.z>1.||projCoords.z<0.)
return 0.;

// 从光源视角看到的深度值(从阴影贴图获取
float closestDepth=texture(shadowMap,projCoords.xy).r;
// 从摄像机视角看到的深度值
float currentDepth=projCoords.z;
// 偏移量,解决阴影失真的问题, 根据表面朝向光线的角度更改偏移量
float bias=max(.05*(1.-dot(normal,lightDir)),.005);
/// 常规做法:如果currentDepth大于closetDepth,说明当前fragment被某个物体遮挡住了,在阴影之中
float shadow=currentDepth-bias>closestDepth?1.:0.;

return shadow;
}

效果图:

PCF

概述: Percentage Closer Filter是对阴影贴图的一种后处理技术,用于减少阴影的锯齿状边缘,通过对阴影贴图中的多个样本进行采样,计算它们的平均值,从而实现阴影的模糊过渡

步骤:

  1. 多次采样:在阴影测试阶段,针对每个片段,不仅仅检查单个像素的深度值,而是对一个小范围内的小像素进行采样(例如3×3或5×5的采样区域)
  2. 计算平均值:对采样的范围进行阴影测试,如果当前片段大于从深度贴图采样的值,认为其在阴影中,累加到shadow变量中,最后对shadow变量取平均值

算法实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
float PCF(vec4 fragPosLightSpace,vec3 normal,vec3 lightDir,sampler2D shadowMap){
// 转换为标准齐次坐标 z[-1, 1]
vec3 projCoords=fragPosLightSpace.xyz/fragPosLightSpace.w;
// xyz: [-1, 1] -> [0, 1]
projCoords=projCoords*.5+.5;
// 只要投影向量的z坐标大于1.0或小于0.0,就把shadow设置为1.0(即超出了光源视锥体的最远处,这样最远处也不会处在阴影中,导致采样过多不真实)
if(projCoords.z>1.||projCoords.z<0.)
return 0.;

// 从光源视角看到的深度值(从阴影贴图获取
float closestDepth=texture(shadowMap,projCoords.xy).r;
// 从摄像机视角看到的深度值
float currentDepth=projCoords.z;
// 偏移量,解决阴影失真的问题, 根据表面朝向光线的角度更改偏移量
float bias=max(.05*(1.-dot(normal,lightDir)),.005);
/// PCF:
float shadow=0.;
// 计算每个纹素的大小
vec2 texelSize=1./textureSize(shadowMap,0);
// 遍历3x3的邻域
for(int x=-PCF_RADIUS;x<=PCF_RADIUS;++x)
{
for(int y=-PCF_RADIUS;y<=PCF_RADIUS;++y)
{
// 从阴影贴图中采样深度值
float pcfDepth=texture(shadowMap,projCoords.xy+vec2(x,y)*texelSize).r;
// 如果当前片段的深度值大于采样的深度值,则在阴影中
shadow+=currentDepth-bias>pcfDepth?1.:0.;
}
}
// 计算平均阴影值
float total=2*PCF_RADIUS+1;
shadow/=(total*total);

return shadow;
}

特点: PCF的优点是相对简单,但可能会导致较为模糊的阴影,且在高分辨率阴影贴图上会更有效。

效果图:

PCSS

概述: Percentage Closer Soft Shadows是一种基于PCF的扩展,旨在生成更自然的软阴影效果。PCSS通过在阴影贴图的采样过程中引入光源半径的概念,来根据片段距离光源的远近来调整阴影的柔和成都

步骤:

  1. 计算光源的大小:由于距离光源较远的区域阴影边缘应该更加模糊,PCSS根据光源的大小(即阴影的投射区域)来动态调整阴影的软化程度
  2. 应用阴影半径:对于每个片段,计算该片段距离光源的距离,并基于该距离来调整PCF的采样区域大小。距离较远的片段采用较大的采样区域,从而产生更软的阴影

算法实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
float PCSS(vec4 fragPosLightSpace,vec3 normal,vec3 lightDir,sampler2D shadowMap){
// 转换为标准齐次坐标 z[-1, 1]
vec3 projCoords=fragPosLightSpace.xyz/fragPosLightSpace.w;
// xyz: [-1, 1] -> [0, 1]
projCoords=projCoords*.5+.5;
// 只要投影向量的z坐标大于1.0或小于0.0,就把shadow设置为1.0(即超出了光源视锥体的最远处,这样最远处也不会处在阴影中,导致采样过多不真实)
if(projCoords.z>1.||projCoords.z<0.)
return 0.;

// 从光源视角看到的深度值(从阴影贴图获取
float closestDepth=texture(shadowMap,projCoords.xy).r;
// 从摄像机视角看到的深度值
float currentDepth=projCoords.z;
// 偏移量,解决阴影失真的问题, 根据表面朝向光线的角度更改偏移量
float bias=max(.05*(1.-dot(normal,lightDir)),.005);
/// PCSS:
// 计算平均遮挡物体的深度值
float avgDepth=findBlocker(projCoords.xy,currentDepth,shadowMap,bias);
// 如果没有遮挡物体,则直接返回0.0(不在阴影中)
if(avgDepth==-1.){
return 0.;
}
// 半影大小
float penumbra=(currentDepth-avgDepth)/avgDepth*lightWidth;
// 采样半径
float filterRadius=penumbra*NEAR_PLANE/currentDepth;
// PCF
filterRadius*=PCFSampleRadius;
float shadow=0.;
// 计算每个纹素的大小
vec2 texelSize=1./textureSize(shadowMap,0);
// 遍历邻域
for(int x=-PCF_RADIUS;x<=PCF_RADIUS;++x)
{
for(int y=-PCF_RADIUS;y<=PCF_RADIUS;++y)
{
// 从阴影贴图中采样深度值
float shadowMapDepth=texture(shadowMap,projCoords.xy+filterRadius*vec2(x,y)*texelSize).r;
// 如果当前片段的深度值大于采样的深度值,则在阴影中
shadow+=currentDepth-bias>shadowMapDepth?1.:0.;
}
}
// 计算平均阴影值
float total=2*PCF_RADIUS+1;
shadow/=(total*total);

return shadow;
}

特点: PCSS能够产生更加平滑和逼真的阴影效果,但相对于普通的PCF,计算开销较大。

效果图:

VSM

概述: Variance Shadow Mapping 是一种通过利用方差来改进传统阴影贴图的方法,与传统的阴影贴图不同,VSM使用每个光源的深度值的均值和方差来创建阴影,这样可以在计算中处理阴影的模糊化,同时避免了对深度贴图的多次采样

步骤:

  1. 存储深度和方差:在深度贴图中,不仅存储每个像素的深度值,还存储该深度值的方差,这样,光源照射到场景中的区域可以通过方差值来表示阴影的软化程度
    • 第一通道存储深度值(z)
    • 第二通道存储深度值的方差(\(\delta^2\))
  2. 深度比较:在片段着色器中,使用一个不等式来判断一个片段是否被阴影遮挡,由于存储了方差,阴影边缘会变得更平滑

算法实现:

  • 需要两个纹理来存储对应点周围区域内的方差和均值
  • 两趟pass可以把每个点的复杂度从\((2R+1)^2\)降到\((4R+2)\)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
float VSM(vec4 fragPosLightSpace,vec3 normal,vec3 lightDir,sampler2D d_d2_filter){
vec3 projCoords=fragPosLightSpace.xyz/fragPosLightSpace.w;
// [-1, 1] => [0, 1]
projCoords=projCoords*.5+.5;
if(projCoords.z>1.||projCoords.z<0.)
return 0.;

depth=projCoords.z;

// 从模糊后的纹理中获得深度值均值和方差
d_d2=texture(d_d2_filter,projCoords.xy).rg;
float var=d_d2.y-d_d2.x*d_d2.x;// E(X-EX)^2 = EX^2-E^2X

// 偏移量,解决阴影失真的问题, 根据表面朝向光线的角度更改偏移量
float bias=max(.05*(1.-dot(normal,lightDir)),.005);
// float bias=.005;
float visibility;
if(depth-bias<d_d2.x){
visibility=1.;// 没有阴影
}
else{
// 使用切比雪夫不等式计算阴影
float t_minus_mu=depth-d_d2.x;
visibility=var/(var+t_minus_mu*t_minus_mu);
}
return 1.-visibility;
}

特点: VSM能提供相对较好的阴影效果,且具有较低的计算开销,但可能会面临一些噪声问题,特别是在低对比度区域

效果图:

与PCSS对比:

PCSS:

VSM:

CSM

概述: Cascaded shadow map(联级阴影贴图)

(待补充)

其他

参考:


OpenGL阴影映射技术介绍
http://example.com/2024/12/02/OpenGL阴影映射技术介绍/
作者
凌云行者
发布于
2024年12月2日
许可协议