Skip to content

光线追踪(基本原理)

上一章讨论阴影图时,我们已经接触到一种“换视角做可见性判断”的思想:从光源看场景,记录最近深度,再判断相机看到的点是否也能被光源看到。

光线追踪 (Ray Tracing) 把这个思想推进得更直接:不再先把三角形投到屏幕上,再问它覆盖了哪些像素;而是从相机的每个像素出发,向场景中发射一条光线,寻找这条光线首先碰到的表面。这个最近交点决定该像素看见什么,也可以继续生成阴影光线、反射光线、折射光线,从而表达更丰富的可见性和光照关系。

这章先讨论光线追踪的基本原理。后续章节才会进一步解决两个现实问题:如何让大量求交变快,以及如何把光线追踪扩展到更完整的全局光照。


光栅化和光线追踪都在回答“屏幕上的像素应该是什么颜色”,但它们的出发点不同。

光栅化与光线追踪的出发点对比

光栅化 (Rasterization) 的基本流程是:几何经过模型、视图、投影变换后落到屏幕空间,然后硬件判断每个三角形覆盖哪些像素或采样点。它非常适合 GPU 并行处理,因为三角形覆盖、插值、深度测试和片元着色都能被组织成高吞吐管线。

光线追踪 则反过来:每个像素先产生一条从相机出发的光线,再让这条光线和场景几何求交。谁是最近交点,谁就挡在相机前面。这个过程天然适合描述反射、折射、阴影和间接光,因为它可以沿着新的方向继续追踪。


一条三维光线通常写成参数方程:

P(t)=O+tDP(t) = O + tD

其中:

  • OO 是光线原点 (Origin),例如相机位置或某个表面交点。
  • DD 是单位方向向量 (Direction)。
  • tt 是沿光线前进的距离参数。
  • P(t)P(t) 是距离原点 tt 处的空间点。

为了表示“从原点向前发射”的半直线,通常只接受:

t>0t > 0

如果求交结果得到 t<0t < 0,说明交点在光线原点背后;如果有多个正的 tt,最近可见表面通常是最小的正 tt


光线追踪的第一步是为每个像素生成一条主光线 (Primary Ray)。可以把相机前方放一张虚拟成像平面,每个像素对应平面上的一个采样点。

  1. 确定相机位置 OcameraO_{camera}
  2. 根据像素坐标找到成像平面上的采样点 PpixelP_{pixel}
  3. 用采样点减去相机位置得到方向:
D=normalize(PpixelOcamera)D = normalize(P_{pixel} - O_{camera})

于是该像素的主光线就是:

R(t)=Ocamera+tDR(t) = O_{camera} + tD

在透视相机中,不同像素的主光线方向不同,它们从相机位置向外散开;在正交相机中,所有主光线方向相同,但不同像素拥有不同的光线原点。


有了光线之后,核心问题变成:这条光线会先碰到哪个物体?

对于每个几何体,求交算法会尝试解出满足“点同时在光线上、也在几何表面上”的 tt。以球为例,球心为 CC,半径为 rr,球面方程是:

PC2=r2\|P - C\|^2 = r^2

把光线方程 P(t)=O+tDP(t)=O+tD 代入球面方程:

O+tDC2=r2\|O + tD - C\|^2 = r^2

展开后会得到关于 tt 的二次方程。判别式决定是否相交:

  • 判别式小于 00:没有实数解,光线没有碰到球。
  • 判别式等于 00:光线与球相切。
  • 判别式大于 00:光线穿过球,有两个交点。

实际渲染时,我们会保留最小的正 tt。这就是相机沿着该方向看到的第一个表面。

真实场景通常由三角网格组成,因此光线与三角形求交非常重要。概念上可以分成两步:

  1. 先求光线与三角形所在平面的交点。
  2. 再判断交点是否落在三角形内部。

如果交点可以写成三角形三个顶点的重心组合:

P=αA+βB+γCP = \alpha A + \beta B + \gamma C

并且满足:

α0,β0,γ0,α+β+γ=1\alpha \ge 0,\quad \beta \ge 0,\quad \gamma \ge 0,\quad \alpha+\beta+\gamma=1

那么它就在三角形内部。许多实际实现会使用 Möller-Trumbore 算法,它能直接计算光线与三角形的 tt 和重心坐标,避免显式构造平面方程。


深度缓冲在光栅化中保存“当前像素最近的片元深度”。光线追踪中也有类似思想,只是它在一条光线上比较所有候选交点的 tt

假设一条光线与多个物体相交:

t1=2.3,t2=5.8,t3=1.7t_1 = 2.3,\quad t_2 = 5.8,\quad t_3 = 1.7

那么最终可见的是 t=1.7t=1.7 的交点,因为它离相机最近,遮挡了后面的物体。这个最近交点通常会携带:

  • 交点位置 PP
  • 表面法线 NN
  • 材质参数。
  • UV 坐标或其它插值属性。
  • 命中的几何体或三角形编号。

这些信息会交给着色过程,用于计算该像素的颜色。

黄色线是从相机像素发出的主光线。命中物体后,蓝色短线表示表面法线;开启阴影光线可以看到交点到光源之间是否被遮挡;增加反射次数会继续沿反射方向追踪下一段光线。

上面的组件展示了最基础的追踪过程:移动像素位置会改变主光线方向;第一段黄色光线命中最近表面后,可以继续发出阴影光线或反射光线。红色虚线表示到光源的路径被挡住,绿色虚线表示交点能直接看到光源。


在局部光照模型中,我们曾经计算:

L=Lambient+Ldiffuse+LspecularL = L_{ambient} + L_{diffuse} + L_{specular}

但这个公式默认光源能照到当前点。光线追踪可以用一条阴影光线 (Shadow Ray) 直接检查这件事。

对于表面交点 PP 和光源位置 LpL_p,阴影光线方向为:

Ds=normalize(LpP)D_s = normalize(L_p - P)

然后从稍微偏离表面的点 P+ϵNP + \epsilon N 发射光线:

Rs(t)=P+ϵN+tDsR_s(t) = P + \epsilon N + tD_s

如果在到达光源之前命中了其它物体,说明光源被遮挡,该点处在阴影中;如果没有命中,说明该点可以接受这个光源的直接照明。

这和 Shadow Mapping 的目标一样,都是做“点到光源之间是否可见”的判断。区别在于:Shadow Mapping 使用光源视角的深度纹理近似可见性;光线追踪直接沿着点到光源的方向求交。


镜面反射可以通过反射方向继续发射光线。给定入射方向 DD 和单位法线 NN,反射方向为:

R=D2(DN)NR = D - 2(D \cdot N)N

如果材质是镜面或接近镜面,渲染器可以从交点沿 RR 再追踪一条光线,得到被反射看到的颜色。于是颜色不再只来自当前表面的局部光照,也可能来自下一次命中的物体。

概念上,一个简单递归光线追踪器可以写成:

function trace(ray, depth) {
hit = closestIntersection(ray)
if (!hit) return background
color = localLighting(hit)
if (depth > 0 && hit.material.reflective) {
reflectedRay = makeReflectionRay(hit)
color += hit.material.reflectance * trace(reflectedRay, depth - 1)
}
return color
}

递归深度不能无限增加。每多一次反射,就需要更多求交和着色计算;同时,光线贡献通常会随反射次数逐渐减弱。因此实际渲染器会设置最大深度,或在路径贡献足够低时提前停止。


除了反射,透明材质还会产生折射光线 (Refraction Ray)。折射方向由入射方向、表面法线和两侧介质的折射率决定,遵循 Snell 定律:

η1sinθ1=η2sinθ2\eta_1 \sin\theta_1 = \eta_2 \sin\theta_2

这让光线追踪很自然地表达玻璃、水、透镜等现象。一个交点可以同时产生反射光线和折射光线,它们的贡献再按 Fresnel 项混合。

不过,本章只需要先建立路径分支的直觉:光线追踪不是只打一条线就结束,而是会在表面处根据材质生成新的查询。每一条新光线都在问同一个问题:

沿着这个方向,场景中第一个被看到的表面是谁?


光线追踪的表达力很强,但直接实现会非常慢。假设屏幕有 W×HW \times H 个像素,场景有 NN 个三角形。如果每条主光线都和每个三角形求交一次,主光线阶段的复杂度就是:

O(WHN)O(W H N)

如果再加上阴影光线、反射光线、折射光线和间接光采样,光线数量会迅速增加。真正困难的不是“光线方程怎么写”,而是“如何避免每条光线都检查整个场景”。

这就是下一章要讨论的主题:加速结构 (Acceleration Structures)。它们会把场景空间组织起来,让光线快速跳过不可能命中的区域。


本章从光栅化切换到光线追踪,重点建立“像素发射光线,光线查询最近交点”的基本模型。

  1. 光线可以写成 P(t)=O+tDP(t)=O+tD,通常只接受 t>0t>0 且最近的交点。
  2. 主光线从相机像素出发,决定该像素首先看见哪个表面。
  3. 光线与球、平面、三角形等几何体求交,本质是在求满足几何方程的参数 tt
  4. 最近交点携带位置、法线、材质和 UV 等信息,交给着色过程计算颜色。
  5. 阴影光线检查交点是否能看到光源;反射光线和折射光线让可见性查询继续沿新方向传播。
  6. 朴素光线追踪需要大量求交,因此必须依赖加速结构避免每条光线遍历整个场景。

下一章会围绕这个性能瓶颈展开:如何用包围盒、空间划分和 BVH 等结构,让光线更快找到可能命中的几何。