Review
通过学习 Radiometry 我们掌握了基于真实物理的光学表达. 然后我们通过对表面反射的分析总结出了基于物理的渲染方程. 现在理论分析已经足够, 剩下的任务就是求解下面的渲染方程, 接下来即将介绍的路径追踪(Path Tracing)就是一种求解渲染方程的有效手段.
文学式编程 Literate Programming
文式编程是Donald Knuth提出的一种编程范式, 在这种编程范式中, 计算机程序以自然语言(如英语)解释其逻辑, 中间穿插着一些宏片段和传统的源代码, 从中可以生成可编译的源代码.
下文中会使用类似的方法, 这一模式在本文中的作用类似于伪代码, 避免行文过程中出现大段代码逻辑进而影响阅读.
利用蒙特卡洛方法求解渲染方程
【路径追踪】数学工具–蒙特卡洛方法(Monte Carlo)
蒙特卡洛方法表明: 当我们需要计算 g(x) 在区间 [a, b] 上的定积分时, 可以转而计算 G(x) 在 n 个随机变量下的平均值, 其中 G(x) = g(x) / PDF(x).
接下来的流程我们先忽略物体的自发光, 渲染方程是一个定义在上半球内对立体角的积分. 我们知道半球所对应的立体角为 2π, 这里我们使用均匀采样, 则可知 PDF(x) = 1 / 2π. 所以我们可以将渲染方程改写成下面的形式:
直接光照
如果我们追踪光路的时候, 只考虑从光源发出的光路, 那么很明显, 我们得到的就是直接光照的结果.
void shade(p, wo)
{
<<随机生成 N 个方向 wi 并以 pdf(w) 分布>>
Lo = 0.0;
For each wi
{
<<追踪一条射线 r(p, wi)>>
<<如果射线 r 与一个光源相交>>
Lo += (1 / N) * L_i * f_r * cosine / pdf(wi)
}
return Lo;
}
间接光照
上面我们得到了直接光照的结果, 但是很明显, 现实中的物体表面接收到的光照并不是全部来自光源. 其他物体反射的光照同样具有重要的作用, 而来自其他物体的光照就是间接光照. 简单来说, 全局光照 = 直接光照 + 间接光照.
根据我们之前介绍 Radiometry 时的分析可知 P 点接收到来自 Q 点的 Radiance 等于 Q 点发射到 P 点的 Radiance. 也就是说 P 点接收到的自 Q 点的 Radiance 就是 Q 点在 QP 方向上的反射, 而这形成了递归的过程.
void shade(p, wo)
{
<<随机生成 N 个方向 wi 并以 pdf(w) 分布>>
Lo = 0.0;
For each wi
{
<<追踪一条射线 r(p, wi)>>
<<IF(射线 r 与一个光源相交)>>
Lo += (1 / N) * L_i * f_r * cosine / pdf(wi);
<<ELSE IF(射线 r 与一个物体相交于 q)>>
Lo += (1 / N) * shade(q, -wi) * f_r * cosine / pdf(wi);
}
return Lo;
}
光线数量爆炸问题
至此我们的实现虽然说是正确的, 但是有一个致命的问题: 每多递归一次就会使光线的数量变为原来的 N 倍. 指数爆炸的计算量即使在离线渲染时依旧是无法接受的. 那么为了避免指数爆炸, 很明显 N 只能取 1.
void shade(p, wo)
{
<<随机生成 1 个方向 wi 并以 pdf(w) 分布>>
<<追踪一条射线 r(p, wi)>>
<<IF(射线 r 与一个光源相交)>>
return L_i * f_r * cosine / pdf(wi);
<<ELSE IF(射线 r 与一个物体相交于 q)>>
return shade(q, -wi) * f_r * cosine / pdf(wi);
}
但是这样同样有明显的问题. 这时虽然光线的数量不会爆炸, 不过随着样本数量的断崖式下降, 画面上的噪点会明显增多. 幸运的是, 这个问题我们可以通过在一个像素上追踪多条光线来解决.
void shadePixel(camPos, pixel)
{
<<在 pixel 上选取 N 个位置样本>>
pixel_radiance = 0.0;
For each sample
{
<<发射一条射线 r(camPos, cam_to_sample))>>
<<IF(射线 r 与场景存在交点))>>
pixel_radiance += (1 / N) * shade(p, cam_to_sample);
}
return pixel_radiance;
}
光线追踪的中止
至此我们实现了 Path Tracing 并解决了光线数量爆炸导致的效率问题, 但是上面的代码依旧存在一个问题, 递归过程无法中止! 只要还存在交点, 追踪就不会停止.
方案1:截断
一个简单的方案是我们限制递归的深度. 但是这个方案的问题是限制递归深度就是限制光线的反弹次数, 限制反弹次数就意味着能量的损失. 比如下图中 3 次反弹和 17 次反弹的对比, 注意图中玻璃容器的位置, 在只计算 3 次反弹的时候这些物体是全黑的. 这背后的道理在于一个封闭的玻璃容器, 光线最少也需要 4 次反弹(折射)才能离开物体.
方案2:Russian Roulette (RR)
俄罗斯轮盘赌是一种利用概率的方法. 我们指定一个概率 P ( 0 < P < 1), 有概率 P 存活, 有概率 (1-P) 被淘汰. RR 方案应用到 Path Tracing 中则为:
- 有概率 P 发射一条光线并返回 Lo / P 作为此次追踪得到的 Radiance
- 有概率 1 - P 追踪过程直接中止
这一方案的意义在于其期望 E = P * (Lo / P) + (1 - P) * 0 = Lo. 也就是我们进行这一随机试验得到的结果的期望与真正的 Radiance 相同.
void shade(p, wo)
{
<<人为指定一个阈值作为 Russian Roulette 的存活概率 P_RR>>
<<在均匀分布在区间[0,1]上的样本中随机选取一个值 ksi>>
<<IF(ksi > P_RR))>>
return 0.0;
<<随机生成 1 个方向 wi 并以 pdf(w) 分布>>
<<追踪一条射线 r(p, wi)>>
<<IF(射线 r 与一个光源相交)>>
return L_i * f_r * cosine / pdf(wi) / P_RR;
<<ELSE IF(射线 r 与一个物体相交于 q)>>
return shade(q, -wi) * f_r * cosine / pdf(wi) / P_RR;
}
效率改进: 采样光源
这时我们实际上已经有一个相对完整的 Path Tracing 了, 但是他还隐含着一个问题是当光源很小的时候, 追踪的光线会出现大量的浪费.
好在蒙特卡洛允许使用任意的采样, 所以我们可以通过在光源上采样来避免光线的浪费. 不过需要注意的是蒙特卡洛积分要求: 对 x 采样就只能在 x 上积分. 我们将采样对象从微分立体角转换成了光源表面上的微面元, 所以积分对象也需要做相应的改变. 这可以通过简单的三角函数计算得到.
我们之前假设在半球内, 光源"意外"的照亮这一点. 但是现在我们将采样转移到了光源上, 于是辐射度可以被转化为两部分:
- 来自光源的部分, 也就是直接光照(没有必要使用 RR)
- 来自其他物体反射的部分, 也就是间接光照(需要使用 RR)
void shade(p, wo)
{
Spectrum L;
// 来自光源的贡献
<<在光源上均匀采样一个点 x, pdf_light = 1 / LightArea>>
L += L_i * f_r * cosine1 * cosine2 / |x - p|^2 / pdf_light;
// 来自反射的贡献
<<人为指定一个阈值作为 Russian Roulette 的存活概率 P_RR>>
<<在均匀分布在区间[0,1]上的样本中随机选取一个值 ksi>>
<<IF(ksi > P_RR))>>
return L;
<<随机生成 1 个方向 wi 并以 pdf(w) 分布>>
<<追踪一条射线 r(p, wi)>>
<<IF(射线 r 与一个物体相交于 q)>>
L += shade(q, -wi) * f_r * cosine1 / pdf(wi) / P_RR;
return L;
}
最后一点点工作: 阴影 Shadow
我们研究到现在还没有考虑光线被其他物体遮挡的问题, 这一点非常简单, 在计算直接光照之前先检查光线是否与其他物体相交即可.
渲染结果
Demo 中只有最基础的框架, 只添加了最简单的最简单的均匀漫反射材质, 所以场景比较单调~ 但是依旧可以看到, 光线追踪类方法的实现原理可以轻松做到光栅化方法难以实现的诸如间接光照、软阴影等效果.
现在我遇到的一个未解决的问题是: 直接光照我们可以通过采样光源得到噪声相当小的结果, 但是对于间接光照来说却没有可以采样的"间接光源". 这导致的问题是场景中非常容易出现高亮的噪点(萤火虫效应 fireflies), 现在的 Demo 中通过大量的采样来消减这一误差, 如果大家有什么更好的方法, 还望不吝赐教.
参考资料
- 全局光照算法技术(第2版) (Advanced Global Illumination, 2nd Edition) Philip Dutre等著, 黄刚译
- Physically Based Rendering: From Theory To Implementation
- GAMES101-现代计算机图形学入门-闫令琪 Lecture 16 Ray Tracing 4