需要预先学习:


1. 什么是屏幕空间

前向渲染与延迟渲染 的光照 Pass 展示了一种新视角:不再遍历场景,而是对屏幕上每个像素从纹理里取数据做计算。把这种思路推广——所有输入只有"当前视角下已渲染的屏幕图像(颜色、深度、法线)"、所有计算都发生在全屏 pass 上的技术,统称屏幕空间(screen space)技术。

它与 全局光照与局部光照 的关系:环境光遮蔽、间接光、反射这些 GI 效果需要整条光路的信息,实时算不动;屏幕空间技术退而求其次——屏幕上看得见的部分,就是我能拿来用的全部:

真实 GI:  光线在完整场景中反弹(包括屏幕外、被遮挡的部分)
屏幕空间: 光线只在一层"屏幕贴片"上反弹(屏幕拍到的 2.5D 深度场)

代价与收益同样明确:

优点 缺点
输入只是几张全屏纹理,复杂度与场景规模无关 屏幕外的信息不存在:反射/AO 在物体进入屏幕的边缘消失
天然适配全屏 pass / compute shader,实现独立于场景组织 被遮挡的几何不会被采样:被挡住的物体不产生 AO 或反射
与延迟渲染的 G-Buffer 天然契合 都是近似,物理正确性有限

常见的屏幕空间成员:SSAO(环境光遮蔽)、SSR(屏幕空间反射)、SSGI/SSDO(间接光)、DoF(景深)、Motion Blur(运动模糊)、甚至 Bloom(提取亮部再模糊)也在此列。这一篇讲透三个代表(SSAO、SSR)加两个后处理(DoF、Motion Blur),它们共享同一块地基。


2. 地基:从深度重建世界坐标

大多数屏幕空间算法的第一步,是回答"这个像素对应 3D 场景里的哪个点、表面朝向哪里"。深度缓冲(深度缓冲)存着每个像素的非线性深度(见 透视校正插值),由它可以重建(reconstruct)像素的世界坐标:

  1. 取像素的 NDC 坐标 (xndc,yndc,zndc)(x_{ndc}, y_{ndc}, z_{ndc})——xyxy 由像素坐标线性映射,zz 采样深度缓冲;
  2. 左乘逆视图投影矩阵 (PV)−1(PV)^{-1}(或分两步:先逆投影到视图空间、再逆视图变换),除以 ww;
  3. 得到该像素的世界坐标。
// 片元着色器:由屏幕像素 + 深度重建世界坐标
vec3 reconstructWorldPos(vec2 uv)
{
    float depth = texture(gDepth, uv).r;              // 非线性深度
    vec4 ndc = vec4(uv * 2.0 - 1.0, depth * 2.0 - 1.0, 1.0);   // [0,1] -> [-1,1]
    vec4 world = inverseViewProj * ndc;               // 逆变换
    return world.xyz / world.w;                       // 透视除法(见 [[透视校正插值]])
}

法线有两个来源:延迟管线直接从 G-Buffer 采样(见 前向渲染与延迟渲染);前向管线没有法线缓冲时,用深度缓冲的邻域差分估算屏幕空间法线——取右、上两个相邻像素的世界坐标差做叉积:

// 深度差分估算屏幕空间法线(无需 G-Buffer)
vec3 getNormalFromDepth(vec2 uv)
{
    vec3 p  = reconstructWorldPos(uv);
    vec3 px = reconstructWorldPos(uv + vec2(texelSize.x, 0.0));
    vec3 py = reconstructWorldPos(uv + vec2(0.0, texelSize.y));
    return normalize(cross(px - p, py - p));
}

注:深度差分法线是"深度场的法线",在物体边缘处混入背景深度,出现硬边——它描述的是"屏幕上这片深度坡面"而不是物体表面本身。G-Buffer 法线(渲染时写下的)更干净,这也是屏幕空间技术与延迟渲染亲和的又一个原因。


3. SSAO:屏幕空间环境光遮蔽

全局光照与局部光照 提过 AO(ambient occlusion,环境光遮蔽):缝隙、交角处的间接光被周围几何挡住,形成柔和的暗角。SSAO(Screen-Space Ambient Occlusion)在屏幕空间近似它。

3.1 原理

每个像素根据重建出的位置 PP 与法线 NN,在 NN 朝向的半球内撒若干采样点,检查每个采样点是否被几何挡住——判断方法是比较采样点的深度与深度缓冲中"该方向射线最先碰到的深度":

          采样点(沿法线半球随机分布)
            •    •
         • /‾‾‾‾‾‾\ •
         /  深度缓冲表面  \
────────●──────────────────●────────
        P (像素)          采样点的深度
        ↑ 若采样点比深度缓冲更"深"(更远),
          说明它藏在表面后面 —— 记一次遮挡

遮挡率 = 被挡采样点数 / 总采样数,再乘到环境光(或间接光)项上:

AO=1−被遮挡采样数总采样数AO = 1 - \frac{\text{被遮挡采样数}}{\text{总采样数}}

3.2 实现要点

采样核生成:在切线空间半球内随机取 64 个方向(半径随机),每个片元共用这一组(见 法线贴图与TBN矩阵 的 TBN 变换到世界)。

逐像素随机旋转:64 个采样点对全屏复用会在暗角处出现规则的条纹。给每个像素一个随机旋转角,把采样核整体转一个随机方向,条纹打散成高频噪声。

半径与范围检测:距离过远的"遮挡"不该计入(很远的墙不该遮挡脚边的缝隙),按采样点与 PP 的距离做范围衰减。

// 片元着色器:SSAO 核心(节选)
float ssao(vec2 uv)
{
    vec3 fragPos = reconstructWorldPos(uv);
    vec3 normal  = texture(gNormal, uv).rgb;

    float occlusion = 0.0;
    for (int i = 0; i < KERNEL_SIZE; ++i)
    {
        // 采样点:切线空间 -> 世界 -> 屏幕方向
        vec3 samplePos = fragPos + (TBN * kernel[i]) * radius;

        // 世界坐标 -> 屏幕空间,采样该处的深度缓冲
        vec4 offset = projection * view * vec4(samplePos, 1.0);
        offset.xyz /= offset.w;
        offset.xy   = offset.xy * 0.5 + 0.5;

        float sceneDepth = reconstructWorldPos(offset.xy).z;
        float sampleDepth = samplePos.z;

        // 采样点藏在表面后面 -> 遮挡;距离太远则不计
        if (sampleDepth > sceneDepth && sampleDepth - sceneDepth < range)
            occlusion += 1.0;
    }

    // 随机旋转已并入 kernel 的 TBN(noiseTexture 提供),此处取平均
    return 1.0 - occlusion / float(KERNEL_SIZE);
}

模糊 pass:随机旋转留下的噪声用一次小半径模糊(保边模糊,不糊掉物体边界)抹平,得到平滑的 AO 图。最后把 AO 乘进光照:ambient *= ao。

3.3 变体与工程现状

变体 改进点
HBAO 只沿水平集方向积分,考虑遮蔽者与被遮蔽者的角度,暗角更物理
GTAO HBAO 的各向异性修正版,接近离线渲染的 ground truth
Alchemy AO 早期工业方案(Crytek),SSAO 的直接前身
RTAO 硬件光追(RTX)下的真 AO,无屏幕空间缺陷,但依赖硬件光追单元(见 光线追踪)

注:SSAO 遮蔽的是间接光,只应乘在环境光 / 间接光项上——乘到直接光照上是最常见的实现错误(缝隙里太阳直射的地方也被压黑,物理上说不通)。


4. SSR:屏幕空间反射

镜面反射需要知道"反射方向上有什么"。PBR 的 IBL 用环境贴图回答,但环境贴图只有远景(skybox),反射不出近处的动态物体。SSR(Screen-Space Reflections)的答案:反射方向上的东西,在屏幕图像里找。

4.1 原理:光线步进

从像素位置 PP 沿反射方向 r=reflect(−v,n)\mathbf{r} = \text{reflect}(-\mathbf{v}, \mathbf{n}) 发射射线,在屏幕空间步进(march):每前进一步,把采样点投影回屏幕坐标,与深度缓冲比较——

  反射方向 r
     \
      \  步进点1   步进点2   步进点3 命中!
       \   •        •        •╱
        \  |屏幕投影 |        ╱
─────────●─────────────────●──────────  深度缓冲表面
         P                  命中处(反射源)

步进点比深度缓冲更远(藏在表面后):继续走;步进点穿过表面(比深度缓冲更近):命中,取命中处的屏幕颜色作为反射色。

// 片元着色器:SSR 光线步进(节选)
vec3 ssr(vec3 fragPos, vec3 normal, vec3 viewDir)
{
    vec3 origin = fragPos;
    vec3 dir = normalize(reflect(-viewDir, normal));   // 反射方向

    vec3 pos = origin + dir * stepSize;
    for (int i = 0; i < MAX_STEPS; ++i)
    {
        // 步进点投影到屏幕
        vec4 clip = projection * view * vec4(pos, 1.0);
        clip.xyz /= clip.w;
        vec2 uv = clip.xy * 0.5 + 0.5;
        if (uv.x < 0.0 || uv.x > 1.0 || uv.y < 0.0 || uv.y > 1.0)
            break;                                      // 出屏:反射失败

        float depth = reconstructWorldPos(uv).z;
        if (pos.z > depth + bias)                       // 穿过表面:命中
        {
            vec2 hitUV = uv;
            return texture(gColor, hitUV).rgb;          // 命中处的颜色即反射色
        }

        pos += dir * stepSize;
    }
    return fallbackColor;                               // 未命中:退回环境贴图
}

4.2 关键改进

  • 二分细化:固定步长要么漏掉小物体(步子太大跨过去)、要么浪费(步子太密)。先粗步进命中,再在区间内二分查找精确交点;
  • Hi-Z 加速:用 剔除总结 的 Hi-Z 深度金字塔按"最近表面的粗略距离"自适应步长,快而不漏;
  • 粗糙度衰减:粗糙表面的反射应是模糊的。按粗糙度在预滤波 mip 链(思路同 纹理 的 mipmap 与 IBL 的预滤波)上采样屏幕颜色,或按 TAAU 的思路做时间累积降噪;
  • 菲涅尔加权:反射强度乘上 PBR 的菲涅尔项——掠射角反射强、正视时弱。

4.3 固有缺陷

SSR 的三个硬伤都源于"屏幕只有一层":

缺陷 现象 缓解手段
屏幕外裁剪 反射源走出画面,反射内容突然消失/截断 边缘衰减遮罩 + 环境贴图兜底
遮挡错误 屏幕里看不见的物体(被挡住)不出现在反射里 无法在屏幕空间解决,靠光追(光线追踪)或探针(见 全局光照与局部光照)
前后混叠 命中检测只比一次深度,细长物体后的几何可能被误当反射源 Hi-Z 步进、命中厚度上限

注:现代管线(UE 的 Lumen 之前)普遍是 SSR + IBL 环境贴图 + 反射探针三者混合:屏幕内交给 SSR,屏幕外远处交给探针/IBL,按屏幕边缘距离淡入淡出。纯 SSR 只在 demo 里存在。


5. DoF:景深

真实镜头无法让所有距离同时清晰:焦点前后存在弥散圆(circle of confusion,CoC)——一个点光源在成像面上散开的圆斑,大小取决于它与焦平面的距离。DoF(Depth of Field,景深)后处理用 CoC 模拟镜头的虚化。

5.1 CoC 的计算

由薄透镜成像公式(不用记推导,记住结论即可),CoC 直径与"像素深度偏离焦平面的量"近似线性:

CoC=aperture⋅∣1zfocus−1zpixel∣⋅fprojCoC = \text{aperture} \cdot \left| \frac{1}{z_{focus}} - \frac{1}{z_{pixel}} \right| \cdot f_{proj}

  • zfocusz_{focus}:焦距(用户设定的清晰距离);
  • zpixelz_{pixel}:该像素的场景深度(深度缓冲重建);
  • aperture\text{aperture}:光圈,控制虚化强度。

离焦平面越远,CoC 越大,该像素应该越"散"。整个流程:

HDR 场景 ──> 计算 CoC 图 ──> 按 CoC 分离 near / far ──> 各自模糊 ──> 按 CoC 合成

5.2 实现要点

不能直接高斯模糊。Bloom 的高斯模糊是固定半径,而 DoF 每个像素的模糊半径不同(CoC 不同)——需要对每个像素按自身 CoC 采样邻居。直接逐像素变半径采样代价大,工业方案分两类:

  • 降采样 + 分层模糊:把画面按 CoC 档位分成几层(近、中、远),每层固定半径模糊(可复用 Bloom 的降采样金字塔),合成时按 CoC 插值取层;
  • 散景滤波(bokeh filter):把每个亮点直接画成一个六边形/圆形"光斑 sprite"(相机光圈叶片的形状),additive 累加,出来的虚化有真实镜头的"光斑感"。

半透明与 DoF 的顺序:半透明物体(半透明物体渲染)没有写深度,CoC 只由不透明表面决定,半透明物体跟随背景虚化——前景雾气跟着远处背景散焦,通常可接受,特写时会穿帮。

注意:DoF 作用于HDR 数据之后、色调映射之前(虚化是物理光学现象,色调映射 之后混合会破坏 HDR 线性关系),这与 Bloom 的位置要求一致。


6. Motion Blur:运动模糊

相机快门开启的时间内场景在动,感光面记录的是一段时间的积分——高速物体在照片上拖出方向性模糊。Motion Blur(运动模糊)用每像素的速度矢量(velocity vector)模拟这一积分。

6.1 速度缓冲

速度矢量回答"这个像素的画面内容在上一帧的哪里"——与 抗锯齿算法 TAA 的重投影是同一个东西:

velocity=uvcurrent−uvpreviousvelocity = uv_{current} - uv_{previous}

  • 静态场景:由当前帧与上一帧的视图投影矩阵计算——顶点着色器里把位置分别乘两个 VP,投影到 UV 相减(相机不动时速度为零);
  • 动态物体:还需乘上一帧的模型矩阵(骨骼动画则要上一帧的骨骼),只靠相机矩阵算不出物体的自身运动。
// 顶点着色器:输出速度矢量
vec4 currPos = currViewProj * model * vec4(aPos, 1.0);
vec4 prevPos = prevViewProj * prevModel * vec4(aPos, 1.0);

currPos.xyz /= currPos.w;
prevPos.xyz /= prevPos.w;

Velocity = (currPos.xy - prevPos.xy) * 0.5;   // NDC 差的一半,即 UV 位移

6.2 tile-max 两级模糊

逐像素沿速度方向采样是最直接的实现(每个像素按自己的 velocity 采样 N 次),但快速运动时相邻像素速度差异大,直接采样会闪烁。工业做法(Call of Duty 系提出的经典流程)分三级:

  1. tile max:把速度缓冲缩小(如 1/16 分辨率),每格取长度最大的速度——保证一格内按最快的运动模糊,避免快速物体"糊不过去";
  2. neighbor max:再对 tile max 结果取 3×3 邻域最大,得到保守的速度场;
  3. 方向模糊:全屏 pass 里每个像素按 neighbor max 速度沿方向采样 NN 次取平均。
速度缓冲 ──tile max──> neighbor max ──┐
                                      ├──> 沿速度方向多次采样平均
HDR 场景 ─────────────────────────────┘

注:速度缓冲是现代管线的"公共设施":TAA/TAAU 的重投影(抗锯齿算法)、Motion Blur、自动曝光的时域平滑都要用它——在顶点着色器多输出一对 UV,多个后处理受益,这也是引擎把它做成独立 pass 的原因。


7. 总结

技术 输入 核心操作 主要缺陷
深度重建 深度缓冲 + 逆 VP 矩阵 NDC 逆变换回世界空间 深度差分法线有硬边
SSAO 深度(法线) 半球采样比较深度 屏幕外遮挡缺失、噪声
SSR 颜色 + 深度 沿反射方向屏幕空间步进 屏幕外反射缺失、遮挡错误
DoF 颜色 + 深度 按 CoC 变半径模糊 / 光斑 半透明无深度、与 TAA 交互复杂
Motion Blur 颜色 + 速度缓冲 沿速度方向多次采样平均 相机快切时全屏拖影(常直接关闭)

它们的共同底色是全局光照与局部光照里"近似工具箱"的思路:算不起完整的全局信息,就把屏幕上已有的信息用到极致。共同的局限也一致——屏幕只是一扇窗,窗外的世界这些技术一无所知;突破这层局限的出路是体素化、探针(见 全局光照与局部光照 的实时 GI 表)与硬件光追(光线追踪)。