0. 为什么要学光线追踪

光线追踪(Ray Tracing)的价值不只是“更真实”,而是它把渲染问题拆成一套非常统一的数学与工程模块:发射射线 → 求交 → 计算散射/发光 → 继续追踪/结束。理解这套闭环后,再回头看光栅化管线、PBR、GI、采样与降噪,会更容易形成体系。

本文目标:做出一个能渲染球体/三角形、支持阴影、反射,并能逐步扩展到路径追踪的最小实现


1. 你需要的最小数学与约定

  • 向量:点乘(投影/夹角)、叉乘(法线/面积)、归一化
  • 射线: r(t) = o + t d ,其中 t > 0
  • 坐标系:右手/左手都行,但要一致(相机、法线方向、叉乘方向)
  • EPS:为避免自相交,常用一个很小的偏移量 \epsilon (如 1e{-4})

2. 关键模块总览(建议按这个顺序实现)

  • 2.1 相机(Camera):把像素坐标映射到世界空间射线
  • 2.2 几何(Hittable):射线-物体求交(球体最容易起步)
  • 2.3 材质(Material / BSDF):命中点如何散射(Lambert / Metal / Dielectric)
  • 2.4 光照/发光(Light / Emission):最初可以先只做“环境色 + 漫反射”
  • 2.5 阴影(Shadow Ray):从命中点向光源打一根射线判断遮挡
  • 2.6 递归(Recursion):镜面反射/折射靠递归追踪实现
  • 2.7 加速结构(BVH):物体多了以后必须有(否则 O(N) 求交会爆炸)
  • 2.8 采样与降噪:多采样、重要性采样、俄罗斯轮盘赌

3. 相机:从像素到射线

最常见的针孔相机(Pinhole Camera):

  • 设定视野角(FOV)、宽高比(aspect)、相机位置 pos
  • lookAt 构造相机的正交基 forward/right/up
  • 对每个像素 (i,j) 在成像平面上取样(可加抖动做抗锯齿)

关键点:你不是“画像素”,你是在为每个像素发射一条或多条射线


4. 求交:从球体开始(最小闭环)

球体求交是入门必做:把射线代入球面方程,解一元二次方程,找到最近的正根 t。

你需要一个统一的“命中记录”:

  • t:最近命中距离
  • p:命中点
  • n:法线(注意朝向:与射线方向同向/反向要统一处理)
  • materialId 或指针:命中物体的材质

工程建议:把“是否命中 + 命中信息”封装成接口,例如 hit(ray, tMin, tMax, outHit)


5. 阴影:Shadow Ray(立刻让画面“立起来”)

当你有点光源(Point Light)或方向光(Directional Light)时,阴影做法很直接:

  1. 主射线命中得到点 p
  2. p + n * eps 朝光源方向发射一根阴影射线
  3. 若在到达光源前被任何物体挡住,则该光源贡献为 0

注意:阴影射线也需要 tMax(点光:到光源距离;方向光:无穷远)。


6. 递归:反射/折射与“为什么会变慢”

镜面反射(Perfect Specular):

  • 反射方向 r = d - 2(d \cdot n)n

折射(Dielectric):

  • 需要折射率 \eta ,并根据入射/出射介质切换 \eta_i / \eta_t
  • 处理全反射(Total Internal Reflection)
  • 实践中常用 Schlick 近似计算菲涅尔反射概率

性能直觉:每一次反射/折射都意味着再打一条射线,递归深度与采样数会把成本指数式推高,所以后面必须引入:BVH + 重要性采样 + 轮盘赌


7. 从“光线追踪”到“路径追踪”:采样才是核心

如果你只做“直接光 + 阴影”,画面已经像回事,但还不算全局光照(GI)。

路径追踪(Path Tracing)的核心是:在命中点按 BSDF 分布随机采样出新的方向,累积贡献:

  • 多次反弹带来间接光(Color Bleeding、软阴影、漫反射全局照明)
  • 多次采样降低噪声(但需要更多时间)

最小可用策略:

  • 每像素 spp(samples per pixel)多次采样
  • 最大深度 maxDepth(比如 5~10)
  • 从某一深度开始用俄罗斯轮盘赌终止路径,保证无偏并控制成本

8. 代码最小骨架(伪代码/结构)

下面是一套“能跑起来”的结构(语言不限,C++/Rust/Python 都行),重点是模块边界清晰:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Color trace(const Ray& ray, int depth) {
if (depth <= 0) return Color(0);

Hit hit;
if (!scene.hit(ray, eps, INF, hit)) {
return skyColor(ray); // 背景/环境贴图
}

// 直接光(可选:先做这一步就能看到阴影)
Color Lo = directLighting(hit);

// 间接光:根据材质散射产生新射线(路径追踪核心)
Scatter s;
if (!hit.mat.scatter(ray, hit, s)) {
return Lo + hit.mat.emission(hit);
}

return Lo + s.attenuation * trace(s.scatteredRay, depth - 1);
}

如果你当前只想“学习光线追踪(不做 GI)”,也可以把 scatter 简化为:只支持镜面反射(Metal)+ 漫反射(Lambert)两种,先把闭环跑通。


9. BVH:从“能跑”到“能用”

当场景里有 10k 个三角形时,朴素求交是:

  • 每根射线都要遍历全部物体:O(N)
  • 每像素多采样 + 多反弹:射线数量成倍增加

BVH(Bounding Volume Hierarchy)提供的加速:

  • 用 AABB 包围盒快速排除大部分物体
  • 让求交接近 O(\log N)(实际依赖构建质量与场景分布)

建议实践路线:

  • 先实现 AABB.hit(ray)(slab method)
  • 再实现 BVH 节点(left/right + box)
  • 最后实现 BVH 构建(按轴排序、递归分割)

10. 学习路线(可直接照着做)

  • 第 1 周:相机 + 球体求交 + 法线可视化(把法线映射到颜色)
  • 第 2 周:Lambert 漫反射 + 点光源 + 阴影射线(硬阴影)
  • 第 3 周:反射/折射 + Schlick 菲涅尔 + 最大递归深度控制
  • 第 4 周:三角形与网格(Möller–Trumbore)+ BVH 加速
  • 第 5 周+:路径追踪(GI)+ 重要性采样 + 轮盘赌 + 简单降噪

11. 常见坑(非常容易踩)

  • 自相交:阴影射线/二次反弹从 p + n*eps 发射
  • 法线方向:统一为“与入射射线方向相反”的外法线(常见做法是根据 dot(ray.d, n) 翻转)
  • Gamma:写入图片前做 gamma 校正(最常用 gamma=2.2)
  • 能量守恒:材质参数别让反射/漫反射叠加超过 1(尤其是混合材质)
  • 噪声:路径追踪早期画面一定噪,先接受它,再用采样/重要性采样解决

12. 推荐资料(入门到进阶)

  • 《Ray Tracing in One Weekend》系列:最适合“先跑起来”,再逐步扩展(免费在线)
  • PBRT(Physically Based Rendering):更严谨的工程与理论,适合中后期系统学习
  • Real-Time Rendering:理解实时渲染体系与很多基础概念(配合光追一起看很香)

如果你愿意,我也可以在这篇文章后面继续追加一个“可运行的小项目”章节:用你偏好的语言(C++/Python/Rust),输出一张 ppm/png,并把项目结构放到仓库里(含 BVH、基础材质、以及一个 demo 场景)。