[光线追踪] 06 -- BRDF类 & 光源类
在上一篇专栏里实现了第一个可以运行的光追例子, 这篇专栏将会分离 Material 和 BRDF, 并且实现两种常见的场景光源.

Sphere::hit 的化简
上一篇专栏里忘记了一件比较重要的事情, 这里专门分开一小节来讲.
求解光线与球体碰撞是求解方程 , 求解一元二次方程的公式是:
, 上下同除以 2 可以化简 b 为 half_b, 并且不需要额外乘 4. 另外, 因为强制要求 Ray::direction 应该是归一化的, 那么
. 根据这些信息可以化简 Sphere::hit 为

同理 Sphere::hit_record 里也一样.

分离 BRDF
在上一篇专栏里实现了哑光材质 Matte 类, 并在 Matte 里实现了 Lambertian 模型的半球渲染. 但是我们需要的渲染模型 , 包含了半球渲染和面积渲染两项, 并且这两项应该是等价的.
由之前的讨论可以得知, 一个 BRDF 应该在半球渲染 Kₕ 里实现采样, 而在面积渲染 Kₛ 里给出 fᵣ. 那么可以做一个 BRDF 类同时提供相应的采样和 fᵣ:

函数 fᵣ 需要局部坐标系的方向 ωᵢ 和 ωₒ, 而 Hitrecord 里包含全局坐标的 ωₒ (-ray.direcion) 和表面法线 normal, 那么额外输入全局坐标的 ωᵢ 就可以通过法线计算出相应的局部坐标下的 ωᵢ 和 ωₒ. 在 Sampler 里返回的采样是在局部坐标, 所以需要法线转换为全局坐标.
至少这里的程序, 只需要一个法线就可以完成局部坐标与全局坐标的转换. 当只存在法线时, 立体角里的 θ 是可以求出的, 但 φ 不能, 也就是说这里的 BRDF 与方向角 φ 无关, 这种 BRDF 被称为各向同性 BRDF. 为了实现各向异性 BRDF, 就必须求出 φ, 亦即需要碰撞点处的纹理坐标系, 程序修改起来也是比较简单的: 在 HitRecord 里记录局部纹理坐标系, 但这个专栏内并没有这样的打算.
实现了 BRDF 的模板类后, 就可以把 Lambertian 模型从 Matte 材质类里分离出来:

因为 Lambertian 模型的采样必须要求是采样集中度为 1 的半球采样, 所以这里选择输入 Sampler 的参数给 Lambertian 的初始化方法, 而不是直接输入 Samplerp.
在分离了 BRDF 后, Matte 类里需要指定 BRDF, 但 BRDF 是通用的, 所以如果输入任意一个 BRDF 的话可能就不是 Matte 了, 所以这里可以实现一个通用的不透明材质 Opaque:


光源
在实现物体光源前, 先来介绍两个不是物体的光源: 平行光和点光源. 平行光可以看作从无限远处的光源, 这意味着在场景内任何地方光线都是从一个方向射入的. 点光源可以看作无限小的球体光源.
因为光源需要使用面积模型进行渲染, 而在面积模型里入射光线是从光源处得出的, 所以只需给出光源照射到物体的位置, 光源处的 HitRecord 就可以由光源本身给出. 因为最后 HitRecord 都是被光源本身内部消化的, 所以干脆光源只给出一个方法 render_light 就足够了.
看到面积模型: , 仅有
和
两项是依靠入射方向 ωᵢ 求出的, 并且恰好这两项是发生在 p 点上的. 而
和
两项是发生在 q 点上.
和
两项则是发生在从 q 传播到 p 的路上. 那么分配计算时, 可以把发生在 p 点上的项放在 Material 里计算, 而剩下的项放在光源里计算, 也就是说光源仍需要提供一个方法计算 ωᵢ. 那么光源类实现为下:

其中, 如果当 has_shadow 为 false 时, v(p, q) 永远返回 1, 并且这个值永远初始化为 true.
对于平行光和点光源来说, 它们的面积都为 0, 所以对它们进行的渲染不应该叫做 MC 积分了, 尽管如此, 但是代码逻辑是没有变化的. 在面积模型里的 可以看作平方反比定律的体现, 但是平行光是不满足平方反比定律的, 所以平行光的这一项可以直接删掉, 那么平行光类实现如下

可以看到在平行光里 get_direction 没用上 Vec3 参数, 这说明平行光对场景里所有地方都是一致的, 并且 direction 表示光的传播方向, 因为这里使用反向光追, 所以 get_direction 方法返回的是 direction 的反向.
类似地, 点光源可以实现如下

至于物体光源就可以留到下一篇专栏里了 (就嗯拖).

渲染部分
因为 Light 不同于 Object, 所以在 World 里需要另外一个 vector 存放光源:
并且需要在 Material 里实现相应的面积模型:



至此代码已经完整, 接下来又是愉快的排列组合环节:

渲染结果如下

在代码以及渲染结果里有几个值得注意的地方: 第一个就是因为平行光和点光源的面积为无限小, 所以是不可见的, 又所以习惯在点光源的位置放置一个渲染方式不太一样的小球体展示点光源, 如下图所示 (相关代码是在 render_figure 里被注释掉的部分, 但这代码完全没考虑被遮挡的情况, yysy, 也不是不能用):

第二个需要注意的是, 看到点光源的颜色值: 45 * RGB(.6, .8, 1), 说明点光源本身亮度是非常高的, 但尽管如此渲染出来的场景仍然比较昏暗, 这是因为平方反比项对亮度起主要作用. 所以大部分情况下, 为了美观会直接把平方反比项删掉或改成反比, 下面是两种改进的渲染图:


可以看到观感都比使用平方反比的要更好. 商业用的光追器提供自定义参数控制光源随距离衰减的强度 (当然也不难实现), 但为了真实性, 在这个程序里还是继续使用符合平方反比的光源.
第三个要注意的是溢色, 之前说过颜色值是定义在 [0,1] 里, 而点光源的颜色是大于 1 的, 那么当储存成图片时, 大于 1 的值会被截断而只保留小数部分, 这会产生极其严重的颜色错误, 比如说把点光源的亮度乘数换成 75 进行渲染:

可以看到尽管场景亮度有所增加, 但光源直接照射的地方出现了错误的颜色. 因为不打算引入 HDR 渲染 (以及相应的 map tonning 技术), 所以在 LDR 范围里有两种常做的方法: 1) 所有颜色值 clamp 到 [0, 1] 里; 2) 颜色值除以最大分量值, 以保证色调正确. 下面是两种方法的实现:

然后改写 render_figure:

在 main 里指定 RemapColor 的实类就可以进行渲染了, 下面是两种不同的渲染结果


在观感上是 Clamp01 比较好, 但是可以看到展示点光源的球体完全变成了白色; MaxTo01 保持着色调正确, 但在过曝区域失去了颜色变化. 两种方法可以按照个人喜好来使用, 之后的程序如果没有特别说明就统一使用 Clamp01 了.

/* 请自行脑部结语 */
项目仓库: https://github.com/nyasyamorina/nyasRT
群: 274767696
封面pid: 哦, 这次封面不是涩图, 那没事了