[光线追踪] 07 -- 物体光源 & 物体的内外部
物体光源
物体光源当然就是会发光的物体, 参考上一篇专栏里的非物体光源, 物体光源应该也要实现 get_direction 和 render_light 两个方法. 同时为了实现 MC 积分, 物体处应该实现物体表面的均匀采样和面积计算, 在获得采样时, 可以顺便获取采样点上的信息 (比如法线和纹理坐标等), 所以实际上如果提供光线来源的点, get_sample 方法应该构建一个完整的 HitRecord. 为此对 Sphere 类新增相应的方法:

光线来源已经储存在 rec.ray.point 上.
然后可以实现物体光源 ObjectLight 类, 因为在渲染光线时, get_direction 和 render_light 必须是同一个点, 所以 ObjectLight 里需要储存下 Object 上的采样. 又因为 get_direction 比 render_light 更早调用, 所以可以在 get_direction 里拿到 Object 的采样, 那么 ObjectLight 类为:

可以看到在 ObjectLight 里调用了材质类的 render_emissive 方法, 为此, 需要实现一个自发光材质 Emissive 类. 因为之前说过, 这里假设光源不会反射颜色, 所以 Emissive 类实现起来比其他材质里简单非常多:

有了自发光材质后, Object 内判断是否为光源的方法可以实现为:



代码到这里就完成了(?), 又是愉快的排列组合时间:

可以看到光源亮度是高达不可思议的 200, 理由跟之前讨论的点光源一样. 下面是渲染结果:

结果是出来了, 但是噪点多得不合理. 实际上这个问题是出在这一行里:

这行是测试在 ray 前方路程 0 ~ t 内是否有物体, 当返回 false 为光源到渲染表面没有物体阻挡. 但根据 hit_anything 的实现, "物体"是包含光源本身的, 并且由于浮点数误差, 实际测试路程可能为 -ε ~ t(1+ε). 为了解决这个问题, 应该把实际测试的路程适当地截断一点: 路程的左端已经在 Sphere::hit 内使用 1e-8 代替 0 解决了, 那么路程右端可以改为 t * (1 - 1e-8), 即

修改后的渲染结果为

可以观察到噪点已经大大减少了
题外话: 在做第二张渲染图的时候发现噪点仍然比 julia 实现的渲染多得多, de 了两个小时 bug 发现是打乱采样集的锅. 把自己实现的打乱换为 std::shuffle 之后噪点大大减少, 但仍然比 julia 的多. 这时候我就非常迷惑了, 本来打乱采样集的目的是减少不同采样集之间的索引相关性, 但是这为什么会增加噪点呢? 另外 julia 实现的里面也有打乱采样集, 但为什么 julia 的噪点就没有增加呢? 已经完全没有头猪了, 摸了.
另外, 球体光源在渲染图里已经变为纯白一片, 这是因为这里使用 Clamp01 处理溢色. 使用 MapTo01 处理的话仍有一个问题, 如下图所示

在渲染图里, 光源的边界不能很好地过渡到黑色背景里 (即类似抗锯齿效果). 产生这个的原因是因为当光线与光源碰撞直接返回光源的颜色值, 而光源的颜色值是比渲染的其他地方高很多的, 从而造成光源本身的颜色直接取代了同一个像素里的其他颜色. 解决起来也是很简单的, 只要在将光源直接返回的颜色使用 MaxTo01 处理就行, 于是可以重写 RayTracer 的方法:




物体的内外部
在数学里物体的"内部"是有准确定义的, 在描述"内部"之前先来看一下表面和法线.
数学上定义"表面"为空间上所有符合 的点集, 也就是说表面为隐函数 f 的等值面. 在表面上某点 p 的法线定义为
, 亦即法线与隐函数的梯度平行并且同向.
计算机图形学里常说"法线从内部指向外部", 那么结合数学上对法线的定义可以得出, 物体的内部即是隐函数 f 值为负数的区域, 外部是 f 为正的区域, 并且两个区域之间被物体表面相隔.
但是实际上, 不只是计算机图形, 部分现实物体都不会严格地把空间分为两个区域 (比如说三角形和纸张). 没有严格分隔内部与外部的面被称为开放表面, 在渲染开放表面时会产生一定问题, 下面先简单实现一个开放表面然后进行渲染.
比较简单的开放表面就是圆面了, 圆面是无限平面的子集, 所以需要定义无限平面: 由平面上的任意一点和其法线定义得到: . 那么圆可以定义为在无限平面上距离圆心一定距离内的子集:
, 为了模型简洁, 可以把圆心限制在平面上. 为了计算光线与平面的相交, 把光线方程代入平面方程得到:
, 整理得
. 那么圆面实现如下:

然后创建一个圆面, 光源, 圆面, 光源交替排列的场景, 相机在两个圆面之间:

在这个场景里, 两个圆面的法线都为 x 轴正方向, 渲染结果如下:

可以看到右边的圆面错误地渲染了右边的光源而不是左边的, 并且如果把右边的光源取消, 右边的圆面甚至无法渲染. 从无限平面处的定义不难知道, 现在可见的右边圆面属于"内部", 这个例子展示了光追不能正确地渲染物体"内部".
解决方法也是很简单, 既然不能正确渲染内部, 那没有内部不就能正确渲染了. 经过刚刚的讲述可以知道, 物体的内外是由法线方向决定的, 当光线照射到物体外部时必定与法线法线相对, 即 , 如下图所示:

类似地, 光线从物体内部照射到表面时结果则相反. 那么只要当光线从物体内部照射到表面时人为地 (本来程序就是人写的就是了) 把法线翻转, 就可以确保内部渲染变为正确的外部渲染. 在 World::hit_object 里增加法线翻转:

修改后再次运行渲染的结果:

渲染就已经正确了.

上面讲到内外部的问题同样会发生在光源上, 为了展示例子, 下面对圆面增加光源相光的方法:

实际上, 因为 this->normal 在光追内应该是常量, 所以 LocalCoord(rec.normal) 也应该是常量, 并且因为 LocalCoord 的计算量比较大, 所以实际上最好可以在光追前就把这个东西计算好, 但这里为了可读性就没这么做了. 话说回来了, 下面是需要进行渲染的场景: 一个大圆面作为地板, 一个圆面光源垂直放在地板上方


可以看到光源只在"外部"有光照, 而在"内部"没有.
把 ObjectLight 复制一份重命名为 DualSideObjectLight, 跟 World:hit_object 里面类似, 重写 DualSideObjectLight::render_light 为

把 scenes5 里的 ObjectLight 替换为 DualSideObjectLight, 渲染结果为:

这里仍然保留 ObjectLight 原行为是因为在部分光源里, 讨论"内部"光照是没必要的 (封闭物体, 比如球体), 并且可以减少计算量, 而且现实里也有很多单面光源.

摸了. 下一篇专栏可以来说一下各种奇形怪状的相机.
项目仓库:https://github.com/nyasyamorina/nyasRT
扣扣涩弔图群: 274767696