第-1-章-从零开始的光线追踪之旅

Course: I wrote a Ray Tracer from scratch… in a Year

🏛 适用环境:自研引擎 / C++17 / SDL2 / Vulkan / OpenGL

🎯 目标:理解“光线追踪”的基本思想、渲染循环、与传统光栅化的区别。


适用于任何底层渲染系统( Vulkan / OpenGL / CPU Software Renderer),本章讲述从零构建一个纯 CPU 光线追踪器的思维路径。

重点是 “思想先于优化”:先让光线击中物体,再谈性能。


Ray Tracing|光线追踪

Rasterization|光栅化

CPU Renderer|软件渲染器

Scene Graph|场景图

Intersection|光线交点

Recursion|递归反射


光线追踪不是“画出光线”,而是“倒着思考”:

从相机发射光线 → 找它撞到什么 → 模拟光线再反弹 → 得到像素颜色。


在传统的 光栅化(Rasterization) 渲染中,我们从场景中“画三角形”:

模型 → 投影 → 光照 → 屏幕像素。

而在 光线追踪(Ray Tracing) 中,我们反过来:

屏幕像素 → 发出光线 → 穿越场景 → 计算交点 → 决定颜色。

这是一种从相机出发的反向光线模拟

一条光线可表示为:

P(t)=O+tD \mathbf{P}(t) = \mathbf{O} + t \mathbf{D}
  • [\mathbf{O}]:光线起点(相机位置)
  • [\mathbf{D}]:光线方向(单位向量)
  • [t]:距离参数,决定光线行进距离

光线击中物体的条件是:

找到某个 [t > 0],使得 [\mathbf{P}(t)] 落在物体表面上。


每个像素都会发出一条或多条光线,算法如下:

📦 伪代码

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;
}

在视频中,作者提到:“我从完全不懂光线追踪,到一年后能输出一张逼真的图像。”

这反映出光线追踪的三层难度:

阶段内容技术关键词
第一阶段光线与物体交点Sphere Intersection
第二阶段光照模型Diffuse / Specular
第三阶段全局光照Path Tracing / Monte Carlo

对比项光栅化光线追踪
渲染方向从模型到像素从像素到模型
核心单位三角形光线
反射与折射伪实现真实模拟
性能极快极慢(需加速结构)
真实感较低极高(物理一致性)

最小光线追踪器结构:

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 有什么物理含义?

🅰️ 光线沿方向向量的行进距离参数。