第-7-章-一年后的启示与优化建议

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


🏛 适用环境:完整 CPU/GPU 光线追踪器(C++17 / Vulkan / OptiX)

🎯 目标:总结作者一年从零编写 Ray Tracer 的关键经验、架构优化技巧、多线程与采样加速方案,并提出未来扩展方向。


这部分内容对应光线追踪项目的 架构与优化层

它不是在讲“算法怎么写”,

而是在讲“如何让算法跑得快、改得动、画得美”。


Multi-threading|多线程

Sampling Strategy|采样策略

Tone Mapping|色调映射

BVH Rebuild|包围体重建

Parallel Rendering|并行渲染

Denoising|去噪

GPU Acceleration|GPU 加速


光线追踪从来不是“写一千行代码”,

而是“调一百万次参数”。

真正的进步,不是学会画光,而是学会如何管理光。


作者最初的 Ray Tracer 只有一个文件、一千行代码;

一年后,它演变成一个模块化引擎

📦 系统分层结构

Renderer/
 ├── core/
 │   ├── ray.h
 │   ├── camera.h
 │   ├── hittable.h
 │   ├── material.h
 │   └── bvh.h
 ├── integrator/
 │   ├── pathtracer.cpp
 │   └── directlight.cpp
 ├── scene/
 │   ├── world.cpp
 │   ├── sky.cpp
 │   └── emitter.cpp
 ├── utils/
 │   ├── random.h
 │   ├── timer.h
 │   └── color.h
 └── main.cpp

这种模块化结构的意义:

  • 🧱 易于扩展新的材质、相机、加速结构
  • 🔁 可单独测试各模块(单元测试)
  • ⚙️ 支持多线程、命令行参数、配置文件化

每个像素的计算相互独立,

天然适合并行:

📦 OpenMP / std::thread 示例

#pragma omp parallel for schedule(dynamic)
for (int j = image_height - 1; j >= 0; --j) {
    for (int i = 0; i < image_width; ++i) {
        Color pixel(0,0,0);
        for (int s=0; s < samples_per_pixel; ++s) {
            float u = (i + randomFloat()) / image_width;
            float v = (j + randomFloat()) / image_height;
            Ray ray = camera.getRay(u, v);
            pixel += trace(ray, world, 0);
        }
        framebuffer[j * image_width + i] = pixel / samples_per_pixel;
    }
}
  • #pragma omp parallel for 轻松并行循环
  • 或使用 std::async 提交任务池
  • 性能提升可达 4~8×(多核 CPU)

路径追踪的噪点源于伪随机不均匀性

解决方案:

  • 使用 Sobol / Halton 序列(低差异采样)
  • Stratified Sampling(分层采样)
  • 蓝噪分布(Blue Noise)

📦 Stratified 采样示例

float u = (i + (s + randomFloat()) / spp_x) / image_width;
float v = (j + (t + randomFloat()) / spp_y) / image_height;

结果:同样采样数下更平滑、收敛更快。


即便 1000 样本仍有噪点,可使用 后处理降噪器

  • Intel OpenImageDenoise (OIDN)
  • NVIDIA OptiX AI Denoiser
  • 或自己实现简单的 Bilateral Filter

📦 双边滤波简例

Color bilateral(const Image& img, int x, int y) {
    float sigmaS = 2.0f, sigmaR = 0.1f;
    Color result(0);
    float totalWeight = 0;
    for (int dx=-2; dx<=2; ++dx)
      for (int dy=-2; dy<=2; ++dy) {
          Color diff = img(x,y) - img(x+dx,y+dy);
          float w = exp(-(dx*dx+dy*dy)/(2*sigmaS*sigmaS))
                  * exp(-dot(diff,diff)/(2*sigmaR*sigmaR));
          result += img(x+dx,y+dy) * w;
          totalWeight += w;
      }
    return result / totalWeight;
}

渲染器输出线性颜色,但显示器是非线性的。

需应用伽马校正:

Cdisplay=Clinear C_{display} = \sqrt{C_{linear}}

同时可应用 ACES 色调映射以避免高亮溢出。

📦 伽马与色调映射

Color toneMap(const Color& c) {
    Color mapped = c / (c + Color(1.0));
    return Color(sqrt(mapped.r), sqrt(mapped.g), sqrt(mapped.b));
}

CPU 路径追踪虽易调试,但瓶颈明显。

作者在后期迁移到 GPU 实现:

  • 将每条光线路径映射为 GPU 线程
  • 使用 Vulkan Ray Query / NVIDIA OptiX
  • BVH 构建使用 LBVH(线性 BVH)并行构造

性能提升:从 30 秒 → 0.2 秒 / 帧


  • 法线可视化模式:直接输出 normal * 0.5 + 0.5
  • 深度图 (Depth Map):输出 t 值 / 距离归一化。
  • 采样数热图:查看噪点分布区域。
  • 包围盒显示:验证 BVH 层级划分是否合理。
  • 反射路径可视化:随机染色不同反弹层。

  • 光线追踪不是“数学难”,而是“耐心难”。
  • 一次次 debug 的画面,从全黑 → 噪点 → 影子 → 光泽,是成就感的积累。
  • 真正的突破,不是性能,而是理解了 光的逻辑
  • 你开始像光一样思考:它走到哪里,为何折返。

1️⃣ 多线程性能对比

  • 单线程 vs OpenMP vs async,测帧时间。

2️⃣ 采样分布实验

  • 比较随机采样与低差异采样图像噪点。

3️⃣ 色调映射对比

  • 同一场景分别输出线性、伽马、ACES。

4️⃣ GPU vs CPU 性能

  • 同场景帧时统计性能对比。

  • 每个模块单独可运行(unit-test 方式)。
  • 用 JSON 配置场景,支持热加载。
  • 将输出缓存为 EXR(HDR 格式)保存全部亮度信息。
  • 使用 “Frame Accumulation” 技术在游戏引擎中实时预览。
  • 若后续整合到 Zgine,可在编辑器中提供 PathTracePreview 模式。

“盒包千物加速光,采样万线降噪忙;

多核并行图飞起,映射调色复真章。”


Q1:路径追踪的最大瓶颈是什么?

🅰️ 随机采样收敛慢、噪点重、每像素计算量大。

Q2:为什么要做 Gamma Correction?

🅰️ 因为显示设备是非线性的,线性颜色需转为感知均衡空间。

Q3:BVH 重建的意义?

🅰️ 当场景动态变化时重建层级结构以保持加速效果。

Q4:低差异采样相对随机采样的优势?

🅰️ 更均匀、更快收敛、噪点更小。


✨ 一年之后,作者终于能在屏幕上看到“光线真的存在”的那一刻。

你写下的每一行代码,都在告诉电脑:

“去追光吧。”

而追光的过程,

其实也是一个开发者——在复杂世界中寻找真实的旅程。