这是一个基于 Dartmouth Introductory Ray Tracer(DIRT)框架、使用 C++ 编写的基于物理的离线渲染器,是 CMU《基于物理的渲染》课程的项目。在一个学期里,我实现了完整的光传输技术栈——从 Whitted 风格的光线追踪,到带多重重要性采样(MIS)的蒙特卡洛路径追踪、体积渲染,并最终以双向路径追踪(BDPT)配合 balance heuristic 的 MIS 权重方案作为期末项目。
关于源码访问的说明: 由于这是受学术诚信政策约束的课程作业,仓库为私有,无法公开完整源码。核心算法、代码摘录和设计决策都记录在本页中,完整报告附在页面末尾。

为什么需要双向路径追踪
单向路径追踪器的每条路径都从相机出发。当光照容易被"碰巧"采样到时,这种方式表现良好;但在间接、难以到达的光照占主导的场景里——小面积光源、透过狭窄开口进入的光线、或 specular-diffuse-specular 的焦散链——大多数相机路径在触及光源之前就终止了,几乎所有采样都被浪费,图像始终充满噪点。
BDPT 从两端同时进攻这个问题:
- 从视点出发追踪一条相机子路径。
- 从光源上采样一个点,追踪一条光源子路径。
- 将一条子路径的每个前缀与另一条子路径的每个前缀连接,得到一整族估计器——每个 (s, t) 组合都是对同一路径长度的不同采样策略。
- 用 MIS 权重组合所有策略,让每个策略只在自己方差低的区域贡献。
MIS 这一步不是可选项。朴素地把所有连接策略求和会对同一光传输重复计数,产生严重的 firefly 噪点。下面的对比最直观地说明了 MIS 的价值:
| 朴素连接(无 MIS) | 加入 MIS 权重 | 路径追踪参考图 |
|---|---|---|
![]() | ![]() | ![]() |
渲染器架构
渲染器是一个由 JSON 场景描述驱动的模块化 C++ 系统,每个子系统都通过工厂模式的解析器可插拔:
- 积分器 —— 法线 / AO 调试视图、纯 BSDF 路径追踪、下一事件估计(NEE)、MIS 路径追踪(power heuristic)、体积路径追踪(uniform 与 NEE+MIS),以及 BDPT(balance heuristic)。
- BSDF —— Lambertian、金属(粗糙反射)、带 Fresnel 的电介质、Phong / Blinn-Phong 波瓣、带法线重要性采样的 Beckmann 微表面、Oren-Nayar 粗糙漫反射,以及纹理驱动的混合材质。所有 BSDF 都暴露统一的
sample / eval / pdf接口和 specular 标志——正是这个设计让同一套材质库能同时服务于 PT、MIS-PT 和 BDPT。 - 光源 —— 支持
sampleOn / pdfOn立体角采样的四边形与球面面光源,以及为期末场景添加的 delta 点光源和聚光灯(带锥角衰减)。 - 加速结构 —— 递归中点分割的包围盒层次结构(BVH)。
- 介质 —— 均匀介质与 Perlin 噪声异构介质,配合 Henyey-Greenstein 相位函数,通过 free-flight 距离采样和 ratio tracking 求解。
- 采样器 —— 独立采样、分层采样、Halton 低差异序列。
- 相机 —— 薄透镜模型,可配置光圈与焦距实现景深。
单向路径追踪中的 MIS
在 BDPT 之前,MIS 路径追踪器在每次弹射时用 power heuristic 组合 BSDF 采样和下一事件估计(光源采样):
C++float powerHeuristic(float pdf1, float pdf2, float power) {
float pow1 = pow(pdf1, power);
float pow2 = pow(pdf2, power);
return pow1 / (pow1 + pow2);
}
每个策略都会对自己采样出的方向计算另一个策略的 PDF,据此加权自己的贡献;权重会沿递归传递,保证自发光辐射不被重复计数。Specular 弹射完全绕过加权(delta 分布无法被另一策略采样,权重为 1)。经典的 Veach 场景在整个粗糙度范围内都能正确渲染:


BDPT 深入解析
子路径生成
两条子路径都是随机游走,在每个顶点记录 MIS 之后需要的数据:正向 PDF(该游走实际生成此顶点的概率)和反向 PDF(相反方向的游走生成此顶点的概率),以及累积的 throughput。
C++struct Vertex {
Vec3f wi, wo; // 入射 / 出射方向
HitInfo hit; // 表面交互信息
Color3f emitted; // 沿游走累积的自发光
Color3f throughput; // 到达此顶点的路径 throughput
Color3f nextThroughput; // 在此顶点散射后的 throughput
float pdfFwd; // 累积正向采样 PDF
float pdfRev; // 累积反向采样 PDF
};
相机游走在每个表面上通过 BSDF 的重要性采样散射,并一路累积两个方向的 PDF 乘积。Specular 顶点只把 throughput 乘以 delta 衰减而不更新 PDF;漫反射顶点则同时追踪两个方向:
C++// traceCameraPath() 内部:逐弹射的 PDF 记账
float pdfMat = hit.mat->pdf(currentRay.d, normalize(srec.scattered), hit);
vertex.pdfFwd = pdfFwd;
pdfFwd *= pdfMat; // 正向:视点 -> 场景
pdfRev *= hit.mat->pdf(-srec.scattered, normalize(-currentRay.d), hit); // 反向游走
vertex.pdfRev = pdfRev;
Color3f evalMat = hit.mat->eval(currentRay.d, normalize(srec.scattered), hit);
throughput *= evalMat / pdfMat;
光源游走首先在光源上同时采样一个位置和一个出射方向(initVertex.pdfFwd = pdfDir * lightPDF),之后的扩展方式与相机游走相同。两条游走都以 throughput 亮度为概率执行俄罗斯轮盘终止,在限制路径长度的同时保持估计器无偏。
连接子路径
主循环枚举弹射预算内所有合法的 (s, t) 策略:
C++Color3f BDPTIntegrator::Li(const Scene &scene, Sampler &sampler, const Ray3f &ray) const {
Color3f L(0.f);
vector<Vertex> lightPath = traceLightPath(scene, sampler);
vector<Vertex> cameraPath = traceCameraPath(scene, sampler, ray);
for (int s = 1; s <= cameraPath.size(); ++s) {
for (int t = 1; t <= lightPath.size(); ++t) {
int depth = t + s - 2;
if ((s == 1 && t == 1) || depth < 0 || depth > m_maxBounces) continue;
Color3f contribution = connect(scene, cameraPath, lightPath, s - 1, t - 1);
float misWeight = calculateMIS(cameraPath, lightPath, s, t);
L += contribution * misWeight;
}
}
return L;
}
一次连接会在两个端点顶点之间投射 shadow ray 做可见性测试,然后把两条子路径的 throughput、两个端点处朝连接方向求值的 BSDF、光源项,以及在立体角与面积测度之间转换的几何项相乘:
C++float geometryTerm(const Vec3f &nx, const Vec3f &x, const Vec3f &ny, const Vec3f &y) {
Vec3f d = y - x;
float cosThetaI = abs(dot(normalize(nx), normalize(d)));
float cosThetaO = abs(dot(normalize(ny), normalize(-d)));
return cosThetaI * cosThetaO / length2(d);
}
// connect():一般 (s, t) 策略
Color3f bsdfCam = cameraVertex.hit.mat->eval(cameraVertex.wi, normalize(dir), cameraVertex.hit);
Color3f bsdfLight = lightVertex.hit.mat->eval(lightVertex.wo, normalize(dir), lightVertex.hit);
Color3f result = lightVertex.throughput * cameraVertex.throughput;
result *= initLight.emitted / initLight.pdfFwd; // 光源辐射 / 采样 PDF
result *= bsdfCam * bsdfLight;
result *= geometryTerm(cameraVertex.hit.sn, cameraVertex.hit.p,
lightVertex.hit.sn, lightVertex.hit.p);
跨策略的 MIS 权重
对于给定长度的完整路径,每个 (s, t) 切分都是对同一条路径的不同采样方式。所选策略的 balance heuristic 权重是它的 PDF 除以所有策略 PDF 之和:
实现上不需要显式构造每个假想策略的 PDF,而是采用 Veach 论文中(并由 PBRT 推广的)递推形式:沿每条子路径累积反向 PDF 与正向 PDF 的比值,表达"相反方向的游走生成这个顶点的可能性"相对于它实际被生成方式的比例:
C++float BDPTIntegrator::calculateMIS(const vector<Vertex> &camPath,
const vector<Vertex> &lightPath,
size_t s, size_t t) const {
if (s == 1 && t == 1) return 1.f;
float sumCamera = 1.f, sumLight = 1.f;
for (int i = 1; i <= s; i++)
sumCamera = (remap0(camPath[i].pdfRev) / remap0(camPath[i].pdfFwd)) * (sumCamera + 1.f);
for (int i = 1; i <= t; i++)
sumLight = (remap0(lightPath[i].pdfRev) / remap0(lightPath[i].pdfFwd)) * (sumLight + 1.f);
return 1.f / (sumLight + 1.f + sumCamera);
}
remap0() 把接近零的 PDF(delta 交互)钳制为 1,让它们从比值中消去而不是除以零——这是递推中处理 specular 顶点的标准做法。
Delta 光源
点光源和聚光灯(可配置锥角与衰减)是为期末场景添加的。Delta 光源无法被随机游走命中,它们的贡献只能通过显式连接策略流入——这是检验 MIS 记账是否正确处理"不可采样策略"的一个很好的压力测试。
| 点光源 | 带衰减的聚光灯 |
|---|---|
![]() | ![]() |
通往 BDPT 之路:功能演进
期末项目建立在一整个学期的渲染器开发之上:
基础 —— 光线与图元求交(球体、四边形、Möller-Trumbore 三角形、OBJ 网格)、实例化变换、递归 Whitted 光线追踪、程序化与图像纹理、BVH 构建。

蒙特卡洛与 BSDF —— cosine 加权、Phong 波瓣、Beckmann 法线重要性采样,每种都用直方图对比验证了解析 PDF 的正确性;分层与 Halton 采样器;薄透镜景深。
| Beckmann 微表面 | 景深 |
|---|---|
![]() | ![]() |
直接光照与 MIS —— 立体角 PDF 的面光源采样、下一事件估计、power heuristic MIS、HDR 环境光照。
体积渲染 —— 解析反演采样的 Henyey-Greenstein 相位函数、指数 free-flight 采样的均匀介质、ratio tracking 的异构 Perlin 介质,以及介质交互处相位函数采样与光源采样之间的 MIS。
| 各向同性体积,NEE | 穿过参与介质的聚光灯 |
|---|---|
![]() | ![]() |
这个项目展示了什么
- 光传输理论的实践 —— 推导并实现了渲染方程的四种求解策略(Whitted、PT、MIS-PT、BDPT),并精确理解每种策略的方差来源。
- 两套 MIS 体系 —— 两策略(BSDF × NEE)上的 power heuristic,以及通过正向/反向 PDF 递推覆盖整个策略族的 balance heuristic。
- 采样功力 —— 每个 BSDF 波瓣的重要性采样、立体角光源采样、低差异序列、无偏俄罗斯轮盘,以及对照真值的 PDF 验证。
- 渲染器工程 —— 让一套材质库服务所有积分器的统一 BSDF 接口、工厂驱动的场景解析,以及让期末场景可渲染的 BVH。
完整技术报告(含推导与更多对比):
BDPT 最终报告Open ↗








