【问题标题】:Artifacts with phong shading and ray-tracing带有 phong 着色和光线追踪的工件
【发布时间】:2019-10-21 11:26:25
【问题描述】:

在我的光线追踪器上实现 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);

我明白了:



看起来更近了,但仍然有一些伪影。

打印 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)似乎是对的,因为在其他地方使用没有问题。

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

【问题讨论】:

    标签: c++ graphics raytracing phong


    【解决方案1】:

    第一个建议:不要用 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
      

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

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

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

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

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

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

    【讨论】:

    • 感谢您的解释,非常有趣,我想到的一个疑问是曝光值。这是用户定义的值吗?有没有办法获得一些不错的起始值的好主意,或者我必须猜测它并看看它的样子?
    • @user2752471:我在 #2 中链接到的文章使用通用常数 K 来说明胶片的光圈、快门速度和灵敏度。我首先将其设置为 1,然后根据需要对其进行调整,将其加倍或减半,直到你得到看起来正确的东西。在物理模拟中,光源的亮度将具有实际的物理单位,您可以计算出一个合理的值。但大多数光线追踪器只是使用一个虚构的定性单位,范围从“非常暗”到“非常亮”。
    【解决方案2】:

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

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

    在这种情况下,kdks 是漫反射和镜面反射常数,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 是表面的法线,@987654336 @ 是传入方向wi 上的传入辐射贡献(这是正在整合的内容)。实际上,这被实现为求和,再次表明在一个方向上的点处的照明贡献是在一个方向上的每个单独的照明计算的加权和类型。对于像您这样的带有点光源的简单渲染器,这只是每个光的贡献的总和,因为假设光只能来自光源而不是环境本身。这不是一个真正的问题,但如果您打算实现更复杂的照明模型,那么您将不得不稍微重写您的程序结构。

    但我怀疑的主要问题是光线追踪场景中的照明通常是在没有边界的线性空间中完成的,这意味着光线不会像您观察到的那样始终保持在 0-1 范围内。灯光可以用远大于 1 的值表示,以区分例如太阳和简单的台灯,或者在您的情况下,许多小灯光的组合可能会导致表面上的值在组合时远​​大于 1。虽然这在渲染过程中不是问题(实际上必须这样才能获得正确的结果),但当您决定最终呈现图像时,它是一个问题,因为监视器只接受 8 位或更现代的 HDR 显示设备,每个通道 10 位颜色,这意味着您必须以某种方式将场景中的整个浮点辐射范围表示为一个更有限的整数范围。

    这个从 HDR 到 LDR 的过程通常是通过色调映射来完成的,这实际上是一种将值范围压缩到以“智能”方式呈现的东西的操作,无论它可能是什么。可以将各种因素结合到色调映射中,例如曝光,甚至可以从物理模拟的相机属性中得出,例如快门速度、光圈和 ISO(因为我们习惯于相机如何捕捉电影和照片中看到的世界),或者可以可以像许多视频游戏一样粗略地近似,或者可以完全忽略。此外,色调映射操作的曲线和“风格”完全是主观的,通常根据看起来适合相关内容的内容进行选择,或者在电影或视频游戏等情况下由艺术家专门选择,这意味着您可以几乎只需选择最适合您的东西,因为没有一个正确的答案(同样,由于相机在媒体中的广泛使用,它通常基于 S 形曲线胶片展品)。

    即使在值范围已转换为更适合显示输出的值之后,色彩空间仍可能不正确,并且取决于您将其写入显示设备的方式,可能需要通过将这些值通过 OETF(光电传递函数)对显示器的输出电子信号进行编码。通常,您不必担心色彩空间,因为大多数人都使用 sRGB(Rec. 709 的轻微变体)的显示器并直接在他们的应用程序中使用它,所以除非您不遗余力地制作色彩空间在你的光线追踪器中,除此之外,没有什么可担心的。另一方面,伽马校正通常必须作为 OpenGL、Direct3D 或 Vulkan 等 API 中的默认帧缓冲区通常已经在伽马空间中完成(而前面提到的光照数学是在线性空间中完成的),但如果你正在输出对于诸如图像之类的东西,则可能不需要它,具体取决于格式。

    不过,总而言之,您几乎只需要应用色调映射运算符并可能对您的最终颜色输出进行伽玛校正,以获得看起来相当正确的东西。如果您需要一个快速而肮脏的方法,您可以尝试x / (x + 1)(也称为 Reinhard 色调映射),其中 x 将是光线追踪的输出。如果输出太暗,您也可以将此函数的输入乘以某个任意常数,以进行简单的“曝光”调整。最后,如果您的输出设备在 gamma 空间中期待某些东西,那么您可以获取色调映射输出并将函数 x^(1.0 / 2.2) 应用于它(注意这是对正确 sRGB OETF 的轻微简化,但只要您使用它就可以了请记住这一点)将其放入伽马空间,但如果您要输出到图像,这通常是不需要的,但仍然需要牢记。另一件需要注意的事情是,色调映射通常会在 0-1 范围内输出,因此如果您确实需要通过乘以 255 或任何输出图像格式可能期望转换为 8 位整数,则应该在完成所有操作之后完成比之前任何一个都更早,因为早点乘以它除了让场景看起来更亮之外几乎没有任何作用。

    我还想提一下,如果您计划将此光线追踪器进一步开发成更详细的东西(例如路径追踪器),使用 Phong 照明模型是不够的,因为它违反了预期的节能特性通过渲染方程。存在许多 BRDF,甚至是相对简单的基于 Phong 的(稍作修改以使其正常运行),因此这样的更改不需要太多额外的代码,但会提高渲染器的视觉保真度并使其更具未来感证明是否曾经实现过更复杂的行为。

    【讨论】:

    • 嗨@Lemon Drop。感谢您的全面解释。在 Lo(wo) = Le(wo) + ∫ f(wi, wo) * (wi . n) * Li(wi) dwi 的情况下,kd 和 ks 系数不能离开表示积分的求和,为什么?它是 f (wi,wo) 的一部分,对吗?所以把它移到外面会影响方程的 (wi . n) * Li(wi) dwi 部分,而它不应该?
    • @user2752471 我想在一个更简单的光照模型中,kd 和 ks 也可以写在外面,但总的来说,这并不是因为它确实让 BRDF 被分离出来时阅读起来更加混乱就像那样,更不用说当真正需要一个时它需要两个循环。此外,一些 BRDF 将 ks 基于菲涅耳并从中派生 kd,因此它无论如何都依赖于积分内的事物,例如 wi,这会阻止它被提取为常数。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-06-02
    • 2017-08-16
    • 2018-11-19
    相关资源
    最近更新 更多