7天从零实现光线追踪:第一周完整代码与核心原理剖析
引言:为什么选择光线追踪?
你是否曾好奇3D电影中的逼真光影是如何实现的?传统光栅化渲染虽然高效,但在处理全局光照、反射折射等效果时往往力不从心。光线追踪(Ray Tracing)作为一种基于物理的渲染技术,通过模拟光线传播路径,能够产生极其真实的视觉效果。本文将带你从零开始,通过raytracing.github.io项目的第一周教程,用不到500行代码实现一个基础但功能完整的光线追踪程序。
读完本文你将掌握:
- 向量数学在3D空间中的核心应用
- 光线与物体相交检测的数学原理
- 三种基本材质(漫反射/金属/玻璃)的实现
- 抗锯齿与景深效果的编程技巧
- 完整可运行的C++光线追踪代码框架
项目架构概览
raytracing.github.io项目采用模块化设计,第一周教程包含10个核心文件,构成了光线追踪程序的基础架构:
src/InOneWeekend/
├── main.cc # 主程序入口,场景定义与渲染控制
├── rtweekend.h # 全局常量与工具函数
├── vec3.h # 向量/点/颜色基础运算
├── ray.h # 光线类定义
├── color.h # 颜色处理与伽马校正
├── hittable.h # 可命中物体接口
├── sphere.h # 球体几何体实现
├── hittable_list.h # 物体集合管理
├── material.h # 材质系统(漫反射/金属/玻璃)
└── camera.h # 虚拟相机与光线生成
核心模块依赖关系
基础数学工具:向量与光线
向量类(vec3.h)详解
向量是光线追踪的数学基础,负责表示3D空间中的点、方向和颜色。该项目中的vec3类实现了完整的3D向量运算:
class vec3 {
public:
double e[3]; // 存储x,y,z分量
// 构造函数
vec3() : e{0,0,0} {}
vec3(double e0, double e1, double e2) : e{e0, e1, e2} {}
// 分量访问
double x() const { return e[0]; }
double y() const { return e[1]; }
double z() const { return e[2]; }
// 基础运算重载
vec3 operator-() const { return vec3(-e[0], -e[1], -e[2]); }
double operator[](int i) const { return e[i]; }
double& operator[](int i) { return e[i]; }
vec3& operator+=(const vec3& v) {
e[0] += v.e[0]; e[1] += v.e[1]; e[2] += v.e[2];
return *this;
}
// 向量运算函数
double length() const { return std::sqrt(length_squared()); }
double length_squared() const { return e[0]*e[0] + e[1]*e[1] + e[2]*e[2]; }
// 随机向量生成
static vec3 random() {
return vec3(random_double(), random_double(), random_double());
}
};
// 类型别名,增强代码可读性
using point3 = vec3; // 3D点
using color = vec3; // RGB颜色
关键向量运算
| 运算类型 | 函数实现 | 应用场景 |
|---|---|---|
| 点积 | dot(u, v) = u.x()*v.x() + u.y()*v.y() + u.z()*v.z() | 计算角度关系,判断方向 |
| 叉积 | cross(u, v) | 构建坐标系,计算法线 |
| 单位化 | unit_vector(v) = v / v.length() | 方向向量标准化 |
| 反射 | reflect(v, n) = v - 2*dot(v,n)*n | 金属材质高光计算 |
| 折射 | refract(uv, n, etai_over_etat) | 玻璃材质光线弯曲 |
光线类(ray.h)实现
光线被定义为P(t) = origin + t*direction,其中t是一个标量参数:
class ray {
public:
ray() {}
ray(const point3& origin, const vec3& direction) : orig(origin), dir(direction) {}
const point3& origin() const { return orig; }
const vec3& direction() const { return dir; }
// 光线行进函数:计算t参数对应的3D点
point3 at(double t) const {
return orig + t*dir;
}
private:
point3 orig; // 光线起点
vec3 dir; // 光线方向(非单位向量)
};
核心渲染逻辑:从光线到像素
相机系统(camera.h)
相机类负责将2D像素坐标转换为3D光线,并处理景深效果:
class camera {
public:
double aspect_ratio = 16.0 / 9.0; // 宽高比
int image_width = 1200; // 图像宽度(像素)
int samples_per_pixel = 10; // 每像素采样数(抗锯齿)
int max_depth = 20; // 光线最大反弹次数
// 相机位置与朝向
point3 lookfrom = point3(13,2,3); // 相机位置
point3 lookat = point3(0,0,0); // 观察点
vec3 vup = vec3(0,1,0); // 上方向向量
// 景深参数
double defocus_angle = 0.6; // 散焦角度
double focus_dist = 10.0; // 对焦距离
void render(const hittable& world) {
initialize(); // 初始化相机参数
// 输出PPM图像头
std::cout << "P3\n" << image_width << ' ' << image_height << "\n255\n";
// 逐行扫描像素
for (int j = 0; j < image_height; j++) {
std::clog << "\rScanlines remaining: " << (image_height - j) << ' ' << std::flush;
for (int i = 0; i < image_width; i++) {
color pixel_color(0,0,0);
// 多采样抗锯齿
for (int sample = 0; sample < samples_per_pixel; sample++) {
ray r = get_ray(i, j);
pixel_color += ray_color(r, max_depth, world);
}
// 写入颜色(已进行伽马校正)
write_color(std::cout, pixel_samples_scale * pixel_color);
}
}
}
private:
// 相机初始化函数:计算视口和像素位置
void initialize() {
image_height = int(image_width / aspect_ratio);
pixel_samples_scale = 1.0 / samples_per_pixel;
// 计算相机坐标系
w = unit_vector(lookfrom - lookat);
u = unit_vector(cross(vup, w));
v = cross(w, u);
// 计算视口参数
auto theta = degrees_to_radians(vfov);
auto h = std::tan(theta/2);
auto viewport_height = 2 * h * focus_dist;
auto viewport_width = viewport_height * (double(image_width)/image_height);
// 计算像素增量向量
vec3 viewport_u = viewport_width * u;
vec3 viewport_v = viewport_height * -v;
pixel_delta_u = viewport_u / image_width;
pixel_delta_v = viewport_v / image_height;
// 计算左上角像素位置
auto viewport_upper_left = center - (focus_dist * w) - viewport_u/2 - viewport_v/2;
pixel00_loc = viewport_upper_left + 0.5 * (pixel_delta_u + pixel_delta_v);
// 计算散焦圆盘参数(景深)
auto defocus_radius = focus_dist * std::tan(degrees_to_radians(defocus_angle / 2));
defocus_disk_u = u * defocus_radius;
defocus_disk_v = v * defocus_radius;
}
// 生成通过像素(i,j)的光线
ray get_ray(int i, int j) const {
auto offset = sample_square(); // 采样正方形内的随机偏移
auto pixel_sample = pixel00_loc
+ ((i + offset.x()) * pixel_delta_u)
+ ((j + offset.y()) * pixel_delta_v);
auto ray_origin = (defocus_angle <= 0) ? center : defocus_disk_sample();
auto ray_direction = pixel_sample - ray_origin;
return ray(ray_origin, ray_direction);
}
};
光线颜色计算(ray_color)
光线颜色计算是光线追踪的核心,通过递归追踪光线与物体的相交,并计算材质的反射/折射效果:
color ray_color(const ray& r, int depth, const hittable& world) const {
// 递归深度达到上限,返回黑色
if (depth <= 0)
return color(0,0,0);
hit_record rec;
// 判断光线是否命中物体(t_min=0.001避免自相交)
if (world.hit(r, interval(0.001, infinity), rec)) {
ray scattered;
color attenuation;
// 调用材质的散射函数计算新光线
if (rec.mat->scatter(r, rec, attenuation, scattered))
return attenuation * ray_color(scattered, depth-1, world);
return color(0,0,0); // 无散射,返回黑色
}
// 未命中物体,返回天空背景色(线性插值)
vec3 unit_direction = unit_vector(r.direction());
auto a = 0.5*(unit_direction.y() + 1.0);
return (1.0-a)*color(1.0, 1.0, 1.0) + a*color(0.5, 0.7, 1.0);
}
材质系统:模拟真实世界的表面特性
材质系统是光线追踪中实现真实感渲染的关键,项目实现了三种基础材质:
材质基类(material.h)
class material {
public:
virtual ~material() = default;
// 散射函数:输入入射光线,输出散射光线和衰减系数
virtual bool scatter(
const ray& r_in, const hit_record& rec, color& attenuation, ray& scattered
) const {
return false;
}
};
1. 漫反射材质(lambertian)
漫反射材质会随机散射光线,产生柔和的无高光表面:
class lambertian : public material {
public:
lambertian(const color& albedo) : albedo(albedo) {}
bool scatter(const ray& r_in, const hit_record& rec, color& attenuation, ray& scattered)
const override {
// 计算散射方向:法线方向+随机单位向量
auto scatter_direction = rec.normal + random_unit_vector();
// 处理退化方向(当随机向量与法线恰好反向)
if (scatter_direction.near_zero())
scatter_direction = rec.normal;
scattered = ray(rec.p, scatter_direction);
attenuation = albedo; // 材质颜色决定光线衰减
return true;
}
private:
color albedo; // 反照率:表面颜色
};
漫反射光线方向生成
2. 金属材质(metal)
金属材质会反射光线,可通过fuzz参数控制表面粗糙度:
class metal : public material {
public:
metal(const color& albedo, double fuzz) : albedo(albedo), fuzz(fuzz < 1 ? fuzz : 1) {}
bool scatter(const ray& r_in, const hit_record& rec, color& attenuation, ray& scattered)
const override {
// 计算理想反射方向
vec3 reflected = reflect(r_in.direction(), rec.normal);
// 添加模糊效果:反射方向+随机向量*模糊系数
reflected = unit_vector(reflected) + (fuzz * random_unit_vector());
scattered = ray(rec.p, reflected);
attenuation = albedo;
// 确保反射光线在表面正面
return (dot(scattered.direction(), rec.normal) > 0);
}
private:
color albedo; // 金属颜色
double fuzz; // 模糊系数(0=完美镜面,1=最大模糊)
};
3. 电介质(玻璃)材质(dielectric)
玻璃材质同时表现反射和折射,并可能产生全内反射:
class dielectric : public material {
public:
dielectric(double refraction_index) : refraction_index(refraction_index) {}
bool scatter(const ray& r_in, const hit_record& rec, color& attenuation, ray& scattered)
const override {
attenuation = color(1.0, 1.0, 1.0); // 玻璃几乎不吸收光线
// 确定折射率比值(入射介质/折射介质)
double ri = rec.front_face ? (1.0/refraction_index) : refraction_index;
vec3 unit_direction = unit_vector(r_in.direction());
double cos_theta = std::fmin(dot(-unit_direction, rec.normal), 1.0);
double sin_theta = std::sqrt(1.0 - cos_theta*cos_theta);
// 判断是否发生全内反射
bool cannot_refract = ri * sin_theta > 1.0;
vec3 direction;
// 使用Schlick近似计算反射概率
if (cannot_refract || reflectance(cos_theta, ri) > random_double())
direction = reflect(unit_direction, rec.normal);
else
direction = refract(unit_direction, rec.normal, ri);
scattered = ray(rec.p, direction);
return true;
}
private:
double refraction_index; // 折射率(真空=1.0,玻璃≈1.5)
// Schlick近似:计算反射系数
static double reflectance(double cosine, double refraction_index) {
auto r0 = (1 - refraction_index) / (1 + refraction_index);
r0 = r0*r0;
return r0 + (1-r0)*std::pow((1 - cosine),5);
}
};
三种材质效果对比
| 材质类型 | 核心特性 | 关键参数 | 视觉效果 |
|---|---|---|---|
| 漫反射 | 随机散射 | albedo(颜色) | 柔和无高光 |
| 金属 | 镜面反射 | albedo(颜色), fuzz(粗糙度) | 高光+边缘模糊 |
| 电介质 | 反射+折射 | refraction_index(折射率) | 透明+内部反射+色散 |
场景构建:main.cc详解
主程序负责创建场景、设置相机参数并启动渲染:
场景定义
int main() {
hittable_list world; // 物体列表
// 创建地面(大球体)
auto ground_material = make_shared<lambertian>(color(0.5, 0.5, 0.5));
world.add(make_shared<sphere>(point3(0,-1000,0), 1000, ground_material));
// 随机生成小球体
for (int a = -11; a < 11; a++) {
for (int b = -11; b < 11; b++) {
auto choose_mat = random_double();
point3 center(a + 0.9*random_double(), 0.2, b + 0.9*random_double());
// 避免与中心大球重叠
if ((center - point3(4, 0.2, 0)).length() > 0.9) {
shared_ptr<material> sphere_material;
if (choose_mat < 0.8) { // 80%概率漫反射材质
auto albedo = color::random() * color::random();
sphere_material = make_shared<lambertian>(albedo);
world.add(make_shared<sphere>(center, 0.2, sphere_material));
} else if (choose_mat < 0.95) { // 15%概率金属材质
auto albedo = color::random(0.5, 1);
auto fuzz = random_double(0, 0.5);
sphere_material = make_shared<metal>(albedo, fuzz);
world.add(make_shared<sphere>(center, 0.2, sphere_material));
} else { // 5%概率玻璃材质
sphere_material = make_shared<dielectric>(1.5);
world.add(make_shared<sphere>(center, 0.2, sphere_material));
}
}
}
}
// 添加三个大球作为场景主体
auto material1 = make_shared<dielectric>(1.5
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考



