同一个像素前方有两个球,应该显示哪一个?光栅器把三角形投到像素上,再比较深度;射线方法从像素反推一个观察方向,分别询问场景中的几何,最后保留沿这个方向最先到达的表面。

两种方法都在解决可见性。本篇只生成物体编号图和法线图,不计算反射光、阴影或多次散射。颜色连续变化可以来自法线可视化,不能据此把图像称为真实感光照结果。

从像素中心到世界射线

射线写成 r(t)=o+td。o 是起点,d 是方向,t 是参数。d 若为单位向量,t 才等于沿射线走过的距离。将 d 放大两倍,同一个交点的 t 就减半,几何位置不变。

沿用第 02 篇相机约定:右手坐标系,相机向局部 −Z 观察,屏幕 y 向下。对于宽 W、高 H 的图像,在像素 (x,y) 中心取样,垂直视场角为 θ,像平面上的横纵坐标为:

sx=(2x+0.5W1)WHtanθ2,sy=(12y+0.5H)tanθ2.s_x=\left(2\frac{x+0.5}{W}-1\right)\frac WH\tan\frac\theta2, \qquad s_y=\left(1-2\frac{y+0.5}{H}\right)\tan\frac\theta2.

相机的 right、up、back 基向量来自已有 look_at。世界方向为 normalize(sₓ right+sᵧ up−back),起点是 eye。减 back 对应向前看,不能直接照搬采用 +Z 相机的教材代码。

视场角是垂直角,因此只有横坐标再乘宽高比。否则宽屏图像会压缩模型。像素中心的 0.5 也不能遗漏;奇数分辨率的正中像素应沿相机正前方发射。检查器使用 3×3 图像中心,确认方向为 (0,0,−1)。

本篇每像素一条射线,没有抗锯齿。后续采样章节可将像素内部位置改为随机变量,但不能先把边缘锯齿归因于求交错误。这里使用针孔模型,也不包含景深。

球面方程怎样变成两个根

球心 c、半径 R 的表面满足 |p−c|²=R²。令 v=o−c,把 r(t) 代入并展开,得到:

at2+2ht+k=0,a=dd,h=vd,k=vvR2.a t^2+2h t+k=0,\qquad a=d\cdot d,\quad h=v\cdot d,\quad k=v\cdot v-R^2.

判别式使用 Δ=h²−ak。Δ<0 没有实根;Δ=0 表示切线重根;Δ>0 有两个候选根。不能假设 a=1,因为对象空间变换之后的方向通常不再是单位长。

直接计算 (−h±√Δ)/a 时,如果两项接近,减法会丢失相对有效位。实现选取 q=−h−copysign(√Δ,h),再计算 q/a 与 k/q。这利用根的乘积 k/a,避免在其中一个根上做相近数相减。

这个改写没有解决所有浮点问题。Δ 本身仍由两个可能接近的数相减,极端尺度仍会误判相交状态。代码使用普通 double,没有 PBRT 的区间算术、误差传播和交点重投影保证。不能把“稳定求根公式”扩大为“所有输入都稳健”。

当 Δ=0 时直接使用 −h/a,不经过 k/q;射线从表面沿切向出发时可能同时有 h=0、k=0,盲目除 q 会产生 0/0。检查器专门加入这个零重根例子。

两个根先排序,再按区间筛选。当前接口接受 t∈(minimum,maximum]:下界严格排除,上界包含。单位球前方 o=(0,0,−3)、d=(0,0,1) 的根为 2 和 4;当 minimum=2 时应选 4,而不是因较小根无效就把整个球拒绝。

从球心出发同样有一负一正两个根。负根位于射线背后,正根才是出射表面。区间约定让这类行为明确,而不是依赖“通常相机在物体外面”的假设。

三角形求交同时给出重心坐标

三角形顶点 A、B、C,令 e₁=B−A、e₂=C−A。交点满足 o+td=A+u e₁+v e₂,这是关于 t、u、v 的三元线性方程。

累计实现采用双精度 Möller–Trumbore 形式:先算 p=d×e₂ 与 det=e₁·p,再由点积和叉积解出 u、v、t。u≥0、v≥0、u+v≤1 表示点落在三角形内,顶点权重为 (1−u−v,u,v)。这与光栅阶段的重心插值含义相同,但权重来自世界几何求交。

当前不做背面剔除,det 正负两侧都接受。几何法线按顶点绕序取 normalize(e₁×e₂),不会自动翻向观察者。后续 BSDF 若区分内外侧,需要保留这个方向约定。

det=0 时拒绝平行或退化情形。代码没有使用固定的 |det|<10⁻⁹ 门限,因为 det 还受边长与方向长度影响,它不是一个无量纲夹角。给所有场景套同一个常数会错删真实交点。

例如 A=(0,0,0)、B=(1,0,0)、C=(0,1,0),射线起点 (−0.75,0.25,10⁻¹²),方向 (1,0,−10⁻¹²)。它在 t=1 到达 (0.25,0.25,0),确实位于三角形内,但 det 只有 10⁻¹²,固定门限会把它当作平行。

只检查 det 是否恰好为零也不是稳健谓词的替代。近退化输入会放大舍入误差,共享边两侧也可能作出不一致判断。PBRT 第四版 §6.5.3 使用变换到射线坐标系后的剪切与边函数方法,本篇的简短实现不继承其 watertight 性质。验收只证明列出的用例,不证明任意网格没有裂缝。

变到模型空间时保留参数

第 16 篇已经把资源坐标与世界实例分开。射线查询可以将世界射线乘实例逆矩阵,在资源空间求交,而不用每次变换全部模型顶点。

对仿射矩阵 M,有 M⁻¹(o+td)=M⁻¹o+t M⁻¹d。起点按点处理,齐次分量为 1;方向按向量处理,齐次分量为 0。因此只要不额外归一化变换后的方向,原来的 t 就被保留。

把单位球沿 X 放大两倍,得到世界椭球。世界起点 (−3,0,0)、方向 (1,0,0),进入椭球时 t=1。逆缩放后的起点为 (−1.5,0,0),方向为 (0.5,0,0),在单位球上求得的根仍为 1 和 5。

如果擅自把对象方向改成 (1,0,0),最近根会变成 0.5。直接把这个参数带回世界射线,点会错误地落在 x=−2.5,尚未到达椭球。当前实验使用已知逆缩放验证 t=1 与世界点 x=−2,不实现通用矩阵求逆或完整实例射线接口。

交点位置可以乘 M 回到世界;表面法线则使用第 09 篇已有的逆转置规则再归一化。位置、方向和法线的变换不能合并成一个不区分语义的函数。

自相交为何不能只调大 epsilon

从计算出的表面点发出下一条射线时,舍入误差可能把起点放到物体内部。即使 t=0 被排除,仍会出现极小的正 t,看起来像物体挡住了自己。

实验用单位球顶部真值 (0,0,1),人为加入已知位置误差 η=10⁻⁸,将起点设为 (0,0,1−η),方向向外。程序确实得到 t=1.00000000447364×10⁻⁸ 的命中。这是可控误差反例,不是宣称当前相机渲染自然产生了同样大小的误差。

将起点移到 z=1+η 后,向外射线不再命中球。若射线向内,则应把起点放在内部一侧;检查器确认从 z=1−η 向内仍能在 t=2−η 到达另一侧。不能通过永久忽略原物体编号修复自交,否则穿过物体的合法交点也会丢失。

偏移还可能越过邻近表面。在 z=1+2η 放置一个三角形,从 z=1+η 向外能命中;把起点过量移到 z=1+3η,就错过了它。增大 epsilon 可以减少自交,也可以制造漏光,二者不矛盾。

通用方案需要估计交点误差界,按射出方向选择几何法线一侧,并处理浮点向外舍入。PBRT §6.8 对此有完整推导。本篇只用已知 η 展示两类错误,没有把固定偏移包装成生产级 SpawnRay。

最近交点与实际图像

Primitive 保存球或三角形以及编号。brute_hit 逐个调用同一求交接口,获得候选后收紧射线 maximum。找到一个命中不能立即返回,因为后面的物体可能更近。

两个候选 t 完全相等时选择较小编号,上界使用包含关系,保证后到的等距候选仍有机会参与决胜。这里的“相等”是浮点结果精确相等,不用任意容差把前后不同表面合并。重复单位球编号 9 和 2 的检查结果为 2。

场景由两个解析球和两个地面三角形组成。相机位于 (0,1,4),看向原点,垂直视场角 0.8 弧度;图像为 256×256,每像素中心发射一条射线。

橙色和蓝色分别表示两球,绿色表示地面,背景黑色。地面的两个三角形采用同一显示颜色,但仍具有不同编号。左球遮住右球的一部分,说明最终颜色来自沿射线最近的几何。

法线图将三个分量从 [−1,1] 映射到 [0,1],再通过既有 sRGB 编码写出。地面颜色恒定,球面连续变化;这不是材质或光源贡献。两张 PNG 都由实际检查器输出的 PPM 转换,未经手工绘制。

65,536 条射线中,42,952 条命中几何。检查器把图元数组顺序反转,逐射线比较命中状态、编号和参数,全部一致。这证明当前场景的聚合行为不依赖数组顺序,但无法单独证明共用的求交内核正确。

为此另用球心到单位射线的垂直距离求弦长:先求球心投影 t₀,再算垂距平方 b²,最近根为 t₀−√(R²−b²)。该独立几何表达式与二次求根逐条比较,最大绝对误差为 2.00728322852228×10⁻¹³。这个场景相机位于所有球外且向前观察,因此只需比较前根;球内情况另有解析用例。

检查还覆盖切线两侧、零重根、非单位方向、区间截断、三角形重心、共享边和缩放参数。共享边使用精确可表示的简单坐标,只作为回归样本,不是 watertight 证明。

复跑与练习

1
2
make -C examples/computer-graphics check
examples/computer-graphics/build/ray_check examples/computer-graphics/build

累计代码为 ray.hppray_check.cpp,输出 ray-ids.ppmray-normals.ppm。记录在 writing-plans/computer-graphics/evidence/17-cpu.txt。没有随机数、GPU 或性能比较。求交内核面向本例有限、非退化的内存几何;它不是接受任意外部数据的解析器,也不保证溢出尺度下的计算。

练习一:单位球、起点 (0,0,−3),把方向改成 (0,0,2),两个根是多少,最近交点到起点的距离是多少?

答案:根为 1 和 2,最近点仍为 (0,0,−1),距离为 2。参数乘方向长度才得到距离。若在变换之后把方向重新归一化,需要同步换算参数。

练习二:三角形 A=(0,0,0)、B=(1,0,0)、C=(0,1,0),交点 (0.25,0.25,0) 的权重是什么?点 (0.8,0.4,0) 为什么被拒绝?

答案:前者权重为 (0.5,0.25,0.25)。后者 u+v=1.2,使 A 的权重为 −0.2,位于三角形之外。与平面相交只是必要条件。

练习三:最近候选已经是 t=2,后续物体的包围范围从 t=1 开始,能否不查询后续物体?

答案:不能。范围仍可能包含比 2 更近的表面。只有能证明其全部候选都不优于当前结果时才能剪枝;下一篇用包围盒与 BVH 缩小需要精确查询的候选集合。

系列导航与资料

前篇:16:多个模型怎样组成场景系列入口。下一篇:18:怎样减少求交次数

PBRT 第四版 §5.2.2 核对针孔相机射线;§6.2.2 核对球体二次式和根筛选;§6.5.3 对照更稳健的三角形算法;§6.8 核对舍入误差、自交与偏移限制。以上章节于 2026-09-20 实际读取,资料摘录保存在 research-17-19.md