实时阴影技术

基础Shadow Map

渲染流程

1. 从光源位置看向场景,渲染一张深度纹理(Shadow Map),记录每个像素的最小深度值(即光源到最近物体的距离)。

  • 设置光源的视角和投影矩阵。对于方向光,通常使用正交投影;对于点光源/聚光灯,使用透视投影。 alt text
  • 对方向光,最简单的shadowmap直接覆盖场景范围(和摄像机视锥无关了),生成一张 shadowmap
  • 对点光源,需要创建一个光周围的深度值的立方体贴图,我们必须渲染场景6次:每次一个面。显然渲染场景6次需要6个不同的视图矩阵,每次把一个不同的立方体贴图面附加到帧缓冲对象上。
  • 因此点光源性能开销较大

2. 在主摄像机渲染场景时,将每个像素的世界坐标变换到光源空间,比较其深度与阴影图中存储的深度:若当前深度大于阴影图中的深度,说明该点被遮挡,处于阴影中;否则被照亮。

普通阴影渲染的问题

阴影失真(Shadow Acne)

受阴影贴图的分辨率影响,当多个片段距离光源比较远的时候,它们可能从深度贴图中采样相同的深度值。图片中,每个斜坡代表深度贴图一个单独的纹理像素。你可以看到,多个片段会采样相同的深度值。 比如最左侧一黑一黄的片段都采样到一个shadowmap的纹素,但是把他们的世界坐标变换到光源空间比较深度时,就会导致黑色深度更小没被遮挡,而黄色深度更大被遮挡。

此时可以加一个阴影偏移(shadow bias),我们简单的对表面的深度(或深度贴图)应用一个偏移量,这样片段就不会被错误地认为在表面之下了:

但 bias 过大了可能会导致阴影悬浮(Peter Panning)

Games202

走样(Aliasing)

因为深度贴图的分辨率固定,一个纹理像素可能覆盖了多个片段,结果就是多个片段会从深度贴图中采样相同的深度值,并得到相同的阴影判定结果,这也就导致了图中的锯齿边缘。

这块可以看: https://learnopengl-cn.github.io/05%20Advanced%20Lighting/03%20Shadows/01%20Shadow%20Mapping/#_7

Percentage Closer Filtering (PCF)

主要是想解决走样而非实现软阴影,但后来人们也想应用到软阴影上。

核心思想是多次采样深度贴图,每一次采样的纹理坐标都稍有不同,独立判断每个采样点的阴影状态后,将子结果混合取平均,最终获得相对柔和的阴影。

最简单的采样深度贴图周边纹理像素,并取平均值:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
float shadow = 0.0;
vec2 texelSize = 1.0 / textureSize(shadowMap, 0);
for(int x = -1; x <= 1; ++x)
{
    for(int y = -1; y <= 1; ++y)
    {
        float pcfDepth = texture(shadowMap, projCoords.xy + vec2(x, y) * texelSize).r; 
        shadow += currentDepth - bias > pcfDepth ? 1.0 : 0.0;        
    }    
}
shadow /= 9.0;

因为这只是最终采样做的手段,所以也可以和 CSM 之类的阴影技术结合。

从信号角度来看,PCF 就是对是否是阴影的这么一个可见性函数进行低通滤波,在边缘处信号量变化剧烈,所以需要先低通滤波再去采样。

CSM (Cascaded Shadow Maps)

针对方向光(如太阳)的大范围场景,CSM 是最常用的阴影技术。它将摄像机的视锥体按深度分割成多个级联(cascade),每个级联对应一张阴影图,覆盖近到远不同距离的区域。

渲染流程

1. 分割视锥体:根据深度将视锥体划分为若干区间(如4个)。分割方式可以是均匀分割、对数分割或混合分割(如practical split scheme,结合均匀和对数)。

2. 计算每个级联的光源正交投影矩阵

  • 对每个级联,计算其包围盒在世界空间中的范围。
  • 调整包围盒以匹配光源方向(通常让包围盒与光源方向对齐,但保持正交投影的轴对齐性质,或使用“Stabilize Cascades”技术避免阴影抖动如用包围球避免旋转的抖动)。

3. 渲染阴影图:为每个级联渲染一张深度图。

4. 主渲染

  • 在片段着色器中,根据当前像素的深度确定它属于哪个级联。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
// select cascade layer
vec4 fragPosViewSpace = view * vec4(fragPosWorldSpace, 1.0);
float depthValue = abs(fragPosViewSpace.z);
    
int layer = -1;
for (int i = 0; i < cascadeCount; ++i)
{
    if (depthValue < cascadePlaneDistances[i])
    {
        layer = i;
        break;
    }
}
if (layer == -1)
{
    layer = cascadeCount;
}
    
vec4 fragPosLightSpace = lightSpaceMatrices[layer] * vec4(fragPosWorldSpace, 1.0);
  • 使用对应级联的阴影图进行阴影测试。
  • 在级联边界处可能需要进行混合,避免突然的过渡。

阴影抖动

原因

1. 当摄像机移动或旋转时,视锥子区域发生变化,导致每个级联的投影矩阵随之改变。

  • 平移变化:摄像机移动时,包围盒的位置平移。
  • 缩放变化:视锥子区域的远近、宽高变化可能导致包围盒大小改变。
  • 旋转影响:摄像机旋转时,视锥子区域的方向变化,其轴对齐包围盒会膨胀或收缩,导致投影矩阵的缩放因子突变。
  • 这些变化使得同一个世界空间点在不同帧中映射到阴影贴图的不同纹素位置。即使场景完全静止,阴影比较结果也可能因为纹素偏移而改变,从而产生抖动。

2. 当物体从一个级联区域移动到另一个时,如果两个级联的阴影贴图分辨率或投影参数不同,阴影可能突然变化,造成跳变。

解决手段

Fit to scene vs. fit to cascade

上图左边为 Fit to Scene,每个CSM的包围盒都包围前一个包围盒,即CSM2包围CSM1,CSM3包围CSM2和CSM1,以此类推。可以解决镜头平移、缩放甚至旋转带来的采样点在Shadowmap的Texel中发生的平移和缩放。缺点是包围盒不够紧凑。

右边为 Fit to CSM

细节

不能用于点光源,且通常只有主光源(如太阳)用 CSM

理论上可以为场景中的每一个方向光都分配一组CSM,但这意味着渲染开销会成倍增长。因为每一组CSM都需要每帧为每个级联渲染一次场景。考虑到性能预算,这种做法非常奢侈。对于次要的方向光(如补光),通常使用简单的单张阴影贴图甚至不开启实时阴影,以平衡画面效果和运行效率。只有在包含多个主光源的复杂场景中,才有可能需要为每个主光源配置CSM。

CSM Caching

在使用CSM时,我们常常会遇到 CSM开销较大的问题,比如现在使用四级CSM级联,就意味着在生成shaodwmap时,很多物体需要重复绘制四次。因此有的时候我们会对 CSM 进行一些优化。

一种方式是降低远处 CSM 的更新频率。比如在原神的PC版中,共有八级的CSM,前四级是每帧都更新的,后四级是逐帧依次更新的,这样相当于每帧需要更新五级的CSM。

另外一种方式是将 CSM 中算出的阴影动态缓存,对于静态物体的 shadowmap,是可以实现前后两帧之间的复用的。上一帧中静态物体的shadowmap,经过一些小小的处理,在当前帧仍然是可用的,对于一些没有覆盖的区域,可以动态来检测,重新绘制生成: CSM Caching

VSM (Virtual Shadow Maps)

这里的 V 即 Virtual,这个概念取自于Virtual Memory,类似的还有 VT (Virtual Texture)

Contact Shadow

在屏幕空间进行逐像素的 RayMaraching,来得到高质量的近距离阴影。因为RayMarching的开销较大,Contact Shadow RayMarching的距离一般都很短,大约在0.1m~0.5m左右。

SDF 阴影

SDF 可以很好的做软阴影。软阴影由来就是一个面光源有一部分被挡住了,那么用SDF就可以近似得到大概有多少范围被挡住的一个信息,虽然不准,但是比较符合人们的观察。

参考 https://zhuanlan.zhihu.com/p/398656596

参考

https://learnopengl-cn.github.io/05%20Advanced%20Lighting/03%20Shadows/01%20Shadow%20Mapping/#_2 https://learnopengl-cn.github.io/05%20Advanced%20Lighting/03%20Shadows/02%20Point%20Shadows/ https://learnopengl.com/Guest-Articles/2021/CSM https://zhuanlan.zhihu.com/p/53689987 https://www.zhihu.com/search?type=content&q=%E7%A8%B3%E5%AE%9ACSM https://learn.microsoft.com/en-us/windows/win32/dxtecharts/cascaded-shadow-maps https://zhuanlan.zhihu.com/p/104687855