在实时三维渲染管线中,直接光照(如阳光与聚光灯)通常只能塑造出高光与清晰的阴影轮廓。然而在真实的物理世界中,由于来自天空球与周围环境的二次漫反射天光极其弥散,任何物体之间相互接触的微小夹角、缝隙、凹槽以及墙脚处,天光都会受到几何包围而难以充分透入,形成自然渐变的柔和暗晕——这便是环境光遮蔽(Ambient Occlusion, AO)。若缺乏 AO 的修饰,三维物体之间就会显得相互孤立、漂浮,画面呈现出塑料般的平淡质感。
屏幕空间环境光遮蔽(Screen Space Ambient Occlusion, SSAO)是工业界性价比最高的动态环境遮蔽解决方案。它无需构建昂贵的光线追踪 BVH 树或体素结构,仅依赖几何阶段已生成的屏幕深度缓冲与法线缓冲,在片元着色器中以极低成本实时解算局部微观几何遮挡。
本文将深入拆解从法向切线半球采样核(Hemisphere Kernel)的数学推导、到**跨距边缘感知双边滤波降噪(Edge-Preserving Bilateral Blur)**的完整生产级落地流水线。
核心算法拓扑与半球积分几何
SSAO 的本质是在屏幕空间对当前像素微观表面的可及立体角(Solid Angle)进行蒙特卡洛数值估算:
- 切线空间基底构建(TBN Matrix):从 G-Buffer 读取当前像素的世界/视空间法线 $\mathbf{N}$。由于视空间中仅有单一法线,我们利用一张平铺的 $4 \times 4$ 随机旋转噪声纹理获取随机切线向量,通过施密特正交化(Gram-Schmidt Process)构建出贴合局部的切线空间正交基底。
- 正向半球采样核(Hemisphere Samples):在法线指向的朝外半球内部,预先生成 16 到 32 个三维偏移采样向量。向量的长度采用二次方加权分布,使更多采样点密集聚拢在当前表面极近处,强化微观接触遮挡的锐利度。
- 屏幕重投影与深度对比(Depth Comparison):将半球采样点从视空间投影至屏幕 NDC 坐标,获取采样点对应的实际几何体深度 $Z_{\text{scene}}$。若当前测试点位于实际表面之后($Z_{\text{sample}} > Z_{\text{scene}}$),则判定该方向遭受遮挡。
- 平滑距离衰减(Range Check):引入光滑步长衰减函数,剔除远景物体(如百米开外的树木)因透视重叠而引发的虚假背景遮挡。
法线 N ▲ * │ * <─── 视空间正向半球采样点 (16~32 点) * │ * * │ * ───────────P─────────── (当前像素表面) (将各采样点投影回屏幕采样深度缓冲,累加遮挡比率)SSAO 核心计算着色器实现
以下是在 Vulkan 延迟管线下运行的 SSAO 片元着色器核心逻辑:
// SSAO.frag - 屏幕空间环境光遮蔽生成 #version 450 layout(location = 0) in vec2 inUV; layout(location = 0) out float outAO; layout(binding = 0) uniform sampler2D uDepthTexture; layout(binding = 1) uniform sampler2D uNormalTexture; layout(binding = 2) uniform sampler2D uNoiseTexture; // 4x4 蓝噪声旋转贴图 layout(push_constant) uniform SSAOParams { mat4 projMatrix; mat4 invProjMatrix; vec4 samples[32]; // 预计算的半球偏移向量 vec2 noiseScale; // 屏幕分辨率 / 4.0 float radius; // 采样半球物理世界半径 (如 0.5米) float bias; // 避免自遮挡的深度偏置 (如 0.025) } ubo; // 从屏幕 UV 与深度还原视空间坐标 vec3 GetViewPos(vec2 uv) { float depth = texture(uDepthTexture, uv).r; vec4 clipPos = vec4(uv * 2.0 - 1.0, depth, 1.0); vec4 viewPos = ubo.invProjMatrix * clipPos; return viewPos.xyz / viewPos.w; } void main() { vec3 fragPos = GetViewPos(inUV); vec3 normal = normalize(texture(uNormalTexture, inUV).xyz); // 采样旋转噪声向量 (映射到 [-1, 1]) vec3 randomVec = normalize(texture(uNoiseTexture, inUV * ubo.noiseScale).xyz * 2.0 - 1.0); // 施密特正交化构建局部 TBN 基底 vec3 tangent = normalize(randomVec - normal * dot(randomVec, normal)); vec3 bitangent = cross(normal, tangent); mat3 TBN = mat3(tangent, bitangent, normal); float occlusion = 0.0; const int KERNEL_SIZE = 32; for (int i = 0; i < KERNEL_SIZE; ++i) { // 1. 将采样点从切线空间旋转至视空间 vec3 samplePos = fragPos + (TBN * ubo.samples[i].xyz) * ubo.radius; // 2. 投影至屏幕裁剪空间 vec4 offset = vec4(samplePos, 1.0); offset = ubo.projMatrix * offset; offset.xyz /= offset.w; vec2 sampleUV = offset.xy * 0.5 + 0.5; // 3. 读取该屏幕坐标处的真实表面深度 vec3 occluderPos = GetViewPos(sampleUV); // 4. 深度比较与平滑距离衰减 (Range Check) float rangeCheck = smoothstep(0.0, 1.0, ubo.radius / abs(fragPos.z - occluderPos.z)); // 当测试点深于遮挡物且超过偏置时,记为被遮挡 occlusion += (occluderPos.z >= samplePos.z + ubo.bias ? 1.0 : 0.0) * rangeCheck; } // 归一化并反转为可用环境光比率 (1.0 代表无遮挡,0.0 代表完全遮黑) outAO = 1.0 - (occlusion / float(KERNEL_SIZE)); }半分辨率执行与跨距双边降噪滤波(Bilateral Blur)
由于我们引入了 $4 \times 4$ 随机旋转噪声来打碎采样带,原始输出的 SSAO 纹理会布满强烈的噪点颗粒感。必须对其施加低开销的降噪模糊:
半分辨率降采计算(Half-Res Execution):为了节约 GPU 片元算力,SSAO Pass 通常在原分辨率的 $50% \times 50%$(即 1/4 像素量)视口中派发计算,仅此一步便将开销压减 70%。
法线与深度感知的双边跨距滤波(Edge-Preserving Cross Bilateral Filter):
普通的盒式模糊或高斯模糊会将墙壁与前景角色之间的边缘彻底糊化,导致出现刺眼的暗晕光芒(Halo Artifacts)。必须在滤波时比对相邻像素的深度与法线夹角:$$W(p, q) = G_{\text{spatial}}(|p - q|) \cdot \exp\left(-\frac{|\Delta z|^2}{2\sigma_z^2}\right) \cdot \max(0.0, \mathbf{N}_p \cdot \mathbf{N}_q)^{16}$$
// SSAOBlur.frag - 边缘保持双边滤波核心片段 float BilateralFilter(vec2 uv, vec2 texelSize) { float centerAO = texture(uSSAOInput, uv).r; float centerDepth = texture(uDepth, uv).r; vec3 centerNormal = texture(uNormal, uv).xyz; float totalWeight = 1.0; float totalAO = centerAO; for (int x = -2; x <= 2; ++x) { for (int y = -2; y <= 2; ++y) { if (x == 0 && y == 0) continue; vec2 sampleUV = uv + vec2(x, y) * texelSize; float sampleDepth = texture(uDepth, sampleUV).r; vec3 sampleNormal = texture(uNormal, sampleUV).xyz; float sampleAO = texture(uSSAOInput, sampleUV).r; // 几何边缘保护:若法线夹角过大或深度阶跃过大,权重归零 float depthDiff = abs(centerDepth - sampleDepth); float normalDot = max(0.0, dot(centerNormal, sampleNormal)); if (depthDiff < 0.005 && normalDot > 0.85) { float w = 1.0; // 空间加权 totalAO += sampleAO * w; totalWeight += w; } } } return totalAO / totalWeight; }时间性超采样(Temporal Accumulation)与演进至 GTAO
在高端主机与桌面平台,现代管线进一步将 SSAO 与时间性抗锯齿(TAA)管线紧密结合:
- 时序抖动复用(Temporal Dithering):在每一帧轻微旋转蓝噪声纹理的采样相位,单帧仅需采集 4 到 8 个半球样本。利用上一帧的历史缓存与运动矢量(Motion Vectors)执行带有颜色包围盒裁剪(Color Neighborhood Clamping)的指数加权移动平均,能够以极低开销达成等效 64 点以上的光滑环境遮蔽。
- 地平线真实遮蔽(Ground Truth Ambient Occlusion, GTAO)演进:通过将半球离散点采样演进为沿屏幕空间切线方向搜索最大地平线仰角(Horizon Angles),并根据余弦辐射定律求闭式解,能够彻底消除传统 SSAO 缺乏方向性遮蔽(Directional Occlusion)的理论缺陷,为微观缝隙赋予更加精准立体的物理阴影。
通过半分辨率半球采样配合严格保边缘的双边滤波降噪,SSAO 能够以低于 0.6ms 的极致开销,为三维世界瞬间注入扎实饱满的物理接触感与深度空间层次。