反射路径

屏幕空间反射 Screen-Space Reflection,SSR 使用当前相机的深度、法线和照明图,查询反射方向上遇到的表面,再从该表面取得光照。其核心更接近屏幕空间射线追踪:先求交,后着色;“反射”是它常见的用途,并不限制其只能处理理想镜面。

开启与关闭SSR时地面反射的差别

左图地面能看到机械臂和周围结构的反射,右图只剩原来的地面着色。机械臂先被光源照亮,再向地面反射光,最后地面将其反射到相机,这是一条间接光照路径。

SSDO 在局部范围内粗略收集方向贡献,SSR 则沿选定方向在深度壳上搜索交点。两者都复用相机信息,但查询路径和质量目标不同;不能把 SSAO 的球内深度失败率直接当成 SSR 的反射颜色。

镜面查询

给定表面位置 pp、单位法线 n\mathbf n 和从表面指向相机的单位方向 v\mathbf v,镜面反射方向为

r=2(n⋅v)n−v.\mathbf r=2(\mathbf n\cdot\mathbf v)\mathbf n-\mathbf v.

从 pp 沿 r\mathbf r 找到交点 qq,取得 qq 向 pp 的出射辐亮度,再按接收材质的反射比例合成。GLSL 中反射函数的输入是传播到表面的方向,所以使用 reflect(-v, n)。

旗帜经地面反射形成的屏幕空间结果

地面每个片元的射线命中不同旗帜位置,便读取相应颜色,形成倒置的空间结构。深度图只提供相机最近可见层,尚不能保证这些交点与完整场景的真实反射交点相同。

理想镜面方向集中,一次查询就能得到主要路径。粗糙材质的反射瓣有宽度,需要在其范围内采多个方向;越接近漫反射,方向范围越大,低样本的噪声与漏检也越明显。因此 SSR 通常更适合镜面或较集中的 Glossy 反射。

射线投影

在一致的视图空间中写射线

x(t)=p+tr,t>0.\mathbf x(t)=\mathbf p+t\mathbf r, \qquad t>0.

位置、法线和观察向量不能混用世界与视图空间。表面起点需适当偏移或忽略起点附近区间,避免马上命中自身。

用当前投影矩阵 PP 得到裁剪坐标 c=P(x,1)T\mathbf c=P(\mathbf x,1)^{\mathsf T},再做齐次除法 xndc=cxyz/cw\mathbf x_{\rm ndc}=\mathbf c_{xyz}/c_w,将 x,yx,y 经视口变换转为像素位置。深度比较同样需要统一约定:可以比较同一投影后的深度,也可以将深度图反建为正的视图距离。

透视除法使等距离的三维步长在屏幕上并不等长。若沿屏幕直线逐像素步进,视图空间属性仍要按透视正确方式恢复;不能将投影端点的视图深度做普通线性插值。射线越过近裁剪面之前,应先裁剪有效区间,避免齐次除法在 w=0w=0 附近发散。

线性步进

Ray Marching 沿射线取样本,在每步将样本投影到屏幕并读取深度。射线深度从可见表面前方变到后方时,可能穿过了深度壳。

固定步长沿反射射线搜索深度壳交点

黄色点为起点,橙色点为沿射线的测试点,绿色点示意找到的交点;弯曲黑线代表几何截面。步长太大可能跨过薄结构,太小则在长空区反复取样。

深度图只有一层表面,并没有体积厚度。实现常给表面一个有限的深度容差 τ\tau,将 [zs,zs+τ][z_s,z_s+\tau] 当成可命中的深度壳。τ\tau 太小容易漏掉交点,太大会把与真实表面相距较远的射线也接纳,甚至错误命中后方自由空间。

发现跨越后,可以在前后两步之间回退、二分或细步搜索,提高交点位置精度。细化只改善已找到区间的定位,不能补回粗步已经跳过却没有发现的物体。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 概念版线性查询,所有 depth 使用同一正视图距离约定。
function traceScreenRay(ray):
previous = startAfterSurfaceOffset(ray)
for step in 0 .. maxSteps - 1:
current = advanceRay(previous, stepSize)
uv = projectToScreen(current)
if outsideScreen(uv) or outsideRayRange(current):
return miss
surfaceDepth = readLinearDepth(uv)
if intervalOverlapsDepthShell(previous, current, surfaceDepth, thickness):
candidate = refineIntersection(previous, current)
if validSurface(candidate):
return candidate
previous = current
return miss

边界、背景深度、射线朝向、法线朝向与厚度都影响最终判断。这是对屏幕深度壳求交的近似,不是对真实三角网格求交。

深度层级

为了减少空区查询,建立层级深度 Hierarchical Z,Hi-Z。以正视图深度、较小值靠近相机为例,父格记录子区域的最小值:

Zmin⁡(l+1)(u,v)=min⁡a,b∈{0,1}Zmin⁡(l)(2u+a,2v+b).Z_{\min}^{(l+1)}(u,v)= \min_{a,b\in\{0,1\}}Z_{\min}^{(l)}(2u+a,2v+b).

以最小深度逐层汇总的深度金字塔

底层覆盖原始像素,上层单个纹素覆盖更大屏幕区域,直到极粗的层。它们不是用于模糊着色的普通 mip;构造操作必须保留遮挡的保守界。

若射线在当前格内部的整个区间都满足 zr<Zmin⁡z_r<Z_{\min},则比该格所有表面更靠近相机,可以安全跨到格子的出口。普通平均值可能大于最近表面的深度,导致射线越过最近物体时还被误认为在“平均表面”之前。

粗块的保守深度界用于排除不可能相交的射线区间

图中白线是深度曲线,蓝色块是各层对一段区域的界。较粗的界可能包括很多空区域,但不能漏掉其子区域的表面;红射线不碰到粗界的区间,才可以整段跳过。其逻辑与 BVH 使用包围盒拒绝一组几何相似。

保守跳过需要检查射线在该格内部的区间,不能只比较格子入口处一个点。层级查询也应使用对应格的界,双线性插值几个父格会破坏原来的保守含义。

采用 Reverse-Z 时,“更近”的深度数值更大,应使用相应的最大值界与反向比较。无论哪种约定,算法依据都是最近表面的保守界。

层级遍历

课程用 Stackless Ray Walk 描述不显式维护遍历栈的访问方式:在确定没有命中的区域尝试更粗层,一次跨过更多像素;出现潜在交点时回到细层检查。

屏幕深度曲线上的反射路径与层级遍历示意

从左侧相机到下方表面是相机可见路径,之后向上的是反射路径。水平线划分屏幕区域,白线为可见深度壳;反射射线经过的空区可以用较粗格跳过,靠近白线时需要较细的检查。

课件简化逻辑是 above Z plane -> level++、below Z plane -> level--。其中层数增加表示查询更粗范围,不表示射线离场景更近。源伪代码省略了起点、格边界、步进回退和终止条件,实际实现可以写成以下更完整的概念流程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 在每格内部取射线区间,潜在相交时不跨过未经检查的细格。
cursor = firstValidRayPosition
level = 0
for iteration in 0 .. maxIterations - 1:
if outsideScreen(cursor) or pastRayEnd(cursor):
return miss
cell = alignedCellContaining(cursor, level)
segment = rayIntervalInsideCell(ray, cursor, cell)
closestDepth = readMinDepth(cell, level)

if segmentIsEntirelyInFront(segment, closestDepth):
cursor = justBeyondCellExit(segment)
level = min(level + 1, maxLevel)
else if level > 0:
level -= 1
// 保留该区间入口,定位正确的子格后继续检查。
else:
hit = testAndRefineDepthShell(segment)
if hit.valid:
return hit
cursor = justBeyondCellExit(segment)
return miss

粗格按 2l2^l 个像素对齐。起点位于中间、射线刚好落在边界或反方向跨格时,应选择射线实际进入的子格,不能简单将步长加倍而忽略对齐。小的边界偏移可避免反复查询同一格;偏移太大也会跳过几何。层级只改变访问效率,不能改变射线方向或把潜在交点直接当成命中。

交点着色

求交确定当前方向的来源 qq,之后仍按渲染方程计算:

Lo(p,ωo)=∫Ω+Li(p,ωi)fr(p,ωi,ωo)cos⁡θi dωi.L_o(p,\boldsymbol\omega_o)=\int_{\Omega^+} L_i(p,\boldsymbol\omega_i)f_r(p,\boldsymbol\omega_i,\boldsymbol\omega_o) \cos\theta_i\,\mathrm d\omega_i.

命中方向上的 LiL_i 由 Lo(q,ωqp)L_o(q,\boldsymbol\omega_{qp}) 提供。当前相机颜色记录的是 qq 朝相机的结果,若把它用于朝 pp 的出射方向,通常需要将次级反射源近似为 Lambertian。源是 Glossy 时,两方向可能看到完全不同的高光,因此读取同一个屏幕颜色会有方向误差。

输入宜为线性 HDR 的直接照明或明确允许复用的照明层。读取最终 Tone Mapping、UI 叠加后的颜色不符合辐亮度积分;把已经含 SSR 的最终图再作为同帧 SSR 来源,还可能重复反馈反射。接收表面自身的 BRDF 与 Fresnel 权重也不能省去。

沿光线的辐亮度不需要再乘 1/r21/r^2。这里以方向积分,面积到立体角的关系隐含在“某块表面占了多少采样方向”中;若改为对面片面积采样,才显式出现两个余弦与平方距离几何项。两种参数化不能重复计入。

射线的第一个有效交点同时回答了该近似路径上“哪块表面可见”。因此不需要为每个命中点另生成一张 VPL 阴影图,但这个可见性只针对当前屏幕深度壳。

粗糙反射

逐像素材质与法线形成的粗糙地面反射

怪物脚边的反射相对紧凑,更远位置的反射范围变宽,表面局部法线也改变了反射结构。课程用同一场景逐项展示四个目标:锐利与模糊反射、接触硬化、掠射拉长、逐像素粗糙度与法线。

接触硬化 Contact Hardening 在这里指靠近被反射物体时更清晰、远离时更分散。具有固定角宽的反射瓣,沿空间传播时足迹随距离扩大,这与把整幅镜像用相同半径模糊不同。

高光拉长 Specular Elongation 描述掠射观察时反射分布的拉伸。即使微面分布是各向同性,投影和方向几何也能形成拉长的屏幕结果,不能直接认定为各向异性材质。

各像素可以有不同粗糙度与法线,因此方向采样和过滤足迹也不同。若从概率密度 pωp_\omega 采方向,估计为

L^o=1N∑jLi(ωj)fr(ωj,ωo)cos⁡θjpω(ωj).\widehat L_o=\frac1N\sum_j \frac{L_i(\boldsymbol\omega_j)f_r(\boldsymbol\omega_j,\boldsymbol\omega_o) \cos\theta_j}{p_\omega(\boldsymbol\omega_j)}.

BRDF Importance Sampling 将样本集中到高贡献反射瓣内,提高有限预算的利用率。未命中屏幕信息的方向应明确用零或其他光照表示作 fallback;将其随意删掉再按命中数平均,会改变估计的能量。

复用与过滤

邻近接收点的反射路径可能相似,可以进行 Hit-Point Reuse,重用已经找到的交点,减少重复查询。但交点对于新接收点的方向、几何关系和 BRDF 权重不同,还要核对可见性与采样分布;邻像素颜色直接复制不是正确的样本复用。

另一优化是 Prefiltered Samples:预过滤交点颜色或照明,再按接收反射瓣的足迹选择尺度与权重。高粗糙度的方向范围更宽,但实际屏幕足迹还取决于交点距离和投影,不能仅按粗糙度给整幅图一个固定模糊宽度。

深度断层附近,普通颜色 mip 会混合前景与背景。联合双边等保边过滤利用深度、法线限制混合,可减轻这类污染。时间累积还可以复用前帧样本,但需要运动对应与历史有效性检查;这些步骤属于后续低采样重建的内容。

屏幕限制

手掌的相机不可见部分无法由当前屏幕深度完整重建

反射可能需要手掌内侧、背面或被手指挡住的几何,当前图像却只记录相机看见的表面。加密步进或提高深度层级精度,都不能补出不存在的输入。

离开屏幕后反射贡献突然截断

地面某些方向的射线离开视口,旗帜反射随之硬截断。将边缘或距离较大的贡献逐渐降低,可以缓和断层。

边缘渐隐减轻硬截断但削弱部分反射

渐隐后的交界更连续,但正确反射并未恢复。实用管线可以在 SSR 缺失时混合环境探针、体素或真实射线查询,权重应避免重复计光并保持连续。

SSR 不直接累加一组近距离点源,因而避开了基础 VPL 方法的一些尖峰与逐源遮挡开销;它仍可能产生采样噪声、错误深度命中、漏反射和时间伪影。优势主要来自镜面方向集中、相机数据可复用;接近漫反射时,大范围方向采样和屏幕缺失使这种优势减弱。