1. 项目定位与学习目标
本指南以 C++ 从零实现光线追踪器 为核心目标,系统化地复用 经典教程《Ray Tracing in One Weekend》 的思想,提供一个可落地的完整实现路径。通过分步驱动,读者可以在实践中逐步掌握从向量运算到最终渲染的完整流程。
在本章节中,学习目标包括:搭建高效的向量与射线工具、实现基本材质与光照模型、构建简洁的相机与场景描述,以及通过并行化提升渲染性能。最终产出将是一段可运行的光线追踪代码,能够渲染出简单场景的图像。
为实现这些目标,我们将坚持“从零到完整”的实践路线:先实现最小可运行版本,再逐步加入复杂材质、反射与折射,以及简单的场景管理。整个过程遵循
1) 先有最小可运行的路径追踪器
2) 在此基础上加入材质模型
3) 再增强相机和场景描述
4) 最后进行性能优化与并行化
实战核心要点
本节的核心要点包括:简化几何与光线交互的直观实现、利用路径追踪的递归求交与采样、以及通过对比调试来验证渲染结果。
2. 第一步:搭建开发环境
在正式开发前,确保拥有稳定的 C++17/20 编译环境,以及一个简单的构建系统。常用工具链包括 GCC/Clang、MSVC 以及 CMake,确保跨平台可移植性。
推荐的工作流程是:创建一个独立的源码目录、设置 CMakeLists.txt、使用标准输入输出检查构建状态,并通过简单的示例场景进行初步渲染验证。稳定的构建过程是后续迭代的基石。
下面给出一个最小的构建命令示例,便于快速验证环境是否就绪:
g++ -std=c++17 -O2 -Wall main.cpp -o raytracer
工具链与依赖的可选配置
如果希望提升构建速度与可维护性,可以引入 CMake、FetchContent 下载第三方头文件、以及简单的单元测试框架。通过这样的组合,能够在团队协作中保持一致性。
3. 第三步:基础向量与数学工具
3.1 向量类型 Vec3 的实现
光线追踪的核心在于高效的几何运算,因此第一步是实现一个通用的三维向量类 Vec3,包含点乘、叉乘、单位化等基本操作。
下面给出一个简化而完备的 Vec3 实现框架,作为后续射线与颜色运算的基础。向量运算的正确性决定了后续渲染质量。
// Vec3.h
#pragma once
#include
#include class Vec3 {
public:double x, y, z;Vec3(): x(0), y(0), z(0) {}Vec3(double x_, double y_, double z_): x(x_), y(y_), z(z_) {}Vec3 operator-() const { return Vec3(-x,-y,-z); }Vec3& operator+=(const Vec3& v) { x+=v.x; y+=v.y; z+=v.z; return *this; }Vec3& operator*=(double t) { x*=t; y*=t; z*=t; return *this; }Vec3& operator/=(double t) { return (*this) *= (1/t); }double length() const { return std::sqrt(length_squared()); }double length_squared() const { return x*x + y*y + z*z; }inline static Vec3 unit_vector(Vec3 v) { return v / v.length(); }
};inline Vec3 operator+(const Vec3& u, const Vec3& v) { return Vec3(u.x+v.x, u.y+v.y, u.z+v.z); }
inline Vec3 operator-(const Vec3& u, const Vec3& v) { return Vec3(u.x-v.x, u.y-v.y, u.z-v.z); }
inline Vec3 operator*(const Vec3& u, const Vec3& v) { return Vec3(u.x*v.x, u.y*v.y, u.z*v.z); }
inline Vec3 operator*(double t, const Vec3& v) { return Vec3(t*v.x, t*v.y, t*v.z); }
inline Vec3 operator*(const Vec3& v, double t) { return t * v; }
inline Vec3 operator/(Vec3 v, double t) { return (1/t) * v; }inline double dot(const Vec3& u, const Vec3& v) { return u.x*v.x + u.y*v.y + u.z*v.z; }
inline Vec3 cross(const Vec3& u, const Vec3& v) {return Vec3(u.y*v.z - u.z*v.y,u.z*v.x - u.x*v.z,u.x*v.y - u.y*v.x);
}inline std::ostream& operator<<(std::ostream &out, const Vec3& v) {return out << v.x << ' ' << v.y << ' ' << v.z;
}
3.2 射线类 Ray 的定义
在光线追踪中,射线描述了起点、方向以及参数化的穿越深度。实现一个简单的 Ray 类,便于后续的相交检测与采样。
// Ray.h
#pragma once
#include "Vec3.h"class Ray {
public:Vec3 orig;Vec3 dir;double tm;Ray() {}Ray(const Vec3& origin, const Vec3& direction, double time = 0.0): orig(origin), dir(direction), tm(time) {}Vec3 origin() const { return orig; }Vec3 direction() const { return dir; }Vec3 at(double t) const { return orig + t*dir; }
};
3.3 颜色输出与简单伪随机
颜色向量实际上对应到最终的像素颜色。初步实现将颜色向量映射到 [0,1] 区间并进行伪随机采样以模拟光线噪声。颜色线性化与伽玛校正在后续阶段逐步引入。
// Colour utilities
#include "Vec3.h"
#include void write_color(std::ostream& out, Vec3 pixel_color, int samples_per_pixel) {// gamma-correct for gamma = 2.0auto r = pixel_color.x;auto g = pixel_color.y;auto b = pixel_color.z;// divide the color by the number of samplesauto scale = 1.0 / double(samples_per_pixel);r = std::sqrt(scale * r);g = std::sqrt(scale * g);b = std::sqrt(scale * b);// Write the translated [0,255] value of each color componentout << static_cast(256 * std::clamp(r, 0.0, 0.999))<< ' ' << static_cast(256 * std::clamp(g, 0.0, 0.999))<< ' ' << static_cast(256 * std::clamp(b, 0.0, 0.999)) << '\n';
}
4. 第四步:光线与相机模型
4.1 摄像机模型的实现
相机负责将场景投影到像素网格上,常见做法是实现一个简化的透视相机,提供对投影平面的射线取样。通过设定 视野参数、焦平距,可以控制渲染效果。
下面给出一个简化的 Camera 实现,用于从图像平面生成射线,随后对每个像素进行采样。
// Camera.h
#pragma once
#include "Vec3.h"
#include "Ray.h"class Camera {
public:Vec3 origin;Vec3 lower_left_corner;Vec3 horizontal;Vec3 vertical;Camera() {auto aspect_ratio = 16.0/9.0;auto viewport_height = 2.0;auto viewport_width = aspect_ratio * viewport_height;auto focal_length = 1.0;origin = Vec3(0,0,0);horizontal = Vec3(viewport_width, 0, 0);vertical = Vec3(0, viewport_height, 0);lower_left_corner = origin - horizontal/2 - vertical/2 - Vec3(0,0,focal_length);}Ray get_ray(double u, double v) const {return Ray(origin, lower_left_corner + u*horizontal + v*vertical - origin, 0.0);}
};
4.2 射线-几何交互的简化思路
在初期版本中,我们不引入复杂的几何网格交点,而是通过场景中的简化几何体来实现交互。最简单的做法是对球体进行交点检测,作为入门级的光线追踪演示。
// Sphere.h
#pragma once
#include "Vec3.h"class Sphere {
public:Vec3 center;double radius;Sphere() {}Sphere(Vec3 cen, double r) : center(cen), radius(r) {}bool hit(const Ray& r, double t_min, double t_max, Vec3& temp) const {Vec3 oc = r.origin() - center;auto a = r.direction().length_squared();auto half_b = dot(oc, r.direction());auto c = oc.length_squared() - radius*radius;auto discriminant = half_b*half_b - a*c;if (discriminant < 0) return false;auto sqrtd = std::sqrt(discriminant);// Find the nearest root in acceptable rangeauto root = (-half_b - sqrtd) / a;if (root < t_min || t_max < root) {root = (-half_b + sqrtd) / a;if (root < t_min || t_max < root)return false;}temp = r.origin() + root*r.direction();return true;}
};
5. 第五步:材质与光照模型
5.1 漫反射材质 Lambertian
材质决定了光线与表面的交互方式。Lambertian 漫反射是最基础且易于实现的材质模型,能够将入射光均匀地散射到各个方向。
通过实现一个通用的材质接口,并为 Lambertian 提供一个简单的实现,我们能够在简单场景中得到可观的渲染效果。

// Material.h
#pragma once
#include "Ray.h"
#include "Vec3.h"class Material {
public:virtual bool scatter(constRay& r_in, const Vec3& hit_point, const Vec3& normal, Vec3& attenuation, Ray& scattered) const = 0;
};// Lambertian.h
#include "Material.h"
#include class Lambertian : public Material {
public:Vec3 albedo;Lambertian(const Vec3& a) : albedo(a) {}virtual bool scatter(const Ray& r_in, const Vec3& hit_point, const Vec3& normal, Vec3& attenuation, Ray& scattered) const override {auto scatter_direction = normal + Vec3::unit_vector(Vec3(1,0,0)); // 简化示例scattered = Ray(hit_point, scatter_direction, r_in.tm);attenuation = albedo;return true;}
};
5.2 反射与折射的扩展
在后续版本中,可以为金属、折射以及混合材质实现更复杂的分支逻辑,以提升真实感。菲涅耳效应、折射率、反射率等因素将逐步纳入计算。
// Metal.h
#pragma once
#include "Material.h"class Metal : public Material {
public:Vec3 albedo;double fuzz;Metal(const Vec3& a, double f) : albedo(a), fuzz(f < 1 ? f : 1) {}virtual bool scatter(const Ray& r_in, const Vec3& hit_point, const Vec3& normal, Vec3& attenuation, Ray& scattered) const override {Vec3 reflected = r_in.direction() - 2*dot(r_in.direction(), normal)*normal;scattered = Ray(hit_point, reflected + fuzz*Vec3(random(), random(), random()), r_in.tm);attenuation = albedo;return (dot(scattered.direction(), normal) > 0);}double random() const { return rand() / double(RAND_MAX); }
};
6. 第六步:场景、渲染循环与输出
6.1 场景描述与对象集合
为了能够渲染出一张完整的场景,需要管理一个对象集合,并进行交点检测。此处采用一个简单的结构把球体与材质绑定,组成可遍历的场景。
场景描述是关键的输入数据,它决定了渲染结果的外观与复杂度。通过逐步扩展场景中的物体种类,可以逐渐提升渲染的真实感。
// HitableList.h
#pragma once
#include "Sphere.h"
#include class HitableList {
public:std::vector objects;HitableList() {}bool hit(const Ray& r, double t_min, double t_max, Vec3& hit_point) const {Vec3 temp;bool hit_anything = false;double closest_so_far = t_max;for (const auto& obj : objects) {if (obj.hit(r, t_min, closest_so_far, temp)) {hit_anything = true;hit_point = temp;closest_so_far = (temp - r.origin()).length();}}return hit_anything;}
};
6.2 渲染循环与图像输出
渲染循环通过对每个像素进行若干次采样,计算颜色并输出到标准输出或文件。多采样抗锯齿与简单的 gamma 校正能够显著提升画质。
// main.cpp 主渲染循环简化版
#include
#include "Vec3.h"
#include "Ray.h"
#include "Camera.h"
#include "HitableList.h"
#include "Lambertian.h"int main() {// Imageconst auto aspect_ratio = 16.0/9.0;const int image_width = 400;const int image_height = static_cast(image_width / aspect_ratio);const int samples_per_pixel = 50;// WorldHitableList world;world.objects.push_back(Sphere(Vec3(0,0,-1), 0.5));world.objects.push_back(Sphere(Vec3(0,-100.5,-1), 100));// CameraCamera cam;// Renderstd::cout << "P3\n" << image_width << ' ' << image_height << "\n255\n";for (int j = image_height-1; j >= 0; --j) {for (int i = 0; i < image_width; ++i) {Vec3 pixel_color(0,0,0);for (int s = 0; s < samples_per_pixel; ++s) {double u = (i + double(rand())/RAND_MAX) / (image_width-1);double v = (j + double(rand())/RAND_MAX) / (image_height-1);Ray r = cam.get_ray(u, v);// 这里应当调用“ray_color”递归函数计算颜色pixel_color += Vec3(0.5*(r.direction().x+1), 0.5*(r.direction().y+1), 0.5*(r.direction().z+1));}write_color(std::cout, pixel_color, samples_per_pixel);}}
}
7. 第七步:完整实现的整合与运行
7.1 项目结构与文件组织
为保持代码清晰,推荐将核心模块分成若干头文件与实现文件,例如 Vec3.h、Ray.h、Camera.h、Sphere.h、Material.h、以及主程序入口 main.cpp,形成清晰的模块边界。
在实现中,接口设计应保持简单可替换,以便后续替换不同的材质、光源模型和几何体。
// 目录结构示例
/project/includeVec3.hRay.hCamera.hSphere.hMaterial.hLambertian.h/srcmain.cppCMakeLists.txt
7.2 构建与运行命令示例
在完成代码后,可以使用简单的构建脚本来编译与执行。以下命令展示了一个典型的构建与运行流程:构建、链接、运行,输出渲染结果为 PPM 图像格式。
cmake -S . -B build
cmake --build build -j
./build/raytracer > image.ppm
7.3 结果检查与调试要点
渲染结果应呈现简易场景的光亮球体与背景渐变。若颜色偏色或形状异常,可以通过以下调试要点快速定位问题:交点判定、向量单位化、材质散射方向与相机参数的合理性。
如果需要更真实的效果,可以在后续阶段加入 全局光照、多bounce 路径追踪、以及体积雾效等模块,以提升场景的真实感与可视性。


