上一节的历史里,"递归""阴影射线""求交"这些词反复出现却从未被正式定义——本节就是实验室的术语校准台。全册后续所有伪码、所有管线图都建立在这一节的四组概念上:射线的数学定义、按意图划分的射线家族、命中记录的三个要素、以及求交接口的三种返回方式。读完本节,你应当能读懂任何一份光追伪码而不卡壳。
数学上,一条射线是两个向量加两个标量:起点 origin、单位方向 direction、有效区间 tmin 与 tmax。空间中任意一点写作 origin 加 direction 乘 t,其中 t 是沿方向的距离参数。这个参数化是整个学科最常用的 trick:求交计算的产出就是"最近的有效 t",命中点、命中点到光源的距离、命中点到相机的深度,全部由同一个 t 表达。
工程上,射线从来不是裸的四个数,而是一个携带"意图标签"的结构体。方向必须归一化,否则 t 的物理含义(距离)就会失真;tmin 通常取一个很小的正数而不是零,这是为了躲开"自交"——命中点正好落在上一条射线的几何表面上,从零起步会立刻撞见自己,产生芝麻大的黑点,业界俗称 shadow acne。这些细节本节先立规矩,第 3 章会逐条兑现成代码。
struct Ray { vec3 origin; // 起点 vec3 dir; // 方向,必须归一化 float tmin; // 有效起点,典型取 1e-3,防自交 float tmax; // 有效终点,如 1e30 表示无限远 int kind; // 意图标签:primary / shadow / reflection / ... }; struct HitRecord { float t; // 命中距离,最近的有效 t vec3 pos; // 命中点 origin 加 dir 乘 t vec3 normal; // 几何法线,着色前要先翻向视线一侧 vec2 uv; // 纹理坐标,插值自三角形顶点 int mat_id; // 材质索引,决定命中后派生什么射线 };
把射线按意图分类,是理解一切光追系统的最快路径。同一发"几何探测"在不同意图下,求交的精度要求、返回时机、后续处理完全不同。
**主光线(primary ray)**从相机出发穿过像素,提问"这个像素最先看见什么"。它决定了画面的几何骨架,数量恰好等于采样数乘像素数,是全场景数量最多、方向最相干(coherent)的一批射线——相邻像素的主光线方向接近,这个性质在第 9 章的并行优化里会被反复利用。
**阴影射线(shadow ray)**从命中点射向光源,提问"光源看得见我吗"。它只需要一个布尔答案:任何距离小于"命中点到光源"的命中都意味着遮挡,立刻返回。省略一切着色信息,让阴影射线成为最便宜、也最先被搬进实时管线的光追应用——第 7 章你会看到,2018 年第一批实时光追游戏几乎全用它做硬阴影。
反射射线与折射射线回答"镜面/玻璃后面是什么"。反射方向用入射方向与法线的镜像公式一步算出;折射方向按斯涅尔定律弯折,且要区分从外向内和从内向外两种情形。它们是 Whitted 递归的发动机,在路径追踪里则退化为材质采样的一种可能结果。
**遮挡与环境光遮蔽射线(occlusion ray)**在半球内随机取短距离方向,提问"这附近挤不挤"。它不求最近命中,只统计遮挡比例,用来近似环境光衰减——比完整路径追踪便宜几个量级,是离线烘焙和实时管线的常客。

同一条几何射线,按意图不同要问求交器不同的问题,这催生了三种接口约定,第 8 章讲 DXR 与 Vulkan 光追接口时会原样重逢。
closest_hit(ray) -> HitRecord # 最近命中:主光线、反射折射用 # 需要完整遍历加速结构,成本最高 any_hit(ray) -> bool # 任一命中即返回:阴影、遮挡用 # 找到一个就提前退出,成本最低 miss(ray) -> bool # 逃逸测试:命中数为零时的出口
closest-hit 必须扫完整棵加速树才能保证"最近";any-hit 一旦撞上任何东西就能收工。这个成本差是实时光追预算分配的第一原则:能降级成 any-hit 的问题,绝不喂给 closest-hit。还有一个容易被忽略的点:透明体的阴影不能用 any-hit 的"撞见即返回"直接判死——玻璃挡住的光并没有全被吃掉,正确做法是命中后衰减能量继续走,这也是实时管线里"透明体阴影"至今仍是难点的原因。
把术语落到一个可复现的小实验上。场景:一面镜子挂在红墙前,墙角放一盏点光源。渲染镜面上一个像素的完整流程如下。第一步,主光线从相机穿像素出发,closest-hit 返回命中记录:命中镜子,法线朝外,材质索引指向"理想镜面"。第二步,按镜面材质派生反射射线,方向为入射方向对法线镜像;这条射线 closest-hit 命中红墙,返回墙面命中记录。第三步,在红墙命中点上,对场景里那盏点光源发射阴影射线——any-hit 返回未被遮挡,于是红色漫反射的直接光照成立。第四步,把红墙返回的能量乘以镜面反射率,加到最初那个像素上。结果:镜子里映出一块被点光源照亮的红墙。变式一:把光源挪到镜子背后,阴影射线会返回遮挡,红墙只剩环境项,镜中影像明显变暗——你会亲眼看到"阴影是几何关系,不归镜子管"。变式二:把镜子换成磨砂材质,理想反射变成按粗糙度采样的随机方向,单次采样噪声四起,这正是第 6 章路径追踪要接手的局面。
这个实验值得亲手做:五十行代码以内就能复现,做完之后回头看本节的结构体定义,每一个字段都有了具体的脸色和脾气。
定义看着平淡,工程化时最容易在三个细节上翻车。其一,方向向量的归一化不是洁癖而是记账需要:命中距离 t 的物理意义是"沿方向走的长度",只有方向是单位向量,t 才能直接参与"衰减与距离平方成反比"这类物理公式;忘记归一化的渲染器最典型的症状是"灯离得越远反而越亮"。其二,tmax 不是摆设:给主光线设一个远平面就能把天光排除在求交之外;给阴影射线把 tmax 设成"到光源的距离",光源后方的误命中天然被截断。这些省下来的遍历开销在实时管线里是实打实的毫秒。其三,epsilon 与场景尺度必须绑定:同样取千分之一这个数,在以毫米为单位的珠宝模型里是一道深渊,在以公里为单位的地形里是一层灰尘。稳妥做法是让 tmin 随命中距离与包围盒尺度自适应缩放,而不是全局拍一个常数了事。
顺手做一个一分钟的口算实验:射线从原点指向正前方,一颗半径半米的球悬在两米外,球心恰好在射线上。直觉会喊"命中点就是球心,两米处",可下一章的一元二次方程一解,近端命中其实在一米五——球面比球心早半米接住射线。这半米之差正是阴影偏移、折射起点这类工程量真正要的精度:靠直觉估会差出一个半径,靠方程才有可靠的 t。口算暴露直觉的粗糙,方程给出可依赖的数字,这个次序在全册会反复出现。
术语台校准完毕。不过到目前为止,"颜色"对我们仍是一个含糊的日常词——它到底是什么物理量、按什么规律在表面上分配?下一节请出对比的另一位主角光栅化,把两条路线放上台面对质。