光线追踪(基本原理)
上一章讨论阴影图时,我们已经接触到一种“换视角做可见性判断”的思想:从光源看场景,记录最近深度,再判断相机看到的点是否也能被光源看到。
光线追踪 (Ray Tracing) 把这个思想推进得更直接:不再先把三角形投到屏幕上,再问它覆盖了哪些像素;而是从相机的每个像素出发,向场景中发射一条光线,寻找这条光线首先碰到的表面。这个最近交点决定该像素看见什么,也可以继续生成阴影光线、反射光线、折射光线,从而表达更丰富的可见性和光照关系。
这章先讨论光线追踪的基本原理。后续章节才会进一步解决两个现实问题:如何让大量求交变快,以及如何把光线追踪扩展到更完整的全局光照。
从光栅化思维到光线追踪思维
Section titled “从光栅化思维到光线追踪思维”光栅化和光线追踪都在回答“屏幕上的像素应该是什么颜色”,但它们的出发点不同。
光栅化 (Rasterization) 的基本流程是:几何经过模型、视图、投影变换后落到屏幕空间,然后硬件判断每个三角形覆盖哪些像素或采样点。它非常适合 GPU 并行处理,因为三角形覆盖、插值、深度测试和片元着色都能被组织成高吞吐管线。
光线追踪 则反过来:每个像素先产生一条从相机出发的光线,再让这条光线和场景几何求交。谁是最近交点,谁就挡在相机前面。这个过程天然适合描述反射、折射、阴影和间接光,因为它可以沿着新的方向继续追踪。
光线的数学表示
Section titled “光线的数学表示”一条三维光线通常写成参数方程:
其中:
- 是光线原点 (Origin),例如相机位置或某个表面交点。
- 是单位方向向量 (Direction)。
- 是沿光线前进的距离参数。
- 是距离原点 处的空间点。
为了表示“从原点向前发射”的半直线,通常只接受:
如果求交结果得到 ,说明交点在光线原点背后;如果有多个正的 ,最近可见表面通常是最小的正 。
相机如何生成主光线
Section titled “相机如何生成主光线”光线追踪的第一步是为每个像素生成一条主光线 (Primary Ray)。可以把相机前方放一张虚拟成像平面,每个像素对应平面上的一个采样点。
- 确定相机位置 。
- 根据像素坐标找到成像平面上的采样点 。
- 用采样点减去相机位置得到方向:
于是该像素的主光线就是:
在透视相机中,不同像素的主光线方向不同,它们从相机位置向外散开;在正交相机中,所有主光线方向相同,但不同像素拥有不同的光线原点。
光线与几何求交
Section titled “光线与几何求交”有了光线之后,核心问题变成:这条光线会先碰到哪个物体?
对于每个几何体,求交算法会尝试解出满足“点同时在光线上、也在几何表面上”的 。以球为例,球心为 ,半径为 ,球面方程是:
把光线方程 代入球面方程:
展开后会得到关于 的二次方程。判别式决定是否相交:
- 判别式小于 :没有实数解,光线没有碰到球。
- 判别式等于 :光线与球相切。
- 判别式大于 :光线穿过球,有两个交点。
实际渲染时,我们会保留最小的正 。这就是相机沿着该方向看到的第一个表面。
真实场景通常由三角网格组成,因此光线与三角形求交非常重要。概念上可以分成两步:
- 先求光线与三角形所在平面的交点。
- 再判断交点是否落在三角形内部。
如果交点可以写成三角形三个顶点的重心组合:
并且满足:
那么它就在三角形内部。许多实际实现会使用 Möller-Trumbore 算法,它能直接计算光线与三角形的 和重心坐标,避免显式构造平面方程。
最近交点决定可见性
Section titled “最近交点决定可见性”深度缓冲在光栅化中保存“当前像素最近的片元深度”。光线追踪中也有类似思想,只是它在一条光线上比较所有候选交点的 。
假设一条光线与多个物体相交:
那么最终可见的是 的交点,因为它离相机最近,遮挡了后面的物体。这个最近交点通常会携带:
- 交点位置 。
- 表面法线 。
- 材质参数。
- UV 坐标或其它插值属性。
- 命中的几何体或三角形编号。
这些信息会交给着色过程,用于计算该像素的颜色。
上面的组件展示了最基础的追踪过程:移动像素位置会改变主光线方向;第一段黄色光线命中最近表面后,可以继续发出阴影光线或反射光线。红色虚线表示到光源的路径被挡住,绿色虚线表示交点能直接看到光源。
阴影光线:直接可见性测试
Section titled “阴影光线:直接可见性测试”在局部光照模型中,我们曾经计算:
但这个公式默认光源能照到当前点。光线追踪可以用一条阴影光线 (Shadow Ray) 直接检查这件事。
对于表面交点 和光源位置 ,阴影光线方向为:
然后从稍微偏离表面的点 发射光线:
如果在到达光源之前命中了其它物体,说明光源被遮挡,该点处在阴影中;如果没有命中,说明该点可以接受这个光源的直接照明。
这和 Shadow Mapping 的目标一样,都是做“点到光源之间是否可见”的判断。区别在于:Shadow Mapping 使用光源视角的深度纹理近似可见性;光线追踪直接沿着点到光源的方向求交。
反射光线与递归追踪
Section titled “反射光线与递归追踪”镜面反射可以通过反射方向继续发射光线。给定入射方向 和单位法线 ,反射方向为:
如果材质是镜面或接近镜面,渲染器可以从交点沿 再追踪一条光线,得到被反射看到的颜色。于是颜色不再只来自当前表面的局部光照,也可能来自下一次命中的物体。
概念上,一个简单递归光线追踪器可以写成:
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}递归深度不能无限增加。每多一次反射,就需要更多求交和着色计算;同时,光线贡献通常会随反射次数逐渐减弱。因此实际渲染器会设置最大深度,或在路径贡献足够低时提前停止。
折射、透明与更多光线路径
Section titled “折射、透明与更多光线路径”除了反射,透明材质还会产生折射光线 (Refraction Ray)。折射方向由入射方向、表面法线和两侧介质的折射率决定,遵循 Snell 定律:
这让光线追踪很自然地表达玻璃、水、透镜等现象。一个交点可以同时产生反射光线和折射光线,它们的贡献再按 Fresnel 项混合。
不过,本章只需要先建立路径分支的直觉:光线追踪不是只打一条线就结束,而是会在表面处根据材质生成新的查询。每一条新光线都在问同一个问题:
沿着这个方向,场景中第一个被看到的表面是谁?
光线追踪为什么昂贵
Section titled “光线追踪为什么昂贵”光线追踪的表达力很强,但直接实现会非常慢。假设屏幕有 个像素,场景有 个三角形。如果每条主光线都和每个三角形求交一次,主光线阶段的复杂度就是:
如果再加上阴影光线、反射光线、折射光线和间接光采样,光线数量会迅速增加。真正困难的不是“光线方程怎么写”,而是“如何避免每条光线都检查整个场景”。
这就是下一章要讨论的主题:加速结构 (Acceleration Structures)。它们会把场景空间组织起来,让光线快速跳过不可能命中的区域。
本章从光栅化切换到光线追踪,重点建立“像素发射光线,光线查询最近交点”的基本模型。
- 光线可以写成 ,通常只接受 且最近的交点。
- 主光线从相机像素出发,决定该像素首先看见哪个表面。
- 光线与球、平面、三角形等几何体求交,本质是在求满足几何方程的参数 。
- 最近交点携带位置、法线、材质和 UV 等信息,交给着色过程计算颜色。
- 阴影光线检查交点是否能看到光源;反射光线和折射光线让可见性查询继续沿新方向传播。
- 朴素光线追踪需要大量求交,因此必须依赖加速结构避免每条光线遍历整个场景。
下一章会围绕这个性能瓶颈展开:如何用包围盒、空间划分和 BVH 等结构,让光线更快找到可能命中的几何。