体积光渲染:实现真实的God Rays效果(C++)
前言
体积光(Volumetric Lighting),也称为”God Rays”或”丁达尔效应”,是指光线穿过有雾气、灰尘或水汽的空间时,光束本身变得可见的现象。这是游戏和电影中常用的视觉效果,能够极大地增强场景的真实感和氛围感。
今天我们用C++实现这个效果,不依赖任何图形库,纯CPU渲染。
效果展示

- 左图:普通渲染(只有表面光照)
- 中图:体积光渲染(光束清晰可见)
- 右图:差异热力图(差异均值39.33)
可以看到,光束从右上方的光源穿过三个球体之间的缝隙,形成了经典的”God Rays”效果。
技术原理
什么是体积光?
在真实世界中,我们之所以能看到光束,是因为:
- 散射(Scattering):光子碰到空气中的微粒(雾气、灰尘、水汽)后向各个方向散射
- 吸收(Absorption):部分光能被微粒吸收
- 方向性:散射后的光子可能射向我们的眼睛,让我们”看到”光束
Ray Marching算法
实现体积光的关键是Ray Marching(光线步进):
1 | 1. 从相机发射一条射线 |
用公式表示:
$$
L_{total} = \sum_{i=0}^{N} L_{scatter}(p_i) \cdot T(p_i) \cdot \Delta t
$$
其中:
- $L_{scatter}(p_i)$:采样点 $p_i$ 的散射光强度
- $T(p_i)$:透射率(考虑遮挡和衰减)
- $\Delta t$:步长
- $N$:步进次数
核心代码实现
1. 体积光计算函数
1 | Vec3 volumetric_light(const Ray& ray, const Scene& scene, double max_dist) { |
2. 阴影测试(遮挡检测)
1 | bool is_occluded(const Vec3& point, const Vec3& light_dir, double light_dist) const { |
3. 主渲染循环
1 | for (int j = 0; j < HEIGHT; j++) { |
关键参数调优
1. 散射系数(SCATTERING)
控制雾气的密度,越大光束越明显,但也容易过曝。
| 值 | 效果 |
|---|---|
| 0.01 | 微弱,几乎看不见 |
| 0.03 | 适中,清晰可见 ✅ |
| 0.08 | 较强,开始过亮 |
| 0.20 | 过强,图片接近全白 |
2. 步进次数(NUM_STEPS)
平衡性能和质量:
| 值 | 渲染时间 | 效果 |
|---|---|---|
| 32 | 0.7秒 | 有条纹感 |
| 60 | 1.4秒 | 平滑自然 ✅ |
| 100 | 2.3秒 | 提升不明显 |
3. 光照强度
1 | light_color = Vec3(1.0, 0.95, 0.85) * 1.2; // 1.2倍亮度 |
太强会导致过曝,太弱则光束不明显。
4. 衰减函数
1 | // 距离平方衰减(物理准确) |
距离平方衰减更符合物理规律,但也需要更高的光强来补偿。
技术挑战与解决
挑战1:过曝问题
现象:初始渲染结果全白(RGB均值255)
原因:
- 散射系数过大(0.2)
- 光照强度过高(3.0倍)
- 累积的散射光超过了1.0的上限
解决:
1 | // 降低散射系数 |
挑战2:性能优化
目标:保持效果的同时缩短渲染时间
方法:
- 减少步进次数(80 → 60)
- 提前退出:如果透射率太低,停止累积
- 使用
-O3编译优化
效果:1200×800分辨率,1.4秒完成
挑战3:平衡光束与场景
如果体积光太强,会掩盖场景本身的细节;太弱则看不出效果。
最终平衡:
- 表面光照均值:17.06(深色背景)
- 体积光均值:56.40(光束清晰)
- 差异均值:39.33(效果明显但不过分)
扩展思路
1. 彩色光源
1 | // 暖色光源(日落) |
2. 多光源混合
1 | Vec3 total_scatter(0, 0, 0); |
3. 时变效果
1 | // 光束随时间移动 |
4. 后处理方法
除了Ray Marching,还可以用屏幕空间的径向模糊实现类似效果(性能更高,但不如Ray Marching准确)。
性能对比
| 方法 | 分辨率 | 时间 | 质量 |
|---|---|---|---|
| Ray Marching (60步) | 1200×800 | 1.4s | 高 ✅ |
| Ray Marching (32步) | 1200×800 | 0.7s | 中 |
| 后处理径向模糊 | 1200×800 | 0.2s | 中低 |
对于离线渲染(如本项目),Ray Marching是最佳选择。对于实时渲染(游戏),通常会用GPU实现Ray Marching或后处理方法。
数学推导(可选)
散射方程
体积渲染方程(简化版):
$$
L(s) = L_0 \cdot T(s) + \int_0^s \sigma_s L_i(t) T(t) dt
$$
其中:
- $L(s)$:到达相机的光强
- $L_0$:背景光
- $T(s)$:透射率,$T(s) = e^{-\int_0^s \sigma_t dt}$
- $\sigma_s$:散射系数
- $\sigma_t$:消光系数(散射+吸收)
- $L_i(t)$:入射光强
离散化后:
$$
L \approx \sum_{i=0}^N \sigma_s L_i(t_i) e^{-\sigma_t t_i} \Delta t
$$
这就是我们代码中的累积公式。
总结
通过这个项目,我们:
- ✅ 实现了真实的体积光效果(丁达尔效应/God Rays)
- ✅ 掌握了Ray Marching算法(光线步进采样)
- ✅ 理解了散射、遮挡、衰减的物理原理
- ✅ 学会了参数调优(散射系数、步进数、光照强度)
- ✅ 解决了过曝问题(从全白到合理的明暗对比)
最终效果:
- 差异均值:39.33(效果明显)
- 渲染时间:1.4秒(1200×800)
- 光束清晰可见,穿过球体间隙形成God Rays
项目信息
- 完整代码:GitHub - daily-coding-practice/2026/02/02-24-Volumetric-Lighting
- 编译运行:
1
2g++ -std=c++17 -O3 god_rays_v2.cpp -o god_rays_v2
./god_rays_v2 - 输出图片:
scene_no_vol.png,scene_with_vol.png
参考资料
- GPU Gems 3: Volumetric Light Scattering
- Scratchapixel - Volume Rendering
- Real-Time Rendering 4th Edition
下期预告:可能会尝试流体模拟(SPH)或布料模拟,敬请期待!










