如果您查看 Phong 反射模型的一般定义,它通常是所有光源的总和,其形式为:
Σ kd * (L . N) * i + ks * (R . V)^a * i
在这种情况下kd
, 和ks
是漫反射和镜面反射常数,L
和N
是R
相关向量,a
是光泽度,i
是当前入射光的强度。您的版本略有不同,因为您可以通过拆分总和并移出常量来重写它,但这并不是一种常见的方法:
kd * Σ ((L . N) * i) + ks * Σ ((R . V)^a * i)
没有做那么多的原因是由于一般的渲染方程是如何工作的,它通常是在一个半球上的一点积分的形式:
Lo(wo) = Le(wo) + ∫ f(wi, wo) * (wi . n) * Li(wi) dwi
在这种情况下,Lo
是沿一个方向的出射辐射wo
,Le
是在一个方向的发射贡献,f
是 BRDF,n
是表面的法线,Li
是在入射方向的入射辐射贡献wi
(这是正在整合的内容)。实际上,这被实现为求和,再次表明在一个方向上的点处的照明贡献是在一个方向上的每个单独的照明计算的加权和的类型。对于像您这样的带有点光源的简单渲染器,这只是每个光的贡献的总和,因为假设光只能来自光源而不是环境本身。这不是一个真正的问题,但是如果您计划实现更复杂的照明模型,那么您将不得不稍微重写您的程序结构。
然而,我怀疑的主要问题是光线追踪场景中的照明通常是在没有边界的线性空间中完成的,这意味着光线不会像您观察到的那样始终保持在 0-1 范围内。灯光可以用远大于 1 的值表示,以区分例如太阳和简单的台灯,或者在您的情况下,许多小灯光的组合可能会导致表面上的值在组合时远大于 1。虽然这在渲染过程中不是问题(实际上必须以这种方式获得正确的结果),但当您决定最终呈现图像时,这是一个问题,因为监视器只接受 8 位或更现代的 HDR 显示设备,每个通道 10 位颜色,
这个从 HDR 到 LDR 的过程通常是通过色调映射完成的,这实际上是一种将值范围压缩到以“智能”方式呈现的东西的操作,无论它可能是什么。可以将各种因素结合到色调映射中,例如曝光,甚至可以从物理模拟的相机属性中得出,例如快门速度、光圈和 ISO(因为我们习惯于相机如何捕捉电影和照片中看到的世界),或者可以可以像许多视频游戏一样粗略地近似,或者可以完全忽略。此外,色调映射操作的曲线和“风格”完全是主观的,通常根据看起来适合相关内容的内容进行选择,或者在电影或视频游戏等情况下由艺术家专门选择,
即使将值范围转换为更适合显示输出的值,但色彩空间可能仍然不正确,并且取决于您将其写入显示设备的方式,可能需要通过将值通过OETF(光电传递函数)将输出编码为显示器的电子信号。通常,您不必担心色彩空间,因为大多数人都在 sRGB(Rec. 709 的轻微变体)的显示器上工作并直接在他们的应用程序中使用它,所以除非您不遗余力地制作色彩空间在你的光线追踪器中,除此之外,没有什么可担心的。另一方面,伽马校正通常必须作为 OpenGL 等 API 中的默认帧缓冲区来完成,
总而言之,您几乎只需要应用色调映射运算符并可能对您的最终颜色输出进行伽玛校正,以获得看起来相当正确的东西。如果您需要一个快速而肮脏的方法,您可以尝试x / (x + 1)
(也称为 Reinhard 色调映射),其中 x 将是光线追踪的输出。如果输出太暗,您也可以将此函数的输入乘以某个任意常数,以进行简单的“曝光”调整。最后,如果您的输出设备在伽马空间中期待某些东西,那么您可以获取色调映射输出并应用该功能x^(1.0 / 2.2)
到它(请注意,这是对正确 sRGB OETF 的轻微简化,但只要记住这一点就可以使用)将其放入伽马空间,但如果您要输出到图像,这通常不需要,但仍然需要牢记。需要注意的另一件事是色调映射通常会在 0-1 范围内输出,因此如果您确实需要通过乘以 255 或任何输出图像格式可能期望转换为 8 位整数,则应该在完成所有操作后完成比之前的任何一个都更早,因为早点乘以它除了让场景看起来更亮之外几乎没有任何作用。
我还想提一下,如果您打算将此光线追踪器进一步开发成更详细的东西(例如路径追踪器),使用 Phong 照明模型是不够的,因为它违反了渲染所期望的节能特性方程。存在许多 BRDF,甚至是相对简单的基于 Phong 的(稍作修改以使其正常运行),因此这样的更改不需要太多额外的代码,但会提高渲染器的视觉保真度并使其更具未来感证明是否曾经实现过更复杂的行为。