深入解析屏幕空间环境光遮蔽(SSAO):数学半球积分、采样核优化、深度不连续瑕疵与双边滤波工程实现

深入解析屏幕空间环境光遮蔽(Screen-Space Ambient Occlusion, SSAO)

在实时 3D 图形渲染管线中,间接光照(Indirect Illumination)的计算是决定场景空间感与真实感的核心关键。然而,完整求解全局光照(Global Illumination, GI)的渲染方程开销极大。环境光遮蔽(Ambient Occlusion, AO) 作为一个高效的经验/物理近似模型,通过计算贴近表面片元被周围几何体遮挡的程度,为漫反射环境光提供了至关重要的暗化因子。

由 Crytek 在 2007 年提出的 屏幕空间环境光遮蔽(SSAO),将三维几何空间的遮挡判定降维至屏幕空间(Screen-Space Depth Buffer),以极高的计算效率实现了动态场景的实时阴影重建。

本文将深入 SSAO 的底层数学推导、采样核分布演进、自遮挡与黑晕瑕疵修正、深度感知双边滤波降噪,并提供标准的 Vulkan/GLSL 工程级实现。


一、 SSAO 核心原理与数学建模

1. 渲染方程与环境光遮蔽项解耦

在 PBR 渲染管线中,忽略自发光项后,表面点 \(\mathbf{p}\) 在观察方向 \(\mathbf{v}\) 的反射辐射度 \(L_o(\mathbf{p}, \mathbf{v})\) 表达为:

\[ L_o(\mathbf{p}, \mathbf{v}) = \int_{\Omega} f_r(\mathbf{p}, \mathbf{\omega}_i, \mathbf{v}) L_i(\mathbf{p}, \mathbf{\omega}_i) (\mathbf{n} \cdot \mathbf{\omega}_i) d\mathbf{\omega}_i \]

在计算漫反射环境间接光(Diffuse Indirect Light)时,假设入射环境光强度 \(L_{in}\) 在半球方向 \(\Omega\) 上各向同性(Uniform Distant Light),则可以将环境光的可见性函数 \(V(\mathbf{p}, \mathbf{\omega}_i) \in [0, 1]\) 从积分中解耦,定义为环境光遮蔽因子 \(A(\mathbf{p})\)

\[ A(\mathbf{p}) = \frac{1}{\pi} \int_{\Omega} V(\mathbf{p}, \mathbf{\omega}_i) (\mathbf{n} \cdot \mathbf{\omega}_i) d\mathbf{\omega}_i \]

其中: * \(\mathbf{n}\) 为表面点 \(\mathbf{p}\) 处的单位法线向量。 * \(\mathbf{\omega}_i\) 为半球方向上的入射光线单位向量。 * \(V(\mathbf{p}, \mathbf{\omega}_i)\) 为遮挡函数:若沿着 \(\mathbf{\omega}_i\) 方向在半径 \(R\) 内击中几何体则 \(V=0\),否则 \(V=1\)。 * Factor \(\frac{1}{\pi}\) 用于将半球余弦积分归一化至 \([0, 1]\) 范围。


2. 屏幕空间离散化与 Monte Carlo 采样

在实时渲染中,SSAO 放弃了复杂的 3D 场景几何求交,转而利用深度图(Depth Buffer)重建视空间 3D 坐标。通过 Monte Carlo 随机采样法,将连续的半球积分离散化为 \(N\) 个离散采样点的遮挡校验:

\[ A(\mathbf{p}) \approx 1.0 - \frac{1}{N} \sum_{i=1}^{N} \chi\left( z_{\mathrm{sample}_i} - z_{\mathrm{buffer}_i} \right) \cdot \max(0.0, \mathbf{n} \cdot \hat{\mathbf{s}}_i) \]

其中: * \(\mathbf{s}_i\) 为在视空间中以 \(\mathbf{p}\) 为原点生成的采样点偏移向量,采样点位置为 \(\mathbf{p}_{\mathrm{sample}_i} = \mathbf{p} + \mathbf{s}_i\)。 * \(z_{\mathrm{sample}_i}\) 为采样点 \(\mathbf{p}_{\mathrm{sample}_i}\) 的视空间深度值。 * \(z_{\mathrm{buffer}_i}\) 为将 \(\mathbf{p}_{\mathrm{sample}_i}\) 投影回屏幕坐标 \((u_i, v_i)\) 后,从 Depth Buffer 读取并重建的实际几何体视空间深度。 * \(\chi(\Delta z)\) 为阶跃遮挡判定函数:当 \(z_{\mathrm{sample}_i} \ge z_{\mathrm{buffer}_i} + \text{Bias}\) 时返回 \(1.0\)(表示被遮挡),否则返回 \(0.0\)


二、 采样核(Sampling Kernel)的设计与演进

采样核的几何分布和随机化策略直接决定了 SSAO 的画面质量与性能开销。

1
2
3
4
5
6
7
8
球体采样核 (Sphere Kernel)             法线定向半球采样核 (Oriented Hemisphere)
+---+---+ +---+---+
/ | | \ / | | \
| * | * | * | | * | * | * |
+----+---+----+ +----+---+----+
\ * | p | * / \ | p | /
+---+---+ +-----+-----+
(表面下方采样导致自遮挡) (沿法线 n 定向,消除自遮挡)

1. 从球体采样(Sphere Kernel)到法线定向半球采样(Oriented Hemisphere)

  • 球体采样核(Sphere Kernel):早期 SSAO 在以 \(\mathbf{p}\) 为中心的 3D 球体空间内随机均匀分布采样点。这种方式的严重缺陷在于,平整表面下方近乎半数的采样点会天然落入几何体内部,导致平面被误判为被遮挡(自遮挡,Self-Occlusion),产生不真实的灰色暗斑。
  • 法线定向半球采样核(Oriented Hemisphere Kernel):现代 SSAO 获取 G-Buffer 中的像素法线 \(\mathbf{n}\),在以法线为中心轴的半球空间(Oriented Hemisphere)内分布采样点,从根本上消除了平坦表面的自遮挡现象。

2. 采样点分布曲线(Scale Power Distribution)

如果采样点在半球内完全均匀分布,会导致靠近着色点 \(\mathbf{p}\) 处的采样密度不足,丢弃高频微小折痕的遮挡细节;同时,较远的采样点容易产生带状带状噪声(Banding Artifacts)。

工程上通常使用非线性指数衰减曲线重新分配采样点的距离模长,使采样点集中密布在原点附近:

\[ \mathbf{s}_i = \mathbf{v}_i \cdot \mathrm{scale}_i \]

\[ \mathrm{scale}_i = \mathrm{lerp}\left(0.1, 1.0, \left(\frac{i}{N}\right)^2\right) \]

式中 \(i \in [0, N-1]\),二次方曲线 \((\frac{i}{N})^2\) 使得大量采样点贴近原点,极大地增强了近距离微遮蔽的细节感知。


3. 随机旋转向量与 4x4 噪声纹理(Random Rotation & Jittering)

为了在极低的采样数(如 \(N = 16 \sim 32\))下消除明显的采样带状图样(Banding),必须引入抖动(Jittering): 1. 创建一张尺寸仅为 \(4 \times 4\) 的随机旋转向量纹理(Random Noise Texture),纹理像素存放随机的切向向量 \(\mathbf{r} \in [-1, 1]^3\)。 2. 在片段着色器中,按屏幕像素坐标平铺采样(Tile Sampling)该噪声图。 3. 利用采样得到的随机向量与像素法线 \(\mathbf{n}\) 构建施密特正交化切线空间矩阵(TBN Base),将固定半球采样核旋转至随机切向空间。

这样可以将原本规则的低采样图样转化为高频噪点(High-Frequency Noise),后续只需施加一次轻量级的图像空间双边滤波(Bilateral Blur)即可获得平滑无噪的阴影。


三、 关键瑕疵(Artifacts)与工程修正

1. 深度不连续与黑晕现象(Shadow Haloing & Bleeding)

  • 成因:当背景物体(如远处的墙面)的采样点投影回屏幕后,其深度值与前景物体(如近处的角色手肘)在屏幕上重叠,但两者在视空间实际距离极远。算法会将远景采样点误判为被前景物体遮挡,产生向外扩散的黑色晕影(Shadow Haloing)。
1
2
3
4
5
6
相机 Ray ──>  前景物体 (Depth z_fg)
|
v [巨大的 Depth 裂缝]
....................
|
v 远处墙面 (Depth z_bg) ──> 采样点落入远处,误判为被前景遮挡!
  • 解决方案:范围衰减检查(Range Check / Distance Attenuation) 引入基于深度差的平滑衰减权重 \(W_{\mathrm{range}}\)。当采样点深度与当前片元深度的绝对差值超过预设半径 \(R\) 时,快速将该采样点的遮挡贡献衰减至 0:

\[ W_{\mathrm{range}} = \mathrm{smoothstep}\left(1.0, 0.0, \frac{|z_{\mathbf{p}} - z_{\mathrm{buffer}_i}|}{R}\right) \]

修正后的离散遮挡累加项为:

\[ \text{Occlusion}_i = \chi\left(z_{\mathrm{sample}_i} - (z_{\mathrm{buffer}_i} + \text{Bias})\right) \cdot W_{\mathrm{range}} \]


2. 深度偏置(Depth Bias / Self-Shadowing Acne)

受限于 Depth Buffer 的浮点数精度以及视空间坐标重建误差,靠近切线的采样点可能微小穿透深度表面,造成高频黑点(Acne)。引入一个极小的常数偏置 \(\mathrm{Bias} \approx 0.025\) 即可完美解决。


四、 图像空间降噪:深度/法线感知双边滤波 (Cross-Bilateral Filter)

由于采样阶段使用了 \(4 \times 4\) 噪声图进行随机旋转,SSAO 原始输出带有明显的高频颗粒噪声。直接使用标准高斯模糊(Gaussian Blur)抹平噪点会导致物体边缘(如墙角、物体轮廓)模糊消隐,破坏空间几何边界。

必须采用交叉双边滤波(Cross-Bilateral Filter),在模糊的同时根据深度梯度与法线相似度保护几何边缘。

1
2
3
4
5
6
7
8
+-----------------------------------------------------------------------+
| 深度与法线感知双边滤波 (Bilateral Filter) |
| |
| [高斯空间权重 w_s] x [深度相似度权重 w_z] x [法线相似度权重 w_n] |
| | |
| v |
| 最终加权核 (边缘处 w_z/w_n 突变降为 0) |
+-----------------------------------------------------------------------+

双边滤波加权公式

对于中心像素 \(p\),在其邻域 \(q \in \Omega\) 内累加采样,加权系数 \(w(p, q)\) 联合定义为:

\[ w(p, q) = G_{\sigma_s}(\|p - q\|) \cdot \exp\left(-\frac{|z_p - z_q|}{\sigma_z}\right) \cdot \max(0.0, \mathbf{n}_p \cdot \mathbf{n}_q)^m \]

其中: * \(G_{\sigma_s}(\|p - q\|)\) 为高斯空间距离权重(Spatial Weight)。 * \(\exp\left(-\frac{|z_p - z_q|}{\sigma_z}\right)\)深度相似度权重:若邻域像素 \(q\) 与中心像素 \(p\) 的深度差过大(跨越几何边缘),权重剧降为 0。 * \(\max(0.0, \mathbf{n}_p \cdot \mathbf{n}_q)^m\)法线相似度权重:若两像素法线夹角过大(折角处),权重降为 0。


五、 GLSL 工程完整源码实现

1. SSAO 核心片段着色器 (SSAO Pass)

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
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
#version 450

layout (location = 0) out float FragOcclusion;

layout (location = 0) in vec2 inUV;

layout (binding = 0) uniform sampler2D gDepth;
layout (binding = 1) uniform sampler2D gNormal;
layout (binding = 2) uniform sampler2D texNoise; // 4x4 RGB16F 随机向量图

const int KERNEL_SIZE = 32;

layout (std140, binding = 3) uniform SSAOUniforms {
mat4 projection;
mat4 invProjection;
vec4 samples[KERNEL_SIZE]; // CPU 端预生成的法线半球采样点
vec2 noiseScale; // screenWidth/4.0, screenHeight/4.0
float radius; // 采样半径 R (如 0.5m)
float bias; // 深度偏置 (如 0.025)
float power; // AO 强度指数曲线
};

// 重建视空间 3D 坐标
vec3 ReconstructionViewPos(vec2 uv) {
float depth = texture(gDepth, uv).r;
vec4 clipPos = vec4(uv * 2.0 - 1.0, depth * 2.0 - 1.0, 1.0);
vec4 viewPos = invProjection * clipPos;
return viewPos.xyz / viewPos.w;
}

void main() {
vec3 fragPos = ReconstructionViewPos(inUV);
vec3 normal = normalize(texture(gNormal, inUV).rgb); // 视空间法线
vec3 randomVec = normalize(texture(texNoise, inUV * noiseScale).xyz);

// 1. 构建切线空间正交基 (Gram-Schmidt 过程)
vec3 tangent = normalize(randomVec - normal * dot(randomVec, normal));
vec3 bitangent = cross(normal, tangent);
mat3 TBN = mat3(tangent, bitangent, normal);

float occlusion = 0.0;
for (int i = 0; i < KERNEL_SIZE; ++i) {
// 2. 获取切线空间采样点并变换至视空间
vec3 samplePos = TBN * samples[i].xyz;
samplePos = fragPos + samplePos * radius;

// 3. 将视空间采样点投影至屏幕 NDC 坐标,获取采样 UV
vec4 offset = vec4(samplePos, 1.0);
offset = projection * offset;
offset.xyz /= offset.w;
offset.xyz = offset.xyz * 0.5 + 0.5;

// 4. 读取该 UV 下实际几何体的视空间深度
float sampleDepth = ReconstructionViewPos(offset.xy).z;

// 5. 范围衰减检查 (Range Check)
float rangeCheck = smoothstep(0.0, 1.0, radius / abs(fragPos.z - sampleDepth));

// 6. 深度遮挡判定累加
occlusion += (sampleDepth >= samplePos.z + bias ? 1.0 : 0.0) * rangeCheck;
}

// 归一化并取反:1.0 表示无遮挡,0.0 表示完全遮挡
float ao = 1.0 - (occlusion / float(KERNEL_SIZE));
FragColor = pow(ao, power); // 调整对比度输出
}

2. 深度/法线感知双边滤波着色器 (Blur Pass)

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
#version 450

layout (location = 0) out float FragColorBlur;

layout (location = 0) in vec2 inUV;

layout (binding = 0) uniform sampler2D ssaoInput;
layout (binding = 1) uniform sampler2D gDepth;
layout (binding = 2) uniform sampler2D gNormal;

const int BLUR_SIZE = 2; // 5x5 核心 (-2 to 2)

void main() {
vec2 texelSize = 1.0 / textureSize(ssaoInput, 0);
float result = 0.0;
float totalWeight = 0.0;

float centerDepth = texture(gDepth, inUV).r;
vec3 centerNormal = texture(gNormal, inUV).rgb;
float centerAO = texture(ssaoInput, inUV).r;

for (int x = -BLUR_SIZE; x <= BLUR_SIZE; ++x) {
for (int y = -BLUR_SIZE; y <= BLUR_SIZE; ++y) {
vec2 offset = vec2(float(x), float(y)) * texelSize;
vec2 sampleUV = inUV + offset;

float sampleAO = texture(ssaoInput, sampleUV).r;
float sampleDepth = texture(gDepth, sampleUV).r;
vec3 sampleNormal = texture(gNormal, sampleUV).rgb;

// 计算深度差权重与法线相似度权重
float depthWeight = exp(-abs(centerDepth - sampleDepth) * 100.0);
float normalWeight = max(0.0, dot(centerNormal, sampleNormal));
normalWeight = pow(normalWeight, 16.0); // 增强法线敏感度

float weight = depthWeight * normalWeight;
result += sampleAO * weight;
totalWeight += weight;
}
}

FragColorBlur = result / max(totalWeight, 0.0001);
}

六、 现代屏幕空间 AO 技术演进选型矩阵

评估维度 SSAO ( Crytek 2007) HBAO (NVIDIA 2008) GTAO (Activision 2016) RTAO (硬件光追)
几何采样空间 屏幕空间半球点采样 屏幕空间地平线仰角步进 半球切面扇区闭式积分 3D BVH 几何求交
物理正确性 较低(经验性近似) 中等(考虑地平线遮蔽) 极高(接近光追真值) 物理真实
边缘黑晕瑕疵 明显(依赖 Range Check) 较少 极少
与 PBR 积分匹配 无法直接匹配 部分匹配 完美的余弦加权积分 完美匹配
GPU 计算开销 极低 (~0.5ms) 中等 (~1.2ms) 中高 (~1.8ms) 高 (~3.5ms+)
硬件要求 任意移动/PC GPU 标准 PC / 高端移动 GPU 工业级现代 GPU 支持 DXR / Hardware RT GPU

七、 总结

屏幕空间环境光遮蔽(SSAO)凭借其“解耦几何复杂度”“纯后处理计算”的思想,奠定了现代实时图形管线中环境光遮蔽的基础: * 数学核心在于将半球环境光积分离散化为视空间 Depth Buffer 的深度校验; * 工程成败在于利用 4x4 旋转噪声降低采样数,配合 Range Check 消除黑晕,并施加 Cross-Bilateral Filter 保护几何边缘; * 尽管现代 AAA 游戏正全面向 GTAO 和 RTAO 演进,SSAO 凭借其极低的算力开销,依然是中低端设备与移动端游戏提升画面空间感的不二之选。

本文作者:Berg Zha

本文链接:http://junglemanpro.com/2026/08/06/SSSAO/

版权声明:本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明出处!

ESC 关闭 | 导航 | Enter 打开
输入关键词开始搜索