SSAO的核心是估算遮蔽因子而非计算光照;它基于G-buffer的深度和法线纹理,在视锥空间邻域采样深度,统计比当前像素更近的样本数以判断遮蔽程度,需用随机旋转半球采样核、动态缩放采样半径并剔除背向法线的样本。

SSAO 的核心思路不是“计算光照”,而是“估算遮蔽因子”
屏幕空间环境光遮蔽(SSAO)本质是在已有 G-buffer(特别是深度和法线纹理)基础上,对每个像素在**其视锥空间邻域内采样深度**,统计有多少样本点比当前像素更“靠近摄像机”——越多,说明该点越容易被周围几何体遮挡,环境光反射越弱。它不依赖场景拓扑,纯屏幕空间,因此快但有噪声和假阴影。关键前提是:你必须已渲染出 depth 和 normal 纹理(通常为 GL_R16F 或 GL_RGBA16F 格式),且它们的坐标系一致(推荐使用 view-space)。
用随机旋转的半球采样核避免带状伪影
直接在固定方向上采样会生成明显条纹或重复图案。标准做法是预生成一个 64 维的 vec3 噪声向量数组(如 Hammersley 序列),绑定为 uniform buffer 或 texture,在 fragment shader 中用屏幕坐标 gl_FragCoord.xy 哈希出一个旋转矩阵,将采样方向转到以当前像素法线为 z 轴的局部半球内。注意:采样半径必须随 depth 变化缩放(例如 radius * 0.2 / (z + 0.1)),否则远处物体会过度模糊;同时要剔除与法线夹角 > 90° 的采样(dot(sampleDir, normal) ),否则会漏光。
常见错误:
- 直接用 screen-space 偏移做采样(如 texelFetch(depthTex, ivec2(uv)+offset)),没转到 view-space,导致遮蔽方向错乱
- 半球采样未归一化或未投影到法线切空间,结果呈现平面化晕染
- 忘记对采样深度做 perspective-correct 插值,depth 值失真
降噪必须分两步:模糊 + 时间累积(TAA)
原始 SSAO 输出极其嘈杂,仅靠高斯模糊(5×5)只能压平高频噪声,但会糊掉边缘细节。真正实用的管线是:
- 第一步:用法线+深度引导的双边模糊(bilateral blur),权重 = exp(-length(delta_uv) * 2.0) * smoothstep(0.0, 0.1, 1.0 - abs(dot(n1, n2)))
- 第二步:在 TAA 框架下累积多帧 SSAO(加权平均),并用 motion vector 纠正像素历史位置。若无 TAA,至少要用 temporal reprojection + 速度缓冲做简单帧间混合(ssao = mix(ssao_prev_reprojected, ssao_current, 0.1))
性能影响显著:全屏 1024×768 下,64 样本 SSAO + 双边模糊约占用 GPU 片段着色器 3–5ms(GTX 1060)。若目标平台是移动端,建议砍到 16 样本 + 3×3 模糊,并禁用随机旋转(改用静态 kernel)。
OpenGL 实现时最容易崩的是深度纹理精度与坐标系错位
最常触发黑块/全白/闪烁的根源不是 shader 写错,而是:
- depth 纹理用了 GL_DEPTH_COMPONENT24,但采样时当成 GL_R32F 读取(应统一用 glTexImage2D(GL_TEXTURE_2D, 0, GL_R32F, ...) 并手动写入 gl_FragDepth)
- G-buffer 法线存的是 world-space,而 SSAO shader 里却用 view-space 的采样偏移(需统一转到 view-space,或全程用 ndc-space 计算)
- 没调用 glEnable(GL_DEPTH_TEST) 导致 G-buffer 深度写入失败,SSAO 输入 depth 全为 1.0
- 使用 GL_NEAREST 采样 depth/normal 纹理,造成块状遮蔽(必须设为 GL_LINEAR)
调试技巧:单独输出 depth 纹理到全屏 quad,确认是否为渐变灰度(近白远黑);再输出 abs(normal.z),检查是否中心亮四周暗(说明法线朝向正常)。
立即学习“C++免费学习笔记(深入)”;


















