VCI - 16: 全局光照I - Whitted光线追踪 (Global Illumination I - Ray Tracing)

课程: 北京大学视觉计算 (Visual Computing) 2025秋季 章节: 第16章 全局光照I - Whitted光线追踪 内容: 光线投射、递归光线追踪、光线-表面相交、加速结构、抗锯齿

目录

  1. 光线追踪概述
  2. 光线投射(Ray Casting)
  3. 光线-表面交点计算
  4. 递归光线追踪
  5. Whitted光线追踪算法
  6. 光线类型和追踪策略
  7. 镜面反射与折射
  8. 抗锯齿
  9. 光线追踪加速结构
  10. 总结

1. 光线追踪概述

1.1 为什么需要光线追踪

光栅化 (Rasterization) 的局限性:

  • 仅支持直接照明(光从光源出发,最多反弹一次,进入眼睛)
  • 难以处理阴影(需要额外的阴影贴图技术)
  • 不支持透明和半透明材质
  • 难以模拟多次光反弹(全局光照)
  • 点光源假设(面光源需要复杂积分)

光线追踪 (Ray Tracing) 的优势:

  • 自然处理反射、折射、透射
  • 自动计算阴影(包括软阴影)
  • 支持多次光线反弹和全局光照
  • 支持面光源
  • 在物理上更加可靠

1.2 光线追踪的基本思想

光线追踪通过向后追踪光线 (backward ray tracing) 来计算像素颜色:

  1. 从相机眼睛出发,穿过每个像素投射光线进入场景
  2. 找到光线与场景的最近交点
  3. 在交点处进行着色,计算直接光照
  4. 递归地追踪反射和折射光线
  5. 所有光线贡献之和即为该像素的最终颜色

2. 光线投射(Ray Casting)

2.1 光线投射的定义

光线投射 (Ray Casting) 是光线追踪的基础,是一种灵活的可见性算法。

基本步骤

  1. 对每个像素 \((x, y)\)
  2. 从相机眼睛出发,通过该像素投射一条光线进入场景
  3. 找到光线与所有表面的交点,获取最近的交点
  4. 在该交点处进行着色计算,得到像素颜色

2.2 光线投射伪代码

Raycast()
{
  for each pixel (x, y) {
    color(pixel) = Trace(ray_through_pixel(x, y))
  }
}

Trace(ray)
{
  // 发射光线,返回沿光线反向传播的RGB辐度
  object_point = Closest_intersection(ray)
  if object_point exists:
    return Shade(object_point, ray)
  else:
    return Background_Color
}

Shade(point, ray)
{
  // 返回离开该点的光线辐度
  normal = compute_surface_normal(point)
  radiance = 0

  for each light source {
    shadow_ray = create_ray(point, light)
    if !in_shadow(shadow_ray) {
      radiance += phong_illumination(point, light)
    }
  }

  return radiance
}

2.3 光线投射的特点

  • 灵活性高:易于处理各种表面类型和材质
  • 支持透明:可以自然处理半透明物体(光栅化难以实现)
  • 着色准确:基于几何和光照的物理基础

3. 光线-表面交点计算

3.1 光线方程

一条光线可表示为参数方程:

\[\mathbf{r}(t) = \mathbf{p} + t\mathbf{d}\]

其中:

  • \(\mathbf{p}\):光线起点(origin)
  • \(\mathbf{d}\):光线方向(direction),通常为单位向量 \(\|\mathbf{d}\| = 1\)
  • \(t \geq 0\):参数,\(t=0\) 在光线起点,\(t > 0\) 沿正方向

3.2 隐式表面表示

表面可用以下方式表示:

隐式函数形式: \(f(\mathbf{x}) = 0\)

参数函数形式: \(\mathbf{x} = \mathbf{g}(u, v)\)

3.3 光线-表面相交

将光线方程代入表面方程求解参数 \(t\) 和表面坐标 $$(u, v)$:

隐式表面: \(f(\mathbf{p} + t\mathbf{d}) = 0\)

  • 一个方程,一个未知数 \(t\)(单变量方程)

参数表面: \(\mathbf{p} + t\mathbf{d} = \mathbf{g}(u, v)\)

  • 三个方程,三个未知数 $$(t, u, v)$(多变量方程)

3.4 光线-球体相交

球体隐式方程(球心在原点): \(x^2 + y^2 + z^2 - R^2 = 0\)

代入光线方程: \((p_x + td_x)^2 + (p_y + td_y)^2 + (p_z + td_z)^2 - R^2 = 0\)

展开为关于 \(t\) 的二次方程: \(At^2 + Bt + C = 0\)

其中:

  • \(A = d_x^2 + d_y^2 + d_z^2 = 1\)(假设方向向量单位化)
  • \[B = 2(p_x d_x + p_y d_y + p_z d_z) = 2\mathbf{p} \cdot \mathbf{d}\]
  • \[C = p_x^2 + p_y^2 + p_z^2 - R^2 = \|\mathbf{p}\|^2 - R^2\]

求解: \(t = \frac{-B \pm \sqrt{B^2 - 4C}}{2} = -\mathbf{p} \cdot \mathbf{d} \pm \sqrt{(\mathbf{p} \cdot \mathbf{d})^2 - \|\mathbf{p}\|^2 + R^2}\)

判断

  • 判别式 $$\Delta = B^2 - 4C > 0$:光线穿过球体,有两个交点
  • 判别式 \(\Delta = 0\):光线与球体相切,一个交点
  • 判别式 \(\Delta < 0\):光线错过球体,无交点
  • 取较小的 \(t > 0\) 作为最近交点

3.5 光线-三角形相交

三角形相交包含两个步骤:

  1. 光线-平面相交:计算光线与三角形所在平面的交点

    • 平面方程:\((\mathbf{x} - \mathbf{x}_0) \cdot \mathbf{n} = 0\)
    • 代入光线方程求解 \(t\)
  2. 点在三角形内:使用重心坐标判断交点是否在三角形内

    • 重心坐标:\(\mathbf{p} = b_0 \mathbf{x}_0 + b_1 \mathbf{x}_1 + b_2 \mathbf{x}_2\)
    • \(b_0, b_1, b_2 \geq 0\) 且 \(b_0 + b_1 + b_2 = 1\) 时点在三角形内

4. 递归光线追踪

4.1 递归光线追踪的思想

从光线投射扩展到递归光线追踪的关键改进:

  • 在交点处不仅计算直接光照,还发出反射光线折射光线
  • 递归追踪这些二级光线,找到它们的交点并继续着色
  • 所有光线的贡献累加得到最终颜色

4.2 光线类型

眼睛光线 (Eye Rays)

  • 从相机眼睛出发,穿过像素的光线
  • 追踪场景中最先击中的表面

阴影光线 (Shadow Rays)

  • 从表面点指向光源的光线
  • 用于判断该点是否在阴影中

反射光线 (Reflection Rays)

  • 从表面点沿镜面反射方向出发
  • 用于计算镜面反射贡献

折射光线 (Transmission Rays)

  • 从表面点沿折射方向出发
  • 用于计算透明物体的折射贡献

4.3 光线树(Ray Tree)

递归光线追踪产生光线树结构:

  • 根:眼睛光线
  • 内部节点:在交点处分裂出的反射和折射光线
  • 叶节点:进入背景或达到最大递归深度的光线

光线树的总贡献即为最终像素颜色。


5. Whitted光线追踪算法

5.1 核心算法

Whitted风格的光线追踪是最基础的递归光线追踪实现:

TraceRay(ray, recursion_depth)
{
  // 限制递归深度,避免无限递归
  if recursion_depth > MAX_DEPTH:
    return BLACK

  // 找最近交点
  intersection = Closest_Intersection(ray, scene)

  if no intersection:
    return Background_Color

  // 初始化辐度
  radiance = ZERO

  // 计算直接光照
  for each light_source:
    shadow_ray = Ray(intersection.point, light_source)
    if not IsInShadow(shadow_ray):
      radiance += PhongIllumination(intersection, light_source)

  // 计算镜面反射
  if material.specular_reflectance > 0:
    reflected_ray = ComputeReflectedRay(intersection)
    radiance += material.specular_reflectance *
               TraceRay(reflected_ray, recursion_depth + 1)

  // 计算镜面折射
  if material.specular_transmittance > 0:
    refracted_ray = ComputeRefractedRay(intersection)
    radiance += material.specular_transmittance *
               TraceRay(refracted_ray, recursion_depth + 1)

  return radiance
}

5.2 算法特点

  • 递归性:通过递归调用自身处理多次光反弹
  • 递归深度限制:防止无限递归(通常3-5层)
  • 能量衰减:每次反射/折射乘以对应系数,自动衰减能量
  • 阴影处理:通过阴影光线自动判断是否在阴影中

6. 光线类型和追踪策略

6.1 光线的分类

光线类型 来源 去向 用途
眼睛光线 相机 场景 确定可见性
阴影光线 表面 光源 判断阴影
反射光线 表面 镜面方向 镜面反射
折射光线 表面 折射方向 透射

6.2 追踪策略

树状追踪(Tree Tracing)

  • 每个交点处同时追踪反射和折射光线
  • 产生光线树,覆盖所有可能的光传播路径
  • 计算量随递归深度指数增长

优点:能捕捉所有重要的光学现象 缺点:计算效率低,噪声多


7. 镜面反射与折射

7.1 镜面反射

反射光线方向计算

已知入射光线方向 \(\mathbf{d}_{in}\) 和表面法线 \(\mathbf{n}\),反射光线方向为:

\[\mathbf{d}_{ref} = \mathbf{d}_{in} - 2(\mathbf{d}_{in} \cdot \mathbf{n})\mathbf{n}\]

或者等价地: \(\mathbf{d}_{ref} = -\mathbf{d}_{in} + 2(\mathbf{d}_{in} \cdot (-\mathbf{n}))(-\mathbf{n})\)

性质

  • 入射角 = 反射角
  • 反射光线与法线在入射光线同侧

7.2 镜面折射(透射)

Snell定律

当光线从折射率为 \(n_1\) 的介质进入折射率为 \(n_2\) 的介质时:

\[n_1 \sin\theta_1 = n_2 \sin\theta_2\]

折射光线方向计算

无需使用三角函数,可以用向量形式:

\[\mathbf{d}_{refr} = \frac{n_1}{n_2}\mathbf{d}_{in} + \left(\frac{n_1}{n_2}\cos\theta_1 - \cos\theta_2\right)\mathbf{n}\]

其中: \(\cos\theta_1 = -\mathbf{d}_{in} \cdot \mathbf{n}\) \(\cos\theta_2 = \sqrt{1 - \left(\frac{n_1}{n_2}\right)^2(1-\cos^2\theta_1)}\)

全内反射

  • 当 \(\sin\theta_2 > 1\)(即 $$\cos\theta_2$ 无实数解)时,发生全内反射
  • 光线从光学密度更高的介质进入更低密度的介质时可能发生

7.3 常见材料的折射率

材料 折射率 $n$
空气/真空 1.0
1.33
玻璃 ≈ 1.5
钻石 2.4

8. 抗锯齿

8.1 锯齿问题

光线追踪中的锯齿问题:

  • 每个像素仅投射一条光线,代表像素的单一点的颜色
  • 但像素代表屏幕上的一个小面积,该面积包含无限个点
  • 这些点可能有不同的颜色,导致采样不足和锯齿

8.2 超采样 (Supersampling)

基本方法

  • 为每个像素投射多条光线(例如 3×3 网格)
  • 对所有光线的结果进行加权平均
  • 使用滤波器(如高斯滤波)进行平滑处理

公式: \(I_{pixel} = \frac{1}{N}\sum_{i=1}^{N} I(x_i, y_i)\)

其中 \((x_i, y_i)\) 是像素内的 \(N\) 个采样点。

8.3 自适应超采样 (Adaptive Supersampling)

对高频区域进行更多采样,对平坦区域采样较少:

算法

  1. 将像素分为 2×2 子网格,投射 5 条光线(4 个角 + 1 个中心)
  2. 比较 5 条光线的颜色是否相似
  3. 如果相似,使用平均值;如果差异大,递归细分该子网格
  4. 继续直到子网格颜色均匀或达到最大深度
  5. 对最终结果应用滤波

优点

  • 自动在复杂区域增加采样密度
  • 在平坦区域减少计算量
  • 节省总体计算成本

9. 光线追踪加速结构

9.1 加速的必要性

性能瓶颈

  • 对于每条光线,需要测试与场景中所有物体的相交
  • 场景包含数百万个三角形时,朴素算法时间复杂度为 \(O(N \cdot M)\)
    • \(N\):光线数量
    • \(M\):场景中的三角形数量
  • 这导致渲染极其缓慢

加速思想

  • 使用空间分割数据结构
  • 光线只需测试可能相交的物体
  • 在树中进行递归搜索而不是遍历所有物体

9.2 空间分割加速结构

均匀网格 (Uniform Grid)

原理

  • 将场景的包围盒分为规则的立方体网格
  • 每个网格单元存储其中包含的物体列表
  • 光线遍历网格,只测试当前网格中的物体

优点

  • 实现简单
  • 对于均匀分布的场景非常快

缺点

  • 对非均匀场景(物体聚集)效率低
  • 可能需要遍历很多空网格
  • 内存占用可能很大

八叉树/四叉树 (Octree/Quadtree)

原理

  • 八叉树:3D空间的递归二分,每个节点分为 8 个子立方体
  • 四叉树:2D空间的递归二分,每个节点分为 4 个子正方形
  • 递归分割直到叶子节点足够简单(包含物体数量少或达到深度限制)

优点

  • 自适应地适应非均匀场景
  • 内存占用相对均衡

缺点

  • 比网格遍历稍复杂
  • 对于均匀场景可能不如网格快

k-d 树 (KD-Tree)

原理

  • 松弛八叉树的限制
  • 每次分割仅沿一个坐标轴(x、y 或 z)
  • 不要求分割点在中点
  • 选择最优分割点以平衡树的高度

优点

  • 更灵活的分割策略
  • 对多种场景都有好的性能
  • 广泛用于现代光线追踪器

缺点

  • 构建算法复杂
  • 需要选择好的分割策略

二叉空间分割树 (BSP-Tree)

原理

  • 进一步松弛 k-d 树
  • 可以用任意超平面进行分割(不仅限于坐标轴平行)
  • 在 3D 中用平面分割,在 2D 中用线分割

优点

  • 最灵活的分割方式
  • 可以精确适应场景几何

缺点

  • 构建最复杂
  • 遍历可能较慢(超平面测试比坐标轴对齐复杂)
  • 易产生很多分割后的物体片段

9.3 构建良好的加速结构

关键考虑因素

  1. 树的平衡性:避免退化树(所有节点在一侧)

    • 平衡树的高度为 \(O(\log N)\)
    • 深度过大会导致光线遍历时间长
  2. 分割数量:权衡树深度和每个节点的物体数

    • 分割过多:树深度大,光线遍历开销大
    • 分割过少:叶子节点包含太多物体,相交测试开销大
  3. 物体重叠:物体可能跨越多个节点

    • 会导致分割后物体数增加
    • BSP 树会产生 O(n³) 个物体片段(最坏情况)
    • 改进策略:选择导致最少分割的分割平面

10. 总结

10.1 核心算法流程

光线追踪工作流

for each pixel (x, y):
  ray = Ray(camera, pixel_direction)
  color = TraceRay(ray, depth=0)
  output_pixel(color)

TraceRay(ray, depth):
  if depth > MAX_DEPTH: return BLACK

  hit = ClosestIntersection(ray)
  if no hit: return BACKGROUND

  color = DirectLighting(hit)
  color += Reflections(hit, depth)
  color += Refractions(hit, depth)

  return color

10.2 关键数学公式

概念 公式
光线方程 \(\mathbf{r}(t) = \mathbf{p} + t\mathbf{d}\)
反射 \(\mathbf{d}_{ref} = \mathbf{d}_{in} - 2(\mathbf{d}_{in} \cdot \mathbf{n})\mathbf{n}\)
折射(Snell) \(n_1 \sin\theta_1 = n_2 \sin\theta_2\)
MIPMAP层级 \(\text{level} = \log_2(d)\)

10.3 光线追踪 vs 光栅化

特性 光线追踪 光栅化
全局光照 ✓ 自然支持 ✗ 需要近似
阴影 ✓ 精确 ✗ 需要阴影贴图
反射/折射 ✓ 精确 ✗ 需要特殊技巧
性能 ✗ 较慢 ✓ 实时
实现 ✓ 相对简单 ✗ 需要 GPU 优化
可扩展性 ✓ 易于扩展 ✓ 已高度优化

10.4 Whitted光线追踪的限制

  1. 镜面限制:仅处理镜面反射和折射,不能表现粗糙表面
  2. 递归限制:固定递归深度限制,深度超限后能量丢失
  3. 采样噪声:每像素一条光线产生高频噪声
  4. 效率低:计算成本随递归深度指数增长
  5. 静态照明:预设光源位置,难以处理动态光源

10.5 后续发展方向

  • 分布式光线追踪:多条光线采样,减少噪声
  • 路径追踪:更强大的递归框架,支持漫反射
  • 光子映射:结合光线和粒子的混合方法
  • 实时光线追踪:GPU 加速,在游戏中使用
  • 深度学习降噪:使用神经网络降低光线追踪的噪声



Enjoy Reading This Article?

Here are some more articles you might like to read next:

  • notes of ML
  • notes of VCI
  • notes of AIP
  • notes of AI Math Fundamentals
  • notes of ICS