Whitted-Style 光线追踪的问题
Whitted-Style 风格的光线追踪特点:
- 总是计算镜面反射和折射。
- 在漫反射表面上光线不会反弹。
但这些假设是有问题的,因此需要引入路径追踪。

左边像镜子,右边像磨砂金属。
左边是完全的镜像反射 Mirror reflection
,右边是 Glossy reflection
。

一根光线从上面打到了一个漫反射物体表面,光线就不再弹射了,这导致如图左侧所示的黑色阴影区域。
如右图物体表面呈现红色区域,说明红色的墙面的颜色跑到了物体表面上,这种现象称之为color bleeding
。这是因为光线打到了墙面后,光线又反弹到了物体表面上,说明了全局光照会反弹不止一次。
如果没有全局光照,天花板就是黑的,物体侧面就是黑的。
Whitted-Style 光线追踪的错误

所以 Whitted-Style 风格的光线追踪是错误的,渲染方程是正确的。
如果需要正确计算物体表面上一个点打到的光线,则需要求解渲染方程。
但是求解它,需要计算半球上的积分,这是一个递归的执行流程。
这个积分式可以使用蒙特拉洛积分的方式来求解。
蒙特拉洛方式求解渲染方程
直接光照的情形
假设我们想只用直接光照要渲染一个像素点。

如图上面是一个面光源,左侧有一个摄像机,右侧有一个漫反射物体。w0是着色点发出光线到摄像机的方向,wi是着色点接收到来自四面八方光线的方向。

假设着色点没有自发光项,那么渲染方程就变为反射方程
这仍然是需要求解一个球面上的积分,所以我们可以使用蒙特拉洛积分的方式来求解。
计算 p 点到相机的 radiance(亮度):
Lo(wo)=∫Ω+Li(p,wi)fr(p,wi,wo)(n⋅wi)dwi
根据蒙特卡洛积分公式:
∫abf(x)dx≈N1k=1∑Np(Xk)f(Xk),Xk∼p(x)
其中的 f(x) 对应上面的渲染方程中的被积函数:
f(wi)=Li(p,wi)fr(p,wi,wo)(n⋅wi)
概率密度函数 p(x) 为:
p(wi)=2π1
假设我们是在半球面上均匀地采样,一个球体的表面积是 4π,半球面是 2π。所以概率密度函数为 2π1。
所以最终,渲染方程的蒙特拉洛近似为:
Lo(p,wo)≈N1k=1∑Np(wik)Li(p,wik)fr(p,wik,wo)(n⋅wik)

写成算法伪代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13
| shade(p, w_o) { L_o = 0.0 for k = 1 to N { w_i = sampleHemisphere() r = ray(p, w_i) if (r.hit(light)) { L_i = r.radiance(light) f_r = brdf(p, w_i, w_o) L_o += (1 / N) * L_i * f_r * dot(n, w_i) / pdf(w_i) } } return L_o }
|
全局光照的情形
对于全局光照的情形,着色点 p 点的入射光 Li(p,wi) 也可能来自其他物体 q 的反射过来的光,所以需要递归地计算。

如图所示,p 点接收到来自 q 点的反射光,就好像是相机在 q 点拍摄到的来自 q 点的直接光照的光线一样。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| shade(p, w_o) { L_o = 0.0 for k = 1 to N { w_i = sampleHemisphere() r = ray(p, w_i) if (r.hit(light)) { L_i = r.radiance(light) f_r = brdf(p, w_i, w_o) L_o += (1 / N) * L_i * f_r * dot(n, w_i) / pdf(w_i) } else if (r.hit(object)) { q = r.hitPoint() w_o_new = -w_i L_i = shade(q, w_o_new) f_r = brdf(p, w_i, w_o) L_o += (1 / N) * L_i * f_r * dot(n, w_i) / pdf(w_i) } } return L_o }
|
问题: 光线弹射指数级爆炸

如图一份光线打到一个着色点,反射出 100 条光线,每条光线又打到另一个着色点,又反射出 10000 条光线,继续反射下一次将会是 100 万条光线,这样指数级增长的光线数量是无法承受的。
当 N=1 时,1 的任何次方都是 1,所以每次只是用一根光线进行采样,就不会出现指数级爆炸的问题。
伪代码编写如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| shade(p, w_o) { w_i = sampleHemisphere() r = ray(p, w_i) if (r.hit(light)) { L_i = r.radiance(light) f_r = brdf(p, w_i, w_o) return L_i * f_r * dot(n, w_i) / pdf(w_i) } else if (r.hit(object)) { q = r.hitPoint() w_o_new = -w_i L_i = shade(q, w_o_new) f_r = brdf(p, w_i, w_o) return L_i * f_r * dot(n, w_i) / pdf(w_i) } }
|
当N=1
时,算法就叫做路径追踪(Path Tracing)。
当N>1
时,算法就叫做分布式路径追踪(Distributed Path Tracing)。

问题: 采样噪点
但是 N=1 时,图像会非常噪点,因为每个像素只采样了一条光线。所以一种解决方案是对每个像素进行多次采样,然后对这些采样结果进行平均,即为Ray Generation
。
伪代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| ray_generation(camera_pos, pixel) { pixel_radiance = 0.0 for i = 1 to N { sample_point = samplePixel(pixel) w_o = computeRayDirection(camera_pos, sample_point) r = ray(camera_pos, w_o) if (r.hit(object)) { p = r.hitPoint() L_o = shade(p, -w_o) pixel_radiance += (1 / N) * L_o } } return pixel_radiance }
|
问题: 递归终止条件
shade 函数是递归调用的,如果没有终止条件,递归会一直进行下去,直到栈溢出。
如果直接暴力限制光线是一个固定的弹射次数,比如 5 次,那么光线在 5 次弹射后就会直接终止,这样会导致光线在某些情况下过早终止,将会损失能量,渲染结果是不对的。
在真实情况下,光线会弹射无数次,但是在计算机中无法做到这一点。
俄罗斯轮盘赌 Russian Roulette(RR) 方法
一种解决方案是俄罗斯轮盘赌(Russian Roulette)方法,简称 RR 方法。
俄罗斯轮盘赌核心与概率有关。若存活概率为 $ 0 < P < 1 $,则有机会安全;若概率为 $ 1 - P $,则会面临危险情况。比如,当左轮手枪中有两颗子弹时,存活概率 $ P = 4/6 $。
此前,我们总是向着色点发射一条光线,以获取着色结果 $ L_o $。
假设我们手动设置一个概率 $ P ( 0 < P < 1 $):
- 以概率 $ P $,发射一条光线,并返回着色结果除以 $ P $,即 $ L_o / P $;
- 以概率 $ 1 - P $,不发射光线,结果为 $ 0 $。
通过这种方式,期望结果仍为 $ L_o $,计算式为:
$ E = P \times (L_o / P) + (1 - P) \times 0 = L_o $。
伪代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| shade(p, w_o) { P_RR = 0.8 ksi = random(0, 1) if (ksi > P_RR) { return 0.0 } w_i = sampleHemisphere() r = ray(p, w_i) if (r.hit(light)) { L_i = r.radiance(light) f_r = brdf(p, w_i, w_o) return L_i * f_r * dot(n, w_i) / pdf(w_i) / P_RR } else if (r.hit(object)) { q = r.hitPoint() w_o_new = -w_i L_i = shade(q, w_o_new) f_r = brdf(p, w_i, w_o) return L_i * f_r * dot(n, w_i) / pdf(w_i) / P_RR } }
|
问题: 渲染效率较低

现在我们已经得到了一个正确的路径追踪算法了,但是它并不高效。
如图,当 SPP(Samples Per Pixel)较小时,图像会非常噪点;当 SPP 较大时,图像才会变得平滑。
对光源采样
一种提高效率的方式是对光源进行采样。

当在着色点对半球进行均匀采样时,如图可能每 5 条光线、500 条光线、50000 条光线里,才会有 1 条光线能击中光源。这就导致大量光线被“浪费”,这种采样方式效率低下。