本节摘要:路径追踪把 5.4 的渲染方程当真:对每个像素随机发射大量路径、按蒙特卡洛求平均,噪声随样本数收敛——影视级真实感的标准答案。体绘制则面向另一类数据:三维标量场(CT 扫描、流体模拟),不再有"表面"可求交,光线在体数据中边走边吸收与发光。本节给出两套算法的最小实现与收敛直觉,并说明降噪器如何让路径追踪逼近实时。这是"像素诞生记"在数学上的最深一站。
阅读完本节,你应当能够:
5.4 的渲染方程是一个半球积分,被积函数(双向反射分布)任意复杂,解析解不存在。Kajiya 1986 年与渲染方程同框登场的路径追踪给出实用解法:积分值 ≈ 随机采样 N 次的平均。直觉是把半球想成靶盘,蒙眼投镖 N 次,命中的函数值平均就是积分的估计——样本越多估计越准:
蒙特卡洛估计:∫f(x)dx ≈ (1/N)·Σ f(xi)/p(xi) p(xi) 为采样概率密度;均匀采样半球时 p 为常数 误差 ∝ 1/√N:样本翻 4 倍,噪声减半——收敛慢,但"无偏"(均值正确)
import numpy as np, random def estimate_pi_mc(n): # 最小蒙特卡洛:单位圆面积的积分估计(1.3 节的例子回归) hit = sum((random.uniform(-1,1)**2 + random.uniform(-1,1)**2) <= 1 for _ in range(n)) return 4 * hit / n for n in (100, 10_000, 1_000_000): print(f"N={n:8d}: π ≈ {estimate_pi_mc(n):.4f}") # 输出示例: # N= 100: π ≈ 3.2400 噪声大 # N= 10000: π ≈ 3.1464 收敛中 # N= 1000000: π ≈ 3.1429 1/√N 的慢而稳

每个像素发射一条随机路径:在交点处按材质的概率分布随机选一个新方向继续走(漫反射均匀选半球、镜面沿反射方向窄分布选),路径沿弹射累积能量贡献,命中光源或弹射数用尽即终止。逐像素重复成千上万条路径求平均:
def path_trace(O, D, depth=0): # 单条随机路径的辐射估计(简化版:仅漫反射弹射) hit = nearest_intersection(O, D) if hit is None or depth >= 5: return 0.0 P, N, albedo = hit # 随机采一个半球方向:余弦加权采样(p 与 cos 成正比,方差更小) Ldir = random_hemisphere_dir(N) # 渲染方程的递归形式:反照率 × 下一跳贡献(余弦被采样概率约掉) return albedo * path_trace(P, Ldir, depth+1) def render_pixel(px, samples=1024): # 逐像素 N 条路径求平均——蒙特卡洛积分 return sum(path_trace(*pixel_ray(px)) for _ in range(samples)) / samples
关键工程细节:直接光采样(专门朝光源采样一跳,避免"路径恰好命中小光源"的低概率浪费)是方差削减的第一课,能让同样的样本数噪声骤降一个量级。其他常用武器:重要性采样(按材质分布选方向)、双向路径追踪、多重重要性采样。单帧电影级画面的每像素样本数以万计,农场渲染一帧数小时是常态——真实感明码标价。
CT 每层切片、流体模拟每个格子,数据是三维数组 f(x,y,z)——密度、温度、速度。里面没有三角形可交,光线穿过体素时按局部密度部分吸收、部分发光(火焰发光、烟雾遮光):
体绘制积分(沿射线参数 s): 颜色 = ∫ 发光(s) · 透过率(0→s) ds 透过率 = exp(-∫ 密度(u)·衰减系数 du) 每前进一步,剩下的光按 e 指数衰减
def volume_raymarch(density_field, O, D, step=0.05, max_s=4.0): # 光线步进:边走边累加发光、扣除透过率 color, transmittance, s = 0.0, 1.0, 0.0 while s < max_s and transmittance > 0.01: p = np.array(O) + s*np.array(D) d = density_field(p) # 采样体数据(三线性插值) a = 1 - np.exp(-d * step) # 这一步的不透明度 color += transmittance * a * emit_color(d) # 发光按剩余透过率加权 transmittance *= (1 - a) # 剩余光被吸收 s += step return color # 密度场:中心一团"发光云" dens = lambda p: np.exp(-np.dot(p, p) * 3) emit = lambda d: d * 2.0 # 密度越大越亮 print(round(volume_raymarch((0,0,-3), (0,0,1)), 4)) # 输出约 0.87:穿过云心一路累积发光,前段透过率高贡献大、后段衰减
传输函数把密度值映射为颜色与不透明度,是科学可视化的"调色盘":医生用它凸显骨骼密度区间、气象员用它染出台风降水带。游戏里的体积雾、云海用同一套方程的实时近似(少步数 + 降噪 + 半分辨率),医学与科研则用全精度离线版本。
路径追踪的 1/√N 收敛太慢,AI 降噪器横空出世:用少量样本(如 32)的噪声图加上辅助缓冲(法线、深度、反照率),神经网络"脑补"出接近万样本的干净结果——影视与 RTX 游戏都已在用。它把成本从"更多样本"换成"一次网络推理",代价是极端场景下会抹掉真实细节(细小阴影、焦散)。这是 7.4 节 AI 与图形学交汇的先声。
物理真实之外还有另一座山——7.3 节看非真实感渲染如何主动放弃"真",追求风格与表达。