一次反弹

全局光照 Global Illumination,GI 包含光在场景中的直接传播和表面间反射。窗户照亮地板,地板又照亮没有直接看到窗户的墙面;彩色墙面反射的光还会改变附近物体的颜色,这种现象称为 Color Bleeding。给所有物体统一加一个环境常量,无法表达这些位置、方向和颜色的差异。

从路径追踪的观察顺序看,相机先看到点 pp,再从 pp 向某方向找到表面 qq,最后计算主光源如何照亮 qq。反过来看光的传播,是“主光源 →q→p→\to q\to p\to 相机”。直接受光的 qq 可以当成次级光源 Secondary Light Source,将其反射光作为接收点的入射光。

课程首先关注一次间接反弹。需要回答两个问题:哪些表面被主光源直接照亮?每个受光表面对当前点贡献多少?如果从主光源看场景,最先看到的表面正是它能直接照到的表面,Shadow Map 已经能记录这一层几何。

反射阴影图

反射阴影贴图 Reflective Shadow Map,RSM 在普通 Shadow Map 的深度之外,保存受光表面的位置、法线和反射光量。每个有效纹素对应一个小面片 Patch,之后将这些面片当成一组次级光源查询。

RSM的深度、世界坐标、法线和反射光量缓冲

从左至右依次是深度、世界位置、法线与反射光量。中间两幅中的 RGB 是数值编码:世界坐标映射为颜色,单位法线也映射到颜色范围,并不是表面的实际材质色。最右侧才包含不同表面反射光的颜色。

RSM 只从一个光源视角观察,却希望知道面片朝接收点的出射辐亮度。为了让这一方向变化可处理,将次级反射面片近似为 Lambertian:

Lodir(q)=ρqπEdir(q).L_o^{\rm dir}(q)=\frac{\rho_q}{\pi}E_{\rm dir}(q).

对于固定的入射照明,Lambertian 出射辐亮度不随观察方向变化,因此向光源方向记录的结果可用于面片的其他正面方向。这一假设针对次级源 qq;接收点 pp 仍可以使用 Glossy BRDF。

辐射通量 Radiant Flux 是总功率,辐射强度 Radiant Intensity 是单位立体角功率,辐照度 Irradiance 是单位接收面积上的入射功率,辐亮度 Radiance 还同时按投影面积和立体角归一化。它们的方向、面积因子不同,不能都用“亮度”代替后直接套同一个距离衰减。

面片贡献

令 r=∥q−p∥r=\|q-p\|,ωpq=(q−p)/r\boldsymbol\omega_{pq}=(q-p)/r,ωqp=−ωpq\boldsymbol\omega_{qp}=-\boldsymbol\omega_{pq}。从接收点看面片,只有其朝向接收点的投影面积起作用:

dω=max⁡(0,nq⋅ωqp)r2 dAq.\mathrm d\omega= \frac{\max(0,\mathbf n_q\cdot\boldsymbol\omega_{qp})}{r^2}\,\mathrm dA_q.

接收点p看到面片q的投影面积与立体角

图中上方的 qq 面片面积为 dA\mathrm dA,向 pp 投影时缩小为 dAcos⁡θ′\mathrm dA\cos\theta'。距离越远,同一投影面积占据的方向范围越小,按 r2r^2 缩小。下方接收面的 cos⁡θ\cos\theta 则来自渲染方程中接收侧的投影。这两个余弦分别属于不同表面。

将立体角积分改写为面片面积积分:

Loindir(p,ωo)=∫ALodir(q)fr(p,ωpq,ωo)V(p,q)cpcqr2 dAq,L_o^{\rm indir}(p,\boldsymbol\omega_o)= \int_A L_o^{\rm dir}(q)f_r(p,\boldsymbol\omega_{pq},\boldsymbol\omega_o) V(p,q)\frac{c_pc_q}{r^2}\,\mathrm dA_q,

其中 cp=max⁡(0,np⋅ωpq)c_p=\max(0,\mathbf n_p\cdot\boldsymbol\omega_{pq}),cq=max⁡(0,nq⋅ωqp)c_q=\max(0,\mathbf n_q\cdot\boldsymbol\omega_{qp})。将小面片用中心点近似,定义 Ψq=Lodir(q)ΔAq\Psi_q=L_o^{\rm dir}(q)\Delta A_q,则离散贡献是

Cq=Ψqfr(p,ωpq,ωo)V(p,q)cpcqr2.C_q=\Psi_q f_r(p,\boldsymbol\omega_{pq},\boldsymbol\omega_o) V(p,q)\frac{c_pc_q}{r^2}.

面积已含在 Ψq\Psi_q 中,求和时不用重复乘面积。若存储真正的 Lambertian 总反射通量 Φq\Phi_q,有 Φq=πLodir(q)ΔAq=πΨq\Phi_q=\pi L_o^{\rm dir}(q)\Delta A_q=\pi\Psi_q,计算前应除以 π\pi。不同 RSM 表达把“flux”归一化因子吸收到不同位置,实现时必须按存储定义一致处理。

距离项

单位方向的余弦形式使用 r2r^2 分母;若分子直接对位置差向量点积,则每个点积多出一个 rr:

max⁡(0,nq⋅(p−q))max⁡(0,np⋅(q−p))r4=(rcq)(rcp)r4=cqcpr2.\frac{\max(0,\mathbf n_q\cdot(p-q)) \max(0,\mathbf n_p\cdot(q-p))}{r^4} =\frac{(rc_q)(rc_p)}{r^4} =\frac{c_qc_p}{r^2}.

所以原论文中的 r4r^4 与归一化余弦写法的 r2r^2 完全等价。只改分母、不同时核对分子向量是否归一化,会造成错误的距离变化。

无参与介质的自由空间中,辐亮度沿射线传播不随距离衰减。这里的平方反比来自面积与立体角的几何换算,不是在传播过程中再削弱一次辐亮度。

RSM 纹素的光源投影面积相同,代表的实际表面面积未必相同。表面倾斜或采用透视光源时,应在反射量生成中处理对应面积或采样权重,否则接收求和的能量会随投影方式变化。

遮挡与采样

主光源到 qq 的遮挡通过光源视角渲染处理了,qq 到 pp 的遮挡仍然未知。若为每个次级源再建阴影图,成本会很高。基础 RSM 因而常省略 V(p,q)V(p,q),结果可能穿过本应挡光的物体。

光源可见面片不一定能对接收点产生有效贡献

黄色线描出直接受光表面,红点 xx 是接收点。某些面片与 xx 之间被桌体挡住,某些面片朝向不对,即使光源能看见它们,也不一定对 xx 有贡献;同一地面上的源还可能因为接收余弦为零而没有贡献。距离很远的面片通常贡献较弱,但不能仅凭“远”严格认定为零。

一张 512×512512\times512 RSM 有 262 144262\,144 个纹素,每像素遍历全部纹素不适合实时处理。将 pp 投到光源图中,在其投影附近选择少量面片,利用“图像附近往往在空间上也较近”的近似减少开销。这一关系在深度断层处容易失效。

RSM中心附近较密、外围较稀的采样及权重

中心附近采得更密,外围稀疏;圆的半径表示估计权重增大,不是面片自身变大。以离散纹素集合为例,若按概率 P(q)P(q) 选取 NN 个样本,无偏求和估计需要

L^oindir=1N∑j=1NCqjP(qj).\widehat L_o^{\rm indir}=\frac1N\sum_{j=1}^N\frac{C_{q_j}}{P(q_j)}.

只覆盖局部邻域会额外遗漏邻域之外的贡献。约 400400 个样本是课程的示例预算;扩大邻域、调整密度或更换场景后,样本数与权重也要重评估。将面片当成点源还会使极近距离项过大,可通过有限面片处理或距离正则化抑制,但后者引入偏差。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 第一趟:从主光源视角生成直接受光面片。
for each valid light-view texel q:
store position[q], normal[q], depth[q]
psi[q] = directOutgoingRadiance(q) * patchArea(q)

// 第二趟:接收点采样次级源;基础 RSM 不查询 q 到 p 的遮挡。
for each camera shading point p:
indirect = 0
for each sampled texel q with probability Pq:
delta = position[q] - position[p]
distanceSquared = dot(delta, delta)
direction = normalize(delta)
cosineP = max(dot(normal[p], direction), 0)
cosineQ = max(dot(normal[q], -direction), 0)
contribution = psi[q] * receiverBRDF(p, direction)
* cosineP * cosineQ / distanceSquared
indirect += contribution / Pq
indirect /= sampleCount

实际代码还应跳过自面片、无效纹素与退化距离,避免将背景深度当成有效次级源。

局部反弹

关闭与开启手电筒反弹照明的室内场景

下排开启反弹照明后,植被附近的绿色传到了顶棚和周围暗区;直接光照覆盖范围基本不变,变化来自受光表面再次反射。少量局部色彩贡献已经能够表现光源与环境的关系,不必把每处暗区都用统一环境项加亮。

RSM 易与 Shadow Map 管线结合,也与 Virtual Point Light,VPL 的思路相关:将受光面片离散为一组虚拟光源。Instant Radiosity 同样利用虚拟光源近似间接照明,但产生样本的过程不同。

每个主光源通常需要一份 RSM,成本随主光源数量增长。忽略次级可见性、次级源漫反射假设、投影邻域采样和有限面片近似,都会影响质量。虽然数据存在二维图像中,它来自光源视角并保存世界位置,不只局限于最终相机的当前画面;这是课程将它归入三维 GI 的原因。

光传播体积

RSM 让每个接收点反复查询许多面片。光传播体积 Light Propagation Volumes,LPV 尝试先将光照送到整个空间,再让接收点查询附近的光照记录。

将场景划为三维网格,单个格子称为体素 Voxel。每个体素保存随方向变化的辐射信息,课程用前两带 SH 共四项来近似,记录较粗的方向分布,而不保存精确的每条光线。

LPV 的处理顺序为 Generation、Injection、Propagation、Rendering:先找受光面片,再注入网格,之后传播若干轮,最后查询着色。这些数据可随光源变化重算,因此区别于固定场景下的离线 PRT。

注入与传播

Generation 可以复用 RSM 面片;Injection 将面片放进对应体素,对其出射方向分布投影并累加四个 SH 系数。课程的低阶实现以漫反射次级源近似为主,低阶 SH 会抹去很窄的方向瓣。

受光表面向所在体素注入低阶球谐光照

粉色箭头表示受光蓝色表面发出的反射光,红框内的 SH 图形表示这一格保存的方向分布。黄色是主光源直接照到的区域,不能把注入格子的反射光与直接照明混为一份量后重复相加。

传播时,从邻格共享面的方向范围收集辐射,转换成当前格的 SH 表示。三维网格有六个面邻居;二维示意只画四个。对角格可以经过多次轴向传播到达。

一个体素依次收集各邻面方向的辐射贡献

图中 aa 到 dd 逐步显示不同邻面贡献如何叠加到中央格的方向分布;这是一次收集邻格的示意,不能将四幅图直接解释为四次物理反弹。整个网格完成一轮后,再基于上一轮的数据传播下一轮。课堂举约四到五轮的例子,实际轮数影响覆盖距离、误差和成本。

将方向积分和 SH 投影组合成邻面传输算子 Av←uA_{v\leftarrow u},可以概括为

cv(k+1)=∑u∈N6(v)Av←ucu(k).\mathbf c_v^{(k+1)}= \sum_{u\in\mathcal N_6(v)}A_{v\leftarrow u}\mathbf c_u^{(k)}.

必须用上一轮网格读取、新网格写入,避免更新顺序改变传播结果;各轮贡献是否累积、是否重复计数则依具体传播设计处理。这里的迭代是离散空间中的传递,不是物理时间积分。没有额外表面相互作用时,增加传播轮数主要使光走得更远,不能自动等价为增加真实反射次数。

体积误差

着色点找到所在格或周围格,插值得到方向光照,再结合接收 BRDF 积分。接收侧可以是 Glossy,但只有四项的入射表示无法保留尖锐反射细节。

LPV屋檐下的漏光与参考结果比较

左图屋檐内侧比参考图亮,反映了粗体素与不完整遮挡造成的漏光。若一个格子同时跨过墙的前后两侧,格内近似均匀的光照记录无法区分两侧;传播再省略格间可见性,光便可能穿过墙体。

提高分辨率能减小部分误差,但每个维度加倍,三维体素数就增为八倍,多轮传播的读写成本也增加。级联 Cascade 网格可以在近处保留细格、远处用粗格,平衡局部细节与空间范围;不同网格交界仍需处理插值和更新。

光源变化后重新注入与传播,可以得到动态 Color Bleeding。但光照的平滑程度、薄几何和遮挡精度仍受体积表示约束。带介质吸收或散射的传播还需要其他项,不能沿用“真空辐亮度不衰减”来忽略介质效应。

体素光照

体素全局光照 Voxel Global Illumination,VXGI 同样使用三维网格,但处理方式不同。它先将场景几何体素化并保存局部光照,接收点再按所需方向查询这些体素;不先像 LPV 那样把光照反复传播到整个空间。

全场景体素化及粗细层级表示

左图红色网格表示场景被离散成体素;右图表现不同大小格子的层级。细层记录局部结构,粗层汇总较大空间范围,可用八叉树或类似三维 mip 层级组织。层级需要保存占据、透明度或覆盖信息,以及辐射和法线的方向信息;只存一个平均颜色不足以描述任意方向反射。

第一阶段生成直接光照,将其与体素化几何及方向信息关联,再构造粗层;第二阶段由相机接收点发出查询。动态物体变化需要更新体素化与层级,不能只修改相机颜色图。

锥体查询

锥体追踪 Cone Tracing 用一个逐渐变宽的方向范围代替许多相近射线。Glossy 表面可沿反射方向查询一个锥体;粗糙度越高,锥体张角越大。漫反射需要覆盖半球,课程用八个锥体作示例,各锥贡献应按方向覆盖与余弦权重组合,不能随意相加导致能量增加。

多个方向锥体共同覆盖表面半球

图中的椭圆是各锥截面,多个方向共同覆盖较宽的接收范围;橙色箭头是观察方向。图标为 AO cones,强调相同半球划分也可以用于遮蔽查询;计算漫反射 GI 时还要读取辐射并乘反射权重,不能只读取占据量。

锥体足迹增大时查询越来越粗的体素层级

左图紫色截面沿锥体变大,查询从细层转向粗层;右图彩色方块表示相应的体素采样尺度。图中的 Quadrilinear 指三维空间插值,再在相邻层级之间插值,不是普通二维纹理的四个邻纹素。

若锥体半角为 θ\theta,距起点 tt 的截面直径近似为 d(t)=2ttan⁡θd(t)=2t\tan\theta。基础体素宽度为 s0s_0,可据此选择

LOD(t)≈log⁡2d(t)s0,\mathrm{LOD}(t)\approx\log_2\frac{d(t)}{s_0},

并夹到有效层级。粗层将较大足迹内的几何和光照过滤汇总,使一次查询代表一束方向。

沿锥体前进时,前面的占据应削弱后面贡献。用预乘透明度辐射 L~j\widetilde L_j 和不透明度 aja_j,一种示意累积为

C←C+TL~j,T←T(1−aj).C\leftarrow C+T\widetilde L_j, \qquad T\leftarrow T(1-a_j).

TT 表示尚能穿过前方体素的比例。距离步长与采样尺度一致、避免过度重复采样,也是累积需要处理的条件。

1
2
3
4
5
6
7
8
9
10
11
12
13
// 查询一个反射锥;体素数据已在前一阶段生成。
function traceCone(origin, direction, halfAngle):
color = 0
transmittance = 1
distance = initialOffset
while distance < maxDistance and transmittance > threshold:
diameter = max(baseVoxelSize, 2 * distance * tan(halfAngle))
lod = clamp(log2(diameter / baseVoxelSize), 0, maxLod)
sample = readVoxelHierarchy(origin + distance * direction, lod)
color += transmittance * sample.premultipliedRadiance
transmittance *= 1 - sample.opacity
distance += stepFactor * diameter
return color

这是层级查询与合成的示意,实际 VXGI 的方向存储、遮挡表示和锥体权重需保持一致。粗体素会混合薄墙两侧的信息,宽锥可能把不相邻的表面合在同一足迹中,所以它仍不是对完整三角场景的精确射线求交。较细体素、更多锥和更完整的方向数据可以改善质量,也增加构建、内存和每像素查询成本。