想象一下这样的场景:深夜,你举着手电筒走进一间完全漆黑的屋子,想看清屋里的家具。你站在门口,把手电筒照向左边——光束笔直地射出,碰到沙发就停住了,沙发那一点被照亮,你记下“米白色”。你又转向右边——光束碰到书架,记下“深棕色”。你每照一个方向,光线就沿直线飞出去,遇到第一个挡路的物体就停下,那个物体的颜色就成了你记录的内容。如果你把屋子分成很多个格子,依次照射每个格子的方向,把看到的颜色填进格子里,你就在“画”一张照片——这张照片就是光线追踪渲染出的画面。 这个故事里藏着一个关键转折:你手里的电筒不是真正的光源,而是“视线”。你没有傻乎乎地从灯泡出发去追光,因为灯泡的光向四面八方散射,绝大多数根本不会飞进你的眼睛。你选择逆向而行——从眼睛出发,向场景发射射线,只计算那些恰好会“走进眼睛”的光路。这就是光线追踪(Ray Tracing)的核心:它模拟的不是光的发射,而是光的“目击”。从射线与物体的求交,到反射折射的计算,再到蒙特卡洛路径追踪,这篇内容将带你完整理解,为什么光线追踪是真实感渲染的金标准。

岗位关联

对 AI 从业者而言,理解光线追踪就是理解电脑如何“看懂”三维世界——它是生成式 AI 图像、自动驾驶仿真、元宇宙渲染的底层视觉基石,掌握它能让你在视觉计算领域畅通无阻。

目录
  1. 可视化演示
  2. 光追基本思想
  3. 射线求交原理
  4. 材质光照计算
  5. 全局光照阴影
  6. 光栅化对比
  7. 性能复杂度
  8. 加速结构优化
  9. 蒙特卡洛追踪
  10. AI 应用场景
  11. Python 演示
  12. 常见误区
  13. 面试题

可视化演示

展示光线追踪中射线与场景求交、反射折射计算、以及蒙特卡洛路径追踪的采样过程,支持模式切换查看不同渲染原理。

本节配有交互动效(进入视口后自动加载,约 3KB)。

'+_captions.intersect+'

光追基本思想

小明举着手电筒站在黑屋子门口。他没有把灯泡拆下来举着——那样光会射向所有方向,他却只能看见极小的一束。聪明的做法是握紧手电筒,让光束只朝自己“想看的方向”打出去,每照一个方向,光束碰到什么,他就记录什么。光线追踪正是这个思路的极致版:摄像机就是小明的眼睛,虚拟屏幕上的每个像素格子,都是手电筒将要照向的一个方向。

在经典 Whitted 模型中,系统为每个像素发射一根主射线(primary ray),从摄像机出发穿过像素中心射入场景。射线与场景中所有物体做求交测试,找到最近的交点。这个点就是“光束第一下碰到的东西”,它决定了该像素的主颜色。得到交点后,还要根据材质继续追问:这个物体会反光吗?它透明吗?如果反光,我该沿着反射方向再发一根射线去“看”它映出的东西;如果透明,就沿折射方向再发一根射线。每一层追问得到的光线颜色按一定权重累加回当前像素。

小明就这样站在黑屋子门口,一根光束、一个格子,把看见的颜色一笔一笔填进小本子。每根光束不仅能告诉他“这个方向有什么”,还会沿着反射和折射的方向继续追问下去——本子上的格子越填越满,屋子的模样就越逼真。

I(p)=Ilocal(p)+kr⋅Ireflect(p)+kt⋅Irefract(p)\displaystyle I(p) = I_{local}(p) + k_r \cdot I_{reflect}(p) + k_t \cdot I_{refract}(p)
这个公式是说:一个像素的最终颜色 = 物体本身的局部光照颜色 + 反射方向追踪到的颜色 + 折射方向追踪到的颜色,反射率和透射率分别控制两者占比。
  1. 像素最终颜色 $I(p)$点 $p$ 被光线击中后,最终写入像素的颜色值,是三个分量相加的总和。
    I(p)=Ilocal(p)+kr⋅Ireflect(p)+kt⋅Irefract(p)I(p) = I_{local}(p) + k_r \cdot I_{reflect}(p) + k_t \cdot I_{refract}(p)
  2. 局部光照项 $I_{local}(p)$光源直接照射产生的漫反射与高光颜色,不依赖其他物体,是本点“自身的颜色”。
    Ilocal(p)I_{local}(p)
  3. 反射项 $k_r \cdot I_{reflect}(p)$反射率 $k_r$ 乘以沿反射方向继续追踪得到的颜色。镜子 $k_r \approx 1$,哑光物体 $k_r$ 接近 0。
    kr⋅Ireflect(p)k_r \cdot I_{reflect}(p)
  4. 折射项 $k_t \cdot I_{refract}(p)$透射率 $k_t$ 乘以沿折射方向继续追踪的颜色,对应玻璃、水面等透明材质。
    kt⋅Irefract(p)k_t \cdot I_{refract}(p)

射线求交原理

光束直线飞出去,小明怎么知道它飞了多远才会碰到家具?他不能一步步走过去摸,数学告诉他:直接解方程。设想一条射线像一把标尺,从起点出发,沿方向 $D$ 每前进一个单位就对应一个刻度 $t$。射线上的每个点都写成 “$起点 + 前进距离 × 方向$“。求交,就是问:”这把标尺走到哪个刻度时,恰好落在某个物体表面上?”

对球体来说,表面是所有到球心距离等于半径的点。把射线参数方程代入球面方程,展开后得到一个关于 $t$ 的一元二次方程。方程有根,说明射线与球有交点;两个根对应“穿入点”和“穿出点”,取最小的正根就是最近的交点。判别式 $\Delta$ 则直接告诉我们根的情况:大于 0 有两个交点,等于 0 相切,小于 0 无交。

对于更复杂的模型,场景几乎全由三角形面片拼成。这时常用 Möller-Trumbore 算法,把射线代入三角形重心坐标方程,一次解出交点距离和重心坐标 $(u, v)$,直接判断交点是否落在三角形内部。这就像小明看到家具的轮廓不是“一整块球”,而是由无数小平面拼成的,他每束光要问的是“最近的那个小平面在不在前方”。

Δ=b2−4ac,t=−b±Δ2a\displaystyle \Delta = b^2 - 4ac, \quad t = \frac{-b \pm \sqrt{\Delta}}{2a}
判别式是“光束与球是否相遇”的判决书:$\Delta \ge 0$ 才说明射线真的穿过了球的表面。
  1. 代入球面方程将射线 $O + tD$ 代入球面方程 $|P - C|^2 = R^2$,其中 $O$ 是射线起点,$C$ 是球心,$R$ 是半径。
    ∣O+tD−C∣2=R2|O + tD - C|^2 = R^2
  2. 展开为一元二次方程按 $t$ 整理成标准形式 $at^2 + bt + c = 0$,系数由起点、方向、球心共同决定。
    (D⋅D)t2+2D⋅(O−C)t+(O−C)⋅(O−C)−R2=0(D \cdot D) t^2 + 2D \cdot (O - C) t + (O - C)\cdot(O - C) - R^2 = 0
  3. 判别式判断相交$\Delta = b^2 - 4ac$ 判断解的个数。$\Delta < 0$ 表示光束从球旁边掠过,根本没有相交。
    Δ=b2−4ac\Delta = b^2 - 4ac
  4. 求最近交点距离两个解中取较大的非负根,表示光束第一次碰到球面时的前进距离 $t$。
    t=−b−Δ2at = \frac{-b - \sqrt{\Delta}}{2a}

材质光照计算

同样一束光照过去,小明发现每件家具的“脾气”完全不一样:镜子把光线整整齐齐地弹回去,你看到的是一张左右颠倒的脸;玻璃让光线穿过去,却在交界处拐了个弯;磨砂墙面则把光向四面八方均匀散射。家具表面吃什么、吐什么,决定了光线碰撞后的命运。

镜面反射遵循最简单的规则——入射角等于反射角。用向量表达更加优雅:已知入射方向 $L$ 和表面法线 $N$,反射方向 $R$ 是 $L$ 关于 $N$ 的镜像。推导只用了“投影”这一个动作:先把 $L$ 投影到 $N$ 上得到“垂直分量”,再把 $L$ 的切向分量反向,合起来就是 $R$。

透明材质则要处理折射。光线从空气进入水面时,速度变慢、方向弯折,弯折的规律由斯涅尔定律描述:入射介质折射率 $n_1$ 与入射角正弦的乘积,等于出射介质折射率 $n_2$ 与折射角正弦的乘积。代入两个介质的折射率,就能求出折射角,确定光线在水面下的新方向。

此外还有菲涅尔效应——反射与折射的“分红”比例会随入射角变化。视线越贴近表面(掠射角越大),反射的份额越高,所以夕阳下湖面看起来像一整面镜子。材质的这些“脾气”被数值化为法线、折射率和菲涅尔系数,光束撞上之后是该弹还是该穿,都由这些参数说了算,画面因此有了不同材质的质感。

R=2(N⋅L)N−L\displaystyle \mathbf{R} = 2(\mathbf{N} \cdot \mathbf{L})\mathbf{N} - \mathbf{L}
斯涅尔定律:光从稀疏介质进入稠密介质时向法线靠拢,反之则远离法线;入射角太大时还会发生全反射。
  1. 入射侧$n_1$ 是入射介质的折射率(空气约 1.0),$\theta_1$ 是入射光线与法线的夹角。
    n1sin⁡θ1n_1\sin\theta_1
  2. 出射侧$n_2$ 是出射介质的折射率(水约 1.33),$\theta_2$ 是折射光线与法线的夹角。
    n2sin⁡θ2n_2\sin\theta_2
  3. 乘积守恒两侧乘积相等,说明光从疏介质到密介质时 $\theta_2$ 变小,光线向法线弯折。
    n1sin⁡θ1=n2sin⁡θ2n_1\sin\theta_1 = n_2\sin\theta_2
  4. 全反射条件当 $n_1 > n_2$ 且 $\theta_1$ 超过临界角 $\arcsin(n_2 / n_1)$ 时,方程无解,光线全部反射。
    θc=arcsin⁡n2n1\theta_c = \arcsin\frac{n_2}{n_1}

全局光照阴影

小明渐渐发现一个奇妙的现象:手电筒照到红沙发后,沙发旁边的白墙居然被染上了淡淡的粉色。光束的故事没有在第一次碰撞就结束——红光从沙发表面散射出来,又飞向白墙,把颜色“传染”给了它。如果只记录第一次碰撞,就会漏掉这种颜色互染,画面会显得生硬而“假”。

全局光照(Global Illumination)要解决的就是光线多次弹射的能量传递。在 Whitted 模型里,反射和折射射线间接地模拟了一部分全局效果。而更基础的一步是阴影判断:从着色点 $p$ 向光源方向发射一根检测射线(shadow ray),如果这根射线在半路上撞到了别的物体,说明光源被挡住了,$p$ 就处在阴影中;否则它被光源直接照亮。

不过,每追一层反射/折射射线,光线的数量就会翻倍(甚至更多)。为了避免计算量爆炸,渲染器会设置递归深度上限,通常 5~8 层:超过上限后不再继续追踪,直接用当前颜色截断。小明就这样开始记录“二手信息”——不光记第一次撞上的家具本色,还记它反弹出去又照亮了什么、挡出什么阴影。多层追下去,屋子里墙角的红晕、桌上的倒影、椅下的阴影全都活了过来,这才是“全局”二字的含义。

S(p)={0若阴影射线被遮挡1否则\displaystyle S(p) = \begin{cases} 0 & \text{若阴影射线被遮挡} \\ 1 & \text{否则} \end{cases}
阴影函数是一个“路灯检查员”:从 $p$ 点朝路灯方向望去,只要有人挡在中间,就回答“阴影中”。
  1. 从着色点出发将 $p$ 设为射线起点,方向指向光源,发射阴影射线。
    S(p)S(p)
  2. 遮挡判定若射线在到达光源之前碰到任何物体,返回 0,表示 $p$ 被遮挡、处于阴影。
    0,若被遮挡0, \text{若被遮挡}
  3. 可见判定若射线一路畅通直达光源,返回 1,表示 $p$ 被直接照亮。
    1,否则1, \text{否则}

光栅化对比

如果说光线追踪是“逐点审讯”——每个像素都派一根光束去问“你最前面看到谁”——那么光栅化就是“贴花纸”:先把沙发的轮廓投影到屏幕上,形成一个多边形区域,然后往区域里大笔涂抹颜色,至于谁挡谁,再拿一张“深度卡片”(z-buffer)去记录,靠比较深度的早晚决定哪个三角形胜出。前者每束光都亲自去问,后者先把形状铺好再涂色。

光栅化要做的核心操作用一个公式就能讲清:把一个 3D 点从相机空间投影到屏幕,只需要把它的 $x$、$y$ 坐标各自除以深度 $z$,再乘上焦距 $f$。除以 $z$ 这一步,让远处的物体投射后变小,产生“近大远小”的透视效果。这个操作在 GPU 上被高度优化,可以同时在千万个三角形上并行执行,所以实时游戏几乎都靠它。

但光栅化对反射、折射、软阴影、间接光这些“全局效果”非常吃力——它本质上是在快速“假装”,用各种 hack(如屏幕空间反射、烘焙光照贴图)去骗过眼睛。光线追踪则从原理上就正确处理这些物理现象,代价是慢。所以业界没有谁取代谁:实时游戏用光栅化撑起 60 帧,电影特效用光线追踪换取一帧渲染几小时的真实感。花纸贴得快但容易露馅,光束追问得慢却诚实地照亮每个角落。

xscreen=fzview⋅xview,yscreen=fzview⋅yview\displaystyle x_{screen} = \frac{f}{z_{view}} \cdot x_{view}, \quad y_{screen} = \frac{f}{z_{view}} \cdot y_{view}
透视投影公式:把相机空间坐标除以深度,再乘焦距,就得到屏幕上该点的位置,产生近大远小的效果。
  1. 取相机空间坐标$(x_{view}, y_{view}, z_{view})$ 是 3D 三角形顶点在相机坐标系中的位置,$z_{view}$ 表示顶点到相机的深度。
    (xview,yview,zview)(x_{view}, y_{view}, z_{view})
  2. 除以深度产生透视用 $f / z_{view}$ 作为缩放因子,深度越大的点缩放越小。
    fzview\frac{f}{z_{view}}
  3. 屏幕坐标输出缩放后的 $x$、$y$ 就是屏幕上的像素位置,远处物体自然收缩。
    xscreen=fzviewxviewx_{screen} = \frac{f}{z_{view}} x_{view}

性能复杂度

假如屋子里摆了 100 件家具,小明的本子上有 100 万个格子,他每照一个格子都要把 100 件家具问一遍:“你的位置在我这条光束前方吗?”——总共要问 1 亿次。如果再把家具换成精度更高的 10 万块小碎片,这个数字会变成 1000 亿。光追的“贵”就贵在这份老实:它不放过任何一次可能的求交。

更精确地说,朴素光线追踪的复杂度是像素总数乘以物体总数。1080p 画面约 200 万个像素,每个像素发射主射线后,还要与场景中每个三角形做求交测试,这就是 $O(W \times H \times M)$ 的来源。求交本身也不便宜,涉及平方根、除法和多次点积运算。好消息是,每个像素之间的计算完全独立——这属于“尴尬地适合并行”(embarrassingly parallel)的问题,可以喊几万个 GPU 核心同时开工,每人各照各的格子。

小明的朋友足够多,每人负责一个格子,大家同时照完再汇总——这就是显卡上成千上万个流处理器干的事。光追从“慢得离谱”到“电影院里放得起”,一半功劳归于并行硬件,另一半归于聪明的加速结构。

O(ray tracing)=O(W×H×M)\displaystyle O(\text{ray tracing}) = O(W \times H \times M)
加速后复杂度:引入层次结构后,每根射线只需经过约 $\log M$ 次判断就能剔除大部分物体,代价从线性降到对数。
  1. 对数项 $\log M$加速结构让每根射线的求交次数从 $M$ 次降到 $\log M$ 次,靠的是树的层次剔除。
    log⁡M\log M
  2. 像素项不变分辨率 $W \times H$ 仍是线性瓶颈,所以提高分辨率依然昂贵。
    W×HW \times H

加速结构优化

小明逐渐学聪明了。他先把屋子划分成几个大区域:左边是“沙发区”,用一个看不见的大罩子整个罩住;中间是“茶几区”;右边是“书柜区”,每个区都有自己的大罩子。现在光束射出后,他先跟这些大罩子打招呼,如果光束根本没碰到某个大罩子,那罩子里面的家具就全都省去细看。大部分无关区域在一瞬间被整块跳过。

这正是包围盒层次结构(Bounding Volume Hierarchy, BVH)的思想:先把场景物体用简单的长方体包围盒包起来,递归地建立一棵二叉树——根节点罩住整个场景,子节点罩住子集,叶子节点是具体的三角形。射线程式从根开始检测:与当前包围盒相交就下探孩子节点,不相交则整棵子树直接抛弃。每下探一层都能快速排除约一半的候选物体,于是单根射线的求交成本从 $M$ 降到了 $\log M$。另一种思路是空间网格,把场景均匀切成小格子,射线沿着路径一格一格推进,只检查穿过的格子里的物体。

就这样,小明不再傻乎乎地摸遍每一件家具——先看大罩子,再下探小抽屉,光束只会停留在线路真正经过的区域周围。BVH、网格、八叉树……这些加速结构把光追从“理论上很美”变成了“工程上可用”。

T=Nbox⋅Cbox+Ntri⋅Ctri\displaystyle T = N_{box} \cdot C_{box} + N_{tri} \cdot C_{tri}
遍历成本模型:射线总共要为“经过的包围盒”和“真正检测的三角形”分别付出代价,好的结构让这两个数量都尽可能小。
  1. 包围盒检测次数 $N_{box}$一根射线在遍历 BVH 过程中触碰到的包围盒总数。
    NboxN_{box}
  2. 包围盒求交成本 $C_{box}$每次包围盒求交的计算开销,通常只是几次比较运算,非常便宜。
    CboxC_{box}
  3. 三角形检测次数 $N_{tri}$真正需要做精确求交的三角形数量,理想情况下只有射线穿过的极少几个。
    NtriN_{tri}
  4. 三角形求交成本 $C_{tri}$精确求交涉及平方根等昂贵运算,所以要尽可能通过包围盒提前剔除。
    CtriC_{tri}

蒙特卡洛追踪

小明决定让手电筒升级成“故事收集器”。他让光束撞到沙发后,随机地弹向一个方向继续飞,撞到墙又再随机弹一次……光束走出了一条弯弯曲曲的路径,沿路记下每次碰撞的颜色贡献。然后,他换一个随机方向重新发射,让故事再演一遍。这些故事各有不同、都带点随机性,但把它们平均起来,画面就逐渐趋近真实。这就是蒙特卡洛路径追踪的直觉:用大量随机采样去逼近复杂的积分结果。

1986 年 Kajiya 提出渲染方程,把“从某点向某个方向看去的亮度”写成自发光加上半球所有方向入射光的积分。这个积分几乎无法解析求解,蒙特卡洛方法提供了唯一的出路:随机采样一组入射方向,用样本均值去估计积分值。路径追踪从交点随机选一个出射方向,递归地构造一条光路,直到光线被吸收或达到深度上限。因为是随机估计,画面会呈现噪声,但样本越多噪声越小,最终收敛到真实解——这就是“无偏”的保证。

多讲几个“光束旅程故事”,平均下来,每个角落都被诚实记录,画面就真了。蒙特卡洛路径追踪也因此成为离线渲染器的金标准——电影里的金属球、玻璃杯、皮肤上的柔和光影,都是几亿条随机射线“平均”出来的结果。

Lo(p,ωo)=Le(p,ωo)+∫Ωfr(p,ωi,ωo) Li(p,ωi) (ωi⋅n) dωi\displaystyle L_o(p, \omega_o) = L_e(p, \omega_o) + \int_{\Omega} f_r(p, \omega_i, \omega_o) \, L_i(p, \omega_i) \, (\omega_i \cdot \mathbf{n}) \, d\omega_i
蒙特卡洛估计:把积分换成随机采样方向的平均值,每个样本除以采样概率 $pdf$ 后累加,样本越多越接近真实值。
  1. 随机方向采样按某种概率分布 $pdf$ 随机抽取 $N$ 个方向 $\omega_k$ 作为样本。
    ωk∼pdf(ωk)\omega_k \sim pdf(\omega_k)
  2. 样本加权和每个方向上的反射贡献除以该方向的采样概率,得到无偏的样本估计。
    fr Li (ωk⋅n)pdf(ωk)\frac{f_r \, L_i \, (\omega_k \cdot \mathbf{n})}{pdf(\omega_k)}
  3. 平均值逼近积分$N$ 个样本的平均值随 $N$ 增大收敛到真实积分,这就是蒙特卡洛积分。
    ⟨Lo⟩≈1N∑k=1N⋯\langle L_o \rangle \approx \frac{1}{N} \sum_{k=1}^{N} \cdots

AI 应用场景

影视特效与动画电影的离线渲染

阶段:渲染管线(VFX Pipeline)中的最终画面生成

解决的问题:传统光栅化无法在合理成本内模拟金属反射、玻璃折射、次表面散射等复杂物理光学效果。光线追踪/路径追踪从物理原理出发,用大规模随机射线逼近渲染方程,产出照片级真实画面,代价是一帧可能要渲染数小时。

具体案例:皮克斯《玩具总动员4》使用自主开发的 RenderMan 渲染器,包含完整的光线追踪与辐射度算法;工业光魔在《星球大战》系列中用基于物理的路径追踪合成飞船金属反射和太空光照。团队会为每个镜头配置数万核的渲染农场,连续渲染数周。

实时游戏与交互式 3D 的光线追踪

阶段:实时渲染引擎(Game Engine)中的光照与反射计算

解决的问题:传统实时游戏靠光栅化加各种“欺骗性”技巧(烘焙光照贴图、屏幕空间反射),遇到动态场景或大面积镜面就会穿帮。NVIDIA RTX 系列显卡内置光追专用硬件(RT Core),把光线求交从软件计算变成硬件加速指令,让游戏在实时帧率下也能计算真实的反射、阴影和全局光照。

具体案例:《赛博朋克 2077》开启光追后,湿漉街道上的霓虹灯反射、车窗上的倒影都来自实时射线求交;Epic 的虚幻引擎 5 提供 Lumen 全局光照方案,将光追思想融入实时渲染,让动态光影无需预计算。

自动驾驶传感器仿真

阶段:合成数据生成(Synthetic Data)与传感器模型验证

解决的问题:自动驾驶模型训练需要海量带标注的道路数据,但真实采数据成本高、极端场景难以覆盖。用光线追踪模拟车载摄像头的成像过程,可以精确仿真阳光、路灯、玻璃反光、雨水折射等真实光学现象,生成带精确标签的“虚拟真实”图像。

具体案例:Waymo 与 NVIDIA DRIVE Sim 使用路径追踪渲染高保真街道场景,测试模型对逆光、夜间、反光路面等极端情况的感知能力;仿真器中的虚拟传感器与真实传感器共享相同光学物理模型,让模型无缝迁移。

Python 演示

python62 行
import math def write_ppm(pixels, width, height):    with open('output.ppm', 'wb') as f:        f.write(b'P6\n%d %d\n255\n' % (width, height))        for row in pixels:            for r, g, b in row:                f.write(bytes((                    max(0, min(255, int(r))),                    max(0, min(255, int(g))),                    max(0, min(255, int(b)))))) def normalize(v):    length = math.sqrt(v[0] * v[0] + v[1] * v[1] + v[2] * v[2])    return (v[0] / length, v[1] / length, v[2] / length) def dot(a, b):    return a[0] * b[0] + a[1] * b[1] + a[2] * b[2] def ray_sphere(origin, direction, center, radius):    oc = (origin[0] - center[0], origin[1] - center[1], origin[2] - center[2])    a = dot(direction, direction)    b = 2.0 * dot(oc, direction)    c = dot(oc, oc) - radius * radius    disc = b * b - 4.0 * a * c    if disc < 0:        return None    sqrt_disc = math.sqrt(disc)    t = (-b - sqrt_disc) / (2.0 * a)    if t > 0:        return t    t = (-b + sqrt_disc) / (2.0 * a)    return t if t > 0 else None def render(width=480, height=320):    origin = (0.0, 0.0, 0.0)    center = (0.0, 0.0, -5.0)    radius = 1.2    light = normalize((0.5, 1.0, -0.8))    pixels = []    for j in range(height):        row = []        for i in range(width):            x = (i - width / 2.0) / width            y = -(j - height / 2.0) / height            d = normalize((x, y, -1.0))            t = ray_sphere(origin, d, center, radius)            if t is None:                row.append((80, 90, 140))                continue            p = (origin[0] + d[0] * t, origin[1] + d[1] * t, origin[2] + d[2] * t)            n = normalize((p[0] - center[0], p[1] - center[1], p[2] - center[2]))            diff = max(0.0, dot(n, light))            half = normalize((light[0] + d[0], light[1] + d[1], light[2] + d[2]))            spec = max(0.0, dot(n, half)) ** 60            row.append((255 * diff, 120 * diff + 220 * spec, 180 * diff))        pixels.append(row)    write_ppm(pixels, width, height) if __name__ == '__main__':    render()    print('output.ppm 已生成')
目的:用纯 Python 实现一个最简单的球体光线追踪器,从每个像素发射射线、与球体求交、计算漫反射与高光,最终写出 PPM 格式图片。
  1. disc = b * b - 4.0 * a * c计算判别式,判断射线是否与球相交,这是球体求交的核心。
  2. t = (-b - sqrt_disc) / (2.0 * a)求最近的交点距离,代表光束第一次碰到球面的位置。
  3. n = normalize((p[0] - center[0], p[1] - center[1], p[2] - center[2]))计算交点处的表面法线,用于光照计算。
  4. diff = max(0.0, dot(n, light))朗伯漫反射模型,法线与光源方向点积越大越亮。
  5. spec = max(0.0, dot(n, half)) ** 60Blinn-Phong 高光模型,半角向量与法线贴近时产生镜面亮点。
  6. row.append((255 * diff, 120 * diff + 220 * spec, 180 * diff))把颜色写入像素行,红色通道由漫反射主导。

常见误区

误区我刚开始学的时候以为光线追踪就是模拟灯泡发出的光线在屋里四处飞来飞去,从光源出发、遇到物体弹开,最后飞到人眼里的过程。

正解恰恰相反。真正的光线追踪几乎总是从摄像机(眼睛)出发、向场景发射射线,这叫逆向光线追踪。因为从光源出发的光线只有极少数会恰好进入人眼,绝大多数计算都白费了。眼睛出发则只计算“看得见”的有效光路,效率高得多。这和本文手电筒的故事一致:你举着手电筒照向感兴趣的方向,而不是满屋子乱照。

为何常见:光追这个名字很容易让人望文生义,以为要“追光”本身;加上物理课本常讲光的传播路径,初学者就顺理成章地以为从光源出发了。

误区看第一张光线追踪示意图时,我看到主射线碰到球就停了,以为每个像素只要算“第一个碰到谁”就完事。

正解那条“碰到就停”的射线只是在定位该像素负责的场景点。真正的渲染还要继续发射阴影射线判断是否被照亮、发射反射射线看镜子映出什么、发射折射射线看玻璃后面的东西。Whitted 模型就是递归追踪这些次级光线,把所有贡献累加回像素。“停”只代表一个阶段结束,不等于整条光路结束。

为何常见:教学示意图为了清晰往往只画主射线求交,省略后续递归,导致初学者把“最近交点”误当成整个渲染过程。

误区听说光线追踪画质更好,是不是以后所有游戏和软件都会换成光追,光栅化直接淘汰?

正解不会。光栅化在速度和硬件友好度上依然有巨大优势,实时游戏需要在几毫秒内渲染千万级三角形,这是纯光追很难做到的。实际工业界是混合管线:光栅化负责基础可见性与几何,光追负责反射、阴影、全局光照等关键特效。NVIDIA 的 RTX 也是光栅化与光追协同工作,而非二选一。就像手电筒和贴花纸各有用途,没有谁“终结”谁。

为何常见:渲染技术迭代快,媒体常宣传“光追革命”,让人误以为新技术要完全取代旧技术,忽略了工程实践中的折中。

误区我以为光打到平面反射回来就是沿着原路返回,把入射方向改成负号就是反射方向。

正解只有垂直入射时反射才是原路返回。一般情况下反射遵循“入射角等于反射角”,反射方向是入射方向关于表面法线的镜像:$R = 2(N \cdot L)N - L$。在镜子里,你的左右是颠倒的——正因为反射方向不是简单的相反数。随手电筒演示中的绿色短线:光线斜着撞上球面后,是沿着与法线对称的方向弹开的。

为何常见:日常经验里常见“光反射回来”的直觉,忽略了角度要素,加上向量取负确实是一部分情况(垂直入射)的答案,很容易以偏概全。

误区我看电影里路径追踪渲染的金属球特别干净完美,以为只要写了路径追踪,输出就自动是高清无噪声的。

正解路径追踪是蒙特卡洛估计,天生带随机噪声,样本较少时画面上会出现明显的颗粒感。它的数学性质是“无偏”——样本无限多时才收敛到真实解,收敛速度与 $1/\sqrt{N}$ 成正比。要提高画质,就得增加采样数(渲染时间线性增长)或用重要性采样、降噪网络去压制方差。电影里那些干净的渲染图,都是每像素成千上万个样本加后期降噪的结果。

为何常见:看到成片都是渲染器工程师调好参数的精修结果,没看到原始渲染时的噪声帧。蒙特卡洛的方差概念对初学者也较抽象。

面试题

Q1. 光线追踪中,主射线通常从什么位置出发?

单选题 · easy

  • A.真实光源
  • B.摄像机眼睛← 正确答案
  • C.场景正中心
  • D.物体表面

答案:B。光线追踪采用逆向追踪,从摄像机出发向场景发射射线。从光源出发是正向光追,绝大多数光线不进眼睛,效率极低。手电筒故事里,小明是从自己的眼睛(视线)出发照向家具,而不是从灯泡出发。

Q2. 射线参数方程 $P(t) = O + tD$ 中,参数 $t$ 代表什么含义?

单选题 · easy

  • A.光线走过的总时间
  • B.沿方向D行走的距离← 正确答案
  • C.像素的颜色强度值
  • D.物体的透明度大小

答案:B。$t$ 是射线从起点 $O$ 出发、沿方向 $D$ 前进的距离参数。$t > 0$ 表示交点在前方,求交算法要找最小的正 $t$ 也就是最近的交点。它和颜色、时间、透明度无关。

Q3. 光栅化渲染管道中,通常用什么结构来解决物体遮挡问题?

单选题 · easy

  • A.深度缓冲← 正确答案
  • B.颜色缓冲
  • C.模板缓冲
  • D.包围盒结构

答案:A。光栅化用深度缓冲(z-buffer)记录每个像素处最近物体的深度值,后绘制的三角形若深度更远则丢弃。颜色缓冲只存颜色、模板缓冲做剪裁特效、包围盒是光追加速结构,都不负责几何遮挡判定。

Q4. 已知入射光线方向 $L$ 和表面法线 $N$,镜面反射方向 $R$ 的正确计算方法是?

单选题 · mid

  • A.R = N - L
  • B.R = 2(N·L)N - L← 正确答案
  • C.R = N + L
  • D.R = L - N

答案:B。反射方向是入射方向关于法线的镜像。先把 $L$ 投影到法线方向得到 $(N·L)N$,加倍后减去原来的 $L$,得到 $R = 2(N·L)N - L$。其余选项都不满足“入射角等于反射角”的几何关系。

Q5. 以下哪些属于加速光线追踪的常见技术?

多选题 · mid

  • A.BVH层次包围盒← 正确答案
  • B.空间网格划分← 正确答案
  • C.颜色深度缓冲
  • D.提前剔除未交物体← 正确答案

答案:ABD。BVH 用包围盒树组织场景,空间网格把场景分格,两者核心都是尽早剔除与射线不相交的物体,把每根射线的求交次数从 $M$ 降到 $\log M$。深度缓冲是光栅化管线的遮挡解决方案,与光追加速无关。

Q6. 朴素光线追踪的渲染复杂度是多少?

单选题 · mid

  • A.O(W+H+M)
  • B.O(W×H×M)← 正确答案
  • C.O(M log M)
  • D.O(W×M)

答案:B。朴素光追对每个像素($W \times H$ 个)都要遍历场景中所有物体($M$ 个)做求交,所以复杂度是 $O(W\times H\times M)$。这一事实决定了光追必须配合并行计算和加速结构才能实用。

Q7. 关于菲涅尔效应,下列描述正确的是?

单选题 · mid

  • A.光速在不同介质中的变化
  • B.掠射角大时反射增强← 正确答案
  • C.颜色随距离有规律衰减
  • D.物体阴影边缘变得更模糊

答案:B。菲涅尔效应描述反射与折射的比例随入射角变化:视线越贴近表面(掠射角大),反射分量越强,所以夕阳下湖面像镜子。它不描述光速变化、颜色衰减或阴影模糊。

Q8. 关于蒙特卡洛路径追踪,下列说法正确的是?

多选题 · mid

  • A.随机采样光线散射方向← 正确答案
  • B.无偏估计逐渐收敛真值← 正确答案
  • C.只计算单个像素颜色
  • D.采样越多画面噪声越少← 正确答案

答案:ABD。路径追踪从交点随机采样出射方向来估计渲染方程的积分,样本均值是无偏估计,随样本增加收敛到真实解且噪声按 $1/\sqrt{N}$ 减小。它当然计算所有像素,不是只算单个像素。

Q9. 光线穿越不同透明介质时发生偏折,遵循下列哪条定律?

单选题 · hard

  • A.牛顿第二运动定律
  • B.斯涅尔折射定律← 正确答案
  • C.安培环路定律
  • D.库仑静电定律

答案:B。折射偏折由斯涅尔定律描述:$n_1\sin\theta_1 = n_2\sin\theta_2$,入射角与折射角正弦分别乘以各自介质折射率后相等。牛顿定律描述力与运动,安培和库仑定律属于电磁学,都不涉及光的折射。

Q10. 与光栅化相比,光线追踪在哪些方面具有明显优势?

多选题 · hard

  • A.天然处理反射和折射← 正确答案
  • B.渲染速度比光栅化快
  • C.阴影边缘更加真实自然← 正确答案
  • D.全局光照效果更加真实← 正确答案

答案:ACD。光追从物理上模拟光线传播,反射、折射、阴影、多次弹射的间接光都自然涌现,所以真实感强。但同等条件下光追比光栅化慢得多——这正是它的主要劣势,因此 B 错误,速度恰恰是光栅化的优势。