上一章导读里那面雨夜橱窗提出了问题,本节是实验室的第一张台子:把"光线追踪为什么会存在"这件事放在历史的显微镜下看清楚——四个转折点,每个转折点都因为前一个方案撞上了一堵具体的墙。看清这堵墙,后面的每一章才有存在的理由;本节末尾的递归伪码,则是你今后所有实验的骨架模板。
1968 年 Arthur Appel 就做过"ray casting"——从视点向场景投射线,只找最近交点,用来画阴影轮廓。它证明了这个方向可行,但止步于一次弹射:没有反射,没有折射,玻璃在它眼里就是一块普通表面。真正的转折发生在 1980 年,Turner Whitted 发表那篇后来被引用了半个世纪的论文,把"递归"两个字加进了流程:主光线命中玻璃,就派生一条反射射线、一条折射射线;命中金属,就派生反射射线;每条次级射线再重复同样的逻辑,直到深度用尽。镜面倒影、透明折射,这些光栅化管线拼了命也凑不齐的现象,一夜之间变成了十几行递归代码的自然产物。
代价同样一目了然。1980 年代的 CPU 上,渲染一张几百乘几百的 Whitted 风格图像要花几个小时,而同期 SGI 工作站上的光栅化硬件已经能实时填充多边形。于是图形工业界集体选择了后者,光线追踪被贴上"学术玩具"的标签,沉寂了近三十年——但理论并没有停。1986 年 James Kajiya 发表渲染方程,第一次把"一个像素的颜色"写成能量守恒的积分方程:出射辐射亮度等于自发光加上入射光在半球上的加权积分。这个方程不提供算法,它提供的是验收标准:此后所有渲染算法,无论光栅化还是路径追踪,都可以拿这把尺子量一量自己近似了哪一项、丢掉了哪一项。
方程写出来了,怎么解?半球积分没有解析解,Kajiya 给出的答案是蒙特卡洛:用随机采样的均值去逼近积分。这就是 1980 年代末到 1990 年代路径追踪(Path Tracing)的起点——不再像 Whitted 那样只认镜面和折射这类"确定方向"的路径,而是让每条射线在命中点按材质的概率分布随机选一个方向继续走,走出成千上万条路径后取平均。软阴影、颜色溢出、焦散,这些过去要各写一个特技模块的现象,全部变成同一套统计估计的副产品。1997 年 Eric Veach 的博士论文又把这套工具箱补齐:双向路径追踪、多重重要性采样、Metropolis 光传输,专门治理"路径难得、噪声收敛慢"的疑难杂症。同一时期 Henrik Wann Jensen 的光子映射(1996)从光源方向正向撒光子,补上了路径追踪最头疼的焦散与间接镜面链路。

历史线走到 2018 年出现最后一次转折:GPU 厂商把 BVH 遍历与射线求交做成固定功能单元(RT Core),微软同步推出 DXR 接口,光线追踪第一次以"实时管线公民"的身份进入消费级硬件。此后的故事——降噪、半分辨率追踪、混合管线——都是"每帧只有几毫秒"这个新约束下的适应过程,我们会在第 7、8 章展开。
历史讲完,上实验台。下面这段伪码是 1980 年那篇论文的核心逻辑,也是今天所有路径追踪器、所有实时光追管线的共同祖先。先看代码,再拆它埋下的三个设计决定。
function whitted_trace(ray, depth): hit = scene.intersect(ray) # 最近求交:场景接口只此一个 if hit == miss: return background_color(ray) # 逃逸出场景,返回环境色 color = hit.emission # 命中点自发光(光源本身) for each light in scene.lights: # 直接光照:硬阴影版 shadow_ray = make_ray(hit.pos + hit.normal * EPS, light.dir) if not scene.occluded(shadow_ray): color += hit.brdf * light.intensity * dot(hit.normal, light.dir) if depth < MAX_DEPTH: if hit.material.is_specular: # 镜面:派生反射射线 r = reflect(ray.dir, hit.normal) color += hit.material.specular * whitted_trace(make_ray(hit.pos, r), depth + 1) if hit.material.is_transparent: # 透明:派生折射射线 t = refract(ray.dir, hit.normal, hit.material.ior) color += hit.material.transmission * whitted_trace(make_ray(hit.pos, t), depth + 1) return color
第一个决定是递归。反射射线命中新表面后,同样走"直接光照 + 再次派生"的流程,深度参数负责刹住递归。这个结构注定被第 6 章的路径追踪改写成迭代循环,但"每次命中只做三件事——收自发光、问直接光、按材质派生"的心法保留到了今天。第二个决定是阴影射线是一次布尔测试:它只回答"光源看得见我吗",不做任何着色计算,这是全部射线家族里最便宜的一种,也是第 3 章 any-hit 求交接口的原型。第三个决定最容易被忽视:漫反射被当成了死胡同。Whitted 模型里命中粗糙表面就直接光照收工,间接漫反射整项缺失——这正是 1986 年渲染方程要补上的那块,也是路径追踪相对递归模型质的飞跃。
把这段伪码跑在三个球加一个平面的经典测试场景上,你会得到第一张"像样"的图:金属球映着同伴,玻璃球背后景物扭曲,地面阴影边缘锐利如刀。锐利恰恰是破绽——真实的软阴影来自光源面积,而 Whitted 的阴影射线只认点光源。这个破绽就是通往第 2 章物理讨论的入口:阴影该软还是硬,不是美术偏好,是光源大小的几何推论。
既然 1980 年算法就齐了,为什么拖到 2018 年才进显卡? 卡住它的从来不是论文,是三道门槛的叠加。第一道是算力账:递归射线每像素几十条、每条都要随机访存去遍历场景,这种负载对为光栅化流水线设计的固定功能单元是错位竞争,直到通用 GPU 把"着色器里随便写循环"变成一等公民才够得着。第二道是生态账:DXR 之前没有统一接口,每家硬件各养一套私有路径,引擎厂商不敢把产品押在孤岛上。第三道是需求账:分辨率涨到 4K、材质全面物理化、场景动态化,光栅化管线上的补丁摞到难以为继,市场才开始愿意为"物理正确"支付电费。三道门槛在 2018 年前后同时松动,才有了那次范式交接。
路径追踪和光线追踪是什么关系? 很多文章混着用,严格区分的话:光线追踪是"按光线组织计算"的方法学——一切计算都围绕一条射线的行进与命中展开;路径追踪是它的一种蒙特卡洛解法,专门求解渲染方程。Whitted 递归、光子映射、双向路径追踪都在这个方法学之下,区别只在路径怎么构造、期望怎么估。本册的章节安排正是按这条族谱推进的。
离线渲染为什么等了几十年也不用专用硬件? 因为离线买得起时间。渲染农场用几千台机器把一帧摊成几个小时,纯软件遍历照样吃得下十亿级三角形的场景;RT Core 治的是"一帧只有十六毫秒"这个离线根本不关心的病。这也解释了本册的编排:第 6 章讲路径追踪时全程不出现专用硬件,它要到第 8 章才登场。
历史给足了动机,下一节把本节反复出现的"射线、命中、材质"这些词钉成严格定义——术语速查表,是进入物理章节前的最后一站语言准备。