3

在我的光线追踪器上实现 phong 着色时,我目前正在试验不同的工件。

第一种情况发生在我以我认为正确的方式实现镜面光照计算时:将光源的贡献累积添加:

specular_color += light_intensity * std::pow (std::max(0.f,reflected*camera_dir),mat.ns); 

茶壶溢出

但是,如果我不积累贡献,通过

specular_color = light_intensity * std::pow (std::max(0.f,reflected*camera_dir),mat.ns);

我明白了:

茶壶-sem-verflow

似乎更接近,但仍然有一些文物。

打印 specular_color 变量假定的一些值(即 3 个浮点对象)我收到的值高达

specular_color (200) 之后:1534.73 1534.73 1534.73

对于 x 和 y=200 的像素,添加了 + 符号

没有它,我会收到:

specular_color (200) 之后:0 0 0

所有这些浮点值都用

a [ctr] = min (final_color.blue*255.0f,255.0f);
     a [ctr+1] = min (final_color.green*255.0f,255.0f);
     a [ctr+2] = min (final_color.red*255.0f,255.0f); 

用于文件写入

而 final_value 无非是:

final_color = diffuse_color * mat.ks + specular_color * mat.kd;

specular_color (light_intensity,reflected,camera_dir) 的组件似乎是正确的,因为在其他地方使用没有问题。

因此,我将不胜感激有关错误可能在哪里以及如何修复它的任何建议。

4

2 回答 2

3

如果您查看 Phong 反射模型的一般定义,它通常是所有光源的总和,其形式为:

Σ kd * (L . N) * i + ks * (R . V)^a * i

在这种情况下kd, 和ks是漫反射和镜面反射常数,LNR相关向量,a是光泽度,i是当前入射光的强度。您的版本略有不同,因为您可以通过拆分总和并移出常量来重写它,但这并不是一种常见的方法:

kd * Σ ((L . N) * i) + ks * Σ ((R . V)^a * i)

没有做那么多的原因是由于一般的渲染方程是如何工作的,它通常是在一个半球上的一点积分的形式:

Lo(wo) = Le(wo) + ∫ f(wi, wo) * (wi . n) * Li(wi) dwi

在这种情况下,Lo是沿一个方向的出射辐射woLe是在一个方向的发射贡献,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 的(稍作修改以使其正常运行),因此这样的更改不需要太多额外的代码,但会提高渲染器的视觉保真度并使其更具未来感证明是否曾经实现过更复杂的行为。

于 2019-06-06T00:47:21.220 回答
2

第一个建议:不要用0.0 - 255.0来表示光源的强度。使用 0.0 到 1.0。规模化和积累会更好。当需要显示时,您将最终强度映射到 0 - 255 范围内的像素值。

如果您将“最亮”的灯光表示为 255,并且您的场景有一个这样的灯光,那么您可以摆脱它。但是,如果您随后添加第二盏灯,则任何给定的像素都可能被两盏灯照亮,最终亮度是您可以表示的最亮事物的两倍,这基本上就是您的第一个示例中发生的情况——您的大多数像素也是明亮地显示。

要对灯光进行标准化,您必须进行额外的除法和乘法运算number_of_lights * 255。这会变得混乱。强度会线性缩放和累积,但像素值不会。因此,使用强度并在最后转换为像素值。

要进行映射,您有几个选项。

  1. 找到图像中的最低和最高强度,并使用线性映射将它们转换为 0 - 255 的像素值。

    例如,如果您的最低强度是 0.1,而您的最高强度是 12.6,那么您可以像这样计算每个像素值:

    pixel_value = (intensity - 0.1) / (12.6 - 0.1) * 255;
    

    这不是很现实(物理上),但无论场景中有多少(或很少)光线,它都足以获得不错的结果。您实际上是在进行粗略的“自动曝光”。不幸的是,您的“黑暗”场景会显得太亮,而明亮的场景可能看起来很暗。

  2. 眼睛和胶片对光强度的实际响应曲线不是线性的。它通常是一条 s 形曲线,通常可以近似为:

    response = 1 - exp(intensity / exposure);
    pixel_value = 255 * response;  // clamp to 0 - 255
    

    这里对曝光方程有很好的解释。基本上,添加第二道相同强度的光不应使像素亮度增加一倍,因为这并不是我们(或电影)真正感知亮度的方式。

    响应曲线可能更复杂。老式感光胶片具有奇怪的特性。例如,使用胶片进行长时间曝光可能会产生与使用快速快门拍摄的“等效”曝光不同的图像。

当您想要获得超级准确时,您还需要查看观察系统的伽马等东西,它不仅试图解释感知中的非线性,而且还考虑显示器和传感器中的非线性。如果我理解正确,HDR 最终是关于将测量的强度仔细映射到显示值,以便在广泛的强度范围内保持对比度。

最后,虽然它与您展示的问题没有直接关系,但看起来您在包含的代码片段中反转了漫反射和镜面反射材质属性:

final_color = diffuse_color * mat.ks + specular_color * mat.kd;

我假设您打算mat.ks用于镜面反射和mat.kd漫反射。当您尝试调整这些值时,这可能会使事情变得混乱。

于 2019-06-06T00:20:04.430 回答