本节数学符号较多,高能预警

引言

上一节我们完成了全局光照路径追踪算法的实现,并制作了三种最基础的材质实例。这一节我们主要实现抽象光源以及对光源的直接采样。

Part0. bug修正

在上一节的末尾,我对若干场景进行了测试,整体效果较好,但是有一些明显的bug需要修正。首先是在Raygen shader中,若光线弹射次数超过了预设值,应该直接break循环,而不需要给radiance置0。如果给radiance置0,那么路径中自发光物体的光照贡献也会被一并清除,自发光就失去意义了。

for (int bounces = 0; ; ++bounces)
{
     if (bounces >= optixLaunchParams.maxBounce) {
     //radiance = 0.0f;
     break;
}

另一个问题是导入模型的问题,需要对knownvertices调整一下位置,否则在多材质的场景中可能会出现异常的三角面:

for (int materialID : materialIDs) {
     if (materialID == -1)
          continue;
     std::map<tinyobj::index_t, int> knownVertices;
...

Part1. 直接光采样理论

之前的场景基本上都是开放或半开放的场景。现在我们考虑一下全封闭的场景,全封闭意味着外面的环境贴图无法提供任何光照,那么此时我们就只能以封闭场景中物体的自发光作为光源。然而当我们创建了一个自发光强度极强的球体放在封闭场景中央,整个场景依旧漆黑无比:

究其原因是,我们场景中大部分物体是漫反射,其散射方向完全随机,这就导致K次采样中只有寥寥无几的几次(甚至0次)击中到自发光上。换言之,正常随机采样很有可能每次都求交不到有自发光的物体上,而且光源越小出现这种悲剧的概率越大,这样最终场景就会非常昏暗。

这里有一个解决思路:将直接光(光源)和间接光(其他物体弹射的光源)分开采样。直接光采直接光的,非直接光采非直接光的,最后将两者加在一起就成了最终的结果。这样就可以保证每次采样不会错过直接光的贡献。

至于如何分开采样,我们依旧回到渲染方程:

$$L(x→out)=Emission + \int_\omega^\omega f_r(x,in→out)L(x←in)cos(N,in)d\omega$$

根据蒙特卡洛积分原理,可以将积分写成采样/pdf的形式:

$$L(x→out)=Emission + \frac{1}{K}\sum \frac{f_r(x,in→out)L(x←in)cos(N,in)}{pdf}$$

这里的L(x←in)指的是从外界发射进来的所有光线的采样,那么我们就可以将其拆解为直接光和间接光部分:

$$L(x→out)=E + \frac{1}{K}\sum (\frac{f_r(x,in→out)L(x←光源)cos(N,in)}{pdf} + \frac{f_r(x,in→out)L(x←表面)cos(N,in)}{pdf})$$

由于路径追踪算法中,当光线击中某个表面后,不需要反复采样取平均(否则会出现光线数量指数级爆炸),因此可以将求和取平均的符号去掉,那么此时计算公式就变成了:

$$L(x→out)=E + (\frac{f_r(x,in→out)L(x←直接光)cos(N,in)}{pdf} + \frac{f_r(x,in→out)L(x←间接光)cos(N,in)}{pdf})$$

这里埋藏了一个坑,那就是:两项的pdf是否是同一个pdf?按照咱们的推导来看,pdf确实是相同的,但是我们实际采样的时候,光源会有一套采样方案(即根据光源外表形状决定的采样方案),而非直接光的采样又会用另外一套方案,因此此时两者的pdf就是不同的了。

现在看这个公式有点太臃肿了,我们令:

$$F_{C_1} = \frac{f_C * cos(N,直接光)}{pdf},F_{C_2} = \frac{f_C * cos(N,间接光)}{pdf}$$

则有:

$$L_C=E_C + F_{C_1} * L_{直接光} + F_{C_2}*L_{间接光}$$

所以我们的思路就变成了:当求交到一个物体表面后,先去场景中找直接光源求交,获得直接光照后*bsdf*cosine/pdf_光源;再去场景中找物体表面求交,获得间接光照后*bsdf*cosine/pdf_bsdf;将两者加在一起,就是本次采样得到的总能量了。(相当于每次求交后要采样两次,一次直接光,一次间接光)

对于递归型path-tracing程序而言,算法其实很明确,这在games101的ray-tracing第四讲有展示过: