递归光线追踪 - 镜面反射

日期: 2026-02-18
开发时间: 约15分钟
迭代次数: 2次
技术: 递归光线追踪、镜面反射、Phong光照、阴影

项目目标

在之前的 Shadow Ray Tracing 基础上,实现递归光线追踪算法,支持镜面反射效果。通过不同的材质反射率,实现从完全漫反射到高度镜面的多样化材质表现。

核心算法

1. 递归光线追踪

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Vec3 trace(const Ray& ray, const Scene& scene, int depth) {
if (depth <= 0) return Vec3(0, 0, 0); // 递归深度限制

// ... 计算直接光照(环境光 + 漫反射 + 镜面高光 + 阴影)...

// 递归反射
if (material.reflectivity > 0.0) {
Vec3 reflect_dir = ray.direction.reflect(normal);
Ray reflect_ray(hit_point + normal * 1e-4, reflect_dir);
Vec3 reflect_color = trace(reflect_ray, scene, depth - 1);

// 混合漫反射和镜面反射颜色
color = color * (1.0 - reflectivity) + reflect_color * reflectivity;
}

return color;
}

关键技术点

  • 递归深度限制:防止无限递归(本项目最大深度5)
  • 反射光线偏移hit_point + normal * 1e-4 防止浮点误差自相交
  • 能量守恒混合(1-r) 部分漫反射 + r 部分反射

2. 镜面反射向量

反射公式:R = V - 2(V·N)N

1
2
3
Vec3 reflect(const Vec3& normal) const {
return *this - normal * (2.0 * this->dot(normal));
}

3. 材质系统

1
2
3
4
5
6
struct Material {
Vec3 color; // 基础颜色
double diffuse; // 漫反射系数 [0,1]
double specular; // 高光系数 [0,1]
double reflectivity; // 反射率 [0,1]
};

场景材质配置

  • 中心银球:reflectivity = 0.8, diffuse = 0.4(高反射 + 适量漫反射,避免纯黑)
  • 左侧红球:reflectivity = 0.4(半透半反)
  • 右侧蓝球:reflectivity = 0.2(轻微反射)
  • 地面:reflectivity = 0.0(纯漫反射)
  • 顶部金球:reflectivity = 0.6(较强反射)

光照配置

  • 主光源:intensity = 1.5(右上方,白光)
  • 副光源:intensity = 1.0(左上方,微蓝光)
  • 环境光:ambient = 0.2(全局基础亮度)

开发过程

迭代历史

版本 问题 解决方案
v1 编译错误:Vec3 * Vec3 未定义 添加逐分量乘法运算符
v2 ✅ 编译通过,运行成功 无需修改
v3 中心银球过暗 增加 diffuse (0.1→0.3), 降低 reflectivity (0.8→0.7)
v4 中心球仍然偏暗 尝试纯镜面反射 (reflectivity=1.0, diffuse=0.0)
v5 中心球变成纯黑 回滚:保留适量漫反射 (diffuse=0.4, reflectivity=0.8) + 增强光源
v6 中心球仍然是黑的! 修复致命Bug:反射向量计算错误

迭代次数: 6次
开发时间: 约45分钟(含多次Bug修复)

v1错误原因: 颜色混合需要向量逐分量相乘,但只定义了标量乘法
v3问题原因: 过低的漫反射系数 (0.1) + 过高的反射率 (0.8) → 直接光照太弱 → 球体过暗
v4致命错误: diffuse=0.0 → 无直接光照 → 当递归深度用尽时 reflected_color = (0,0,0) → 纯黑球
v5以为修复了:增加 diffuse=0.4 + 增强光照,但球体仍然是黑的
v6根本原因: 反射向量计算错误 - 用了 (-ray.direction).reflect(normal) 导致反射方向完全错误!

v1修复方法:

1
2
3
Vec3 operator*(const Vec3& v) const { 
return Vec3(x * v.x, y * v.y, z * v.z);
}

v3修复方法: 调整材质参数平衡

1
2
3
// 修改前: Material(Vec3(0.9, 0.9, 0.9), 0.1, 0.9, 0.8)
// 修改后: Material(Vec3(0.9, 0.9, 0.9), 0.3, 0.9, 0.7)
// 说明: 增加漫反射让球体更亮,稍降反射率保留更多直接光照

v6致命Bug修复: 反射向量计算错误

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// ❌ 错误代码 (v1-v5都有这个Bug):
Vec3 reflect_dir = (ray.direction * -1.0).reflect(normal);
// ^^^^^^^^^^^^^^^^^^^^^^^^
// 多余的负号!导致反射方向完全错误

// 反射公式: R = I - 2(I·N)N
// 其中 I 是入射方向(从相机指向表面)
// ray.direction 本身就是入射方向,不需要取反!

// ✅ 正确代码 (v6):
Vec3 reflect_dir = ray.direction.reflect(normal);
// ^^^^^^^^^^^^^^^
// 直接反射入射方向

// 为什么错误导致黑球?
// 1. 错误的反射方向 → 反射光线射向错误位置
// 2. 错误位置可能是空白区域(返回天空色)或背面
// 3. 即使有 diffuse=0.4,80%的颜色来自错误的反射 → 整体偏暗/黑色
// 4. 这也解释了为什么v3-v5调整材质参数都无效!

// 验证修复:
// 中心球区域采样: RGB(50.9%, 75.8%, 65.4%) ← 明亮的青绿色 ✅
// 说明正确反射了周围的红球、蓝球、金球和地面!

效果展示

递归光线追踪 - 镜面反射

可观察到的效果

  • ✅ 中心银球高反射率镜面效果(清晰反射周围物体)
  • ✅ 球体亮度正常(diffuse=0.4 提供基础亮度)
  • ✅ 红球、蓝球、金球和地面在银球表面形成清晰倒影
  • ✅ 不同反射率材质对比明显
  • ✅ 阴影与反射正确交互

技术进步

相比之前的 Shadow Ray Tracing 项目:

特性 之前 现在
递归追踪
镜面反射
材质系统 单一 多样化反射率
颜色混合 基础 向量逐分量运算

完整光照模型

1
2
3
4
5
6
✅ 环境光 (Ambient)
✅ 漫反射 (Diffuse)
✅ 镜面高光 (Specular)
✅ 阴影 (Shadow Ray)
✅ 反射 (Reflection) ← 今日新增
⏳ 折射 (Refraction) ← 下一步

学习收获

  1. 递归算法的应用 - 光线反射是经典的递归问题,理解了递归深度控制的重要性
  2. 能量守恒原理 - 反射率决定了直接光照与反射光照的权重分配
  3. 数值稳定性技巧 - 偏移量 1e-4 防止自相交,这是图形学中常见的数值技巧
  4. 性能权衡 - 递归深度对渲染质量和速度有直接影响

下一步计划

  • 折射效果 - 实现透明材质(玻璃球)
  • 抗锯齿 - 多重采样提升图像质量
  • 软阴影 - 面光源代替点光源
  • BVH加速 - 优化光线求交性能

代码仓库

GitHub: daily-coding-practice/2026/02/18-recursive-raytracing-reflection


项目状态: ✅ 完成
输出: 800×600 PNG图像
渲染时间: 约2秒
代码行数: 324行 C++