第-1-章-从零开始的光线追踪之旅
Course: I wrote a Ray Tracer from scratch… in a Year
🎮 第 1 章:从零开始的光线追踪之旅
🏛 适用环境:自研引擎 / C++17 / SDL2 / Vulkan / OpenGL
🎯 目标:理解“光线追踪”的基本思想、渲染循环、与传统光栅化的区别。
🧭 引擎定位
适用于任何底层渲染系统( Vulkan / OpenGL / CPU Software Renderer),本章讲述从零构建一个纯 CPU 光线追踪器的思维路径。
重点是 “思想先于优化”:先让光线击中物体,再谈性能。
🏷 关键词速记
Ray Tracing|光线追踪
Rasterization|光栅化
CPU Renderer|软件渲染器
Scene Graph|场景图
Intersection|光线交点
Recursion|递归反射
🎮 30 秒玩法摘要
光线追踪不是“画出光线”,而是“倒着思考”:
从相机发射光线 → 找它撞到什么 → 模拟光线再反弹 → 得到像素颜色。
📚 核心原理讲解
🔹 1. 什么是光线追踪?
在传统的 光栅化(Rasterization) 渲染中,我们从场景中“画三角形”:
模型 → 投影 → 光照 → 屏幕像素。
而在 光线追踪(Ray Tracing) 中,我们反过来:
屏幕像素 → 发出光线 → 穿越场景 → 计算交点 → 决定颜色。
这是一种从相机出发的反向光线模拟。
🔹 2. 光线方程
一条光线可表示为:
- [\mathbf{O}]:光线起点(相机位置)
- [\mathbf{D}]:光线方向(单位向量)
- [t]:距离参数,决定光线行进距离
光线击中物体的条件是:
找到某个 [t > 0],使得 [\mathbf{P}(t)] 落在物体表面上。
🔹 3. 光线追踪渲染循环
每个像素都会发出一条或多条光线,算法如下:
📦 伪代码
for each pixel (x, y):
Ray ray = camera.generateRay(x, y)
Color color = trace(ray, depth=0)
framebuffer[x][y] = color而 trace() 函数递归处理反射、折射与阴影:
Color trace(Ray ray, int depth) {
if (depth > MAX_DEPTH) return backgroundColor;
HitInfo hit = scene.intersect(ray);
if (!hit) return backgroundColor;
Vector3 normal = hit.normal;
Vector3 lightDir = normalize(light.pos - hit.pos);
float diffuse = max(dot(normal, lightDir), 0.0f);
return hit.material.color * diffuse;
}🔹 4. 为什么选择从零实现
在视频中,作者提到:“我从完全不懂光线追踪,到一年后能输出一张逼真的图像。”
这反映出光线追踪的三层难度:
| 阶段 | 内容 | 技术关键词 |
|---|---|---|
| 第一阶段 | 光线与物体交点 | Sphere Intersection |
| 第二阶段 | 光照模型 | Diffuse / Specular |
| 第三阶段 | 全局光照 | Path Tracing / Monte Carlo |
🔹 5. 与光栅化的根本区别
| 对比项 | 光栅化 | 光线追踪 |
|---|---|---|
| 渲染方向 | 从模型到像素 | 从像素到模型 |
| 核心单位 | 三角形 | 光线 |
| 反射与折射 | 伪实现 | 真实模拟 |
| 性能 | 极快 | 极慢(需加速结构) |
| 真实感 | 较低 | 极高(物理一致性) |
📦 代码实现片段
最小光线追踪器结构:
struct Ray {
Vec3 origin, dir;
Ray(const Vec3& o, const Vec3& d): origin(o), dir(normalize(d)) {}
Vec3 at(float t) const { return origin + dir * t; }
};
struct Sphere {
Vec3 center; float radius;
bool hit(const Ray& ray, float& t) const {
Vec3 oc = ray.origin - center;
float a = dot(ray.dir, ray.dir);
float b = 2.0f * dot(oc, ray.dir);
float c = dot(oc, oc) - radius * radius;
float discriminant = b*b - 4*a*c;
if (discriminant < 0) return false;
t = (-b - sqrt(discriminant)) / (2.0f * a);
return t > 0;
}
};这段代码实现了最核心的部分:
“判断一条光线是否击中球体。”
🧪 实验与验证步骤
1️⃣ 实现一个单球场景
- 定义相机位置
O(0,0,0) - 画布范围 [-1,1]
- 球心
C(0,0,-5)半径r=1 - 对每个像素生成射线并测试相交。
2️⃣ 着色验证
- 若击中,颜色 = (法线 + 1) * 0.5 → 显示球面渐变。
- 若未击中,显示蓝色背景。
3️⃣ 递归验证
- 添加一次镜面反射(depth +1),可看到球体反光。
🪄 创作与实现建议
- 从 CPU 开始(不依赖 GPU),有助于理解光线与几何的本质。
- 输出
.ppm或.png直接验证结果,不必过早做 UI。 - 用 C++17 + STL 即可,无需外部库。
- 后期再引入多线程(std::thread 或 OpenMP)。
- 保留中间结果截图(早期图像灰模阶段最重要!)。
🧠 记忆口诀
“光线反打摄像机,交点才显物体迹;像素生光行万里,方得一帧真天地。”
✅ 实践题
Q1:光线追踪中的“反向”指的是什么?
🅰️ 从相机出发追踪光线,而非从光源发射。
Q2:为什么每个像素都需要独立光线?
🅰️ 因为每个像素代表不同视角方向,需要独立计算可见物体与光照。
Q3:光线方程中的 t 有什么物理含义?
🅰️ 光线沿方向向量的行进距离参数。