在射线跟踪器上实施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)似乎是正确的,因为它们已在其他地方使用而没有问题。
因此,对于可能存在错误以及如何修复错误的任何建议,我将不胜感激。
答案 0 :(得分:3)
如果查看Phong反射模型的一般定义,通常是所有光源的总和,形式为:
Σ kd * (L . N) * i + ks * (R . V)^a * i
在这种情况下,kd
和ks
是散射常数和镜面常数,L
,N
和R
是相关矢量,{{1} }是光泽,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
是BRDF,{{1} }是表面的法线,f
是在入射方向n
(正在积分的方向)上的入射辐射贡献。实际上,这被实现为求和,再次表明方向上一点上的照明贡献是方向上每个单独照明计算的加权和类型。对于像您这样的带有点光源的简单渲染器,这只是每种光的贡献之和,因为假定光只能来自光源而不是环境本身。这并不是真正的问题,但是如果您打算实现更复杂的照明模型,则必须重新编写程序的结构。
但是,我怀疑的主要问题是,光线跟踪场景中的照明通常是在没有边界的线性空间中进行的,这意味着光不会像您观察到的那样始终保持在0-1的范围内。可以用大于1的值表示光,以区分例如太阳与简单的台灯,或者在您的情况下,可能是许多小灯的组合导致组合时表面上的值远大于1。尽管这在渲染过程中不是问题(实际上必须采用这种方式才能获得正确的结果),但是当您决定最终呈现图像时,这是一个问题,因为监视器只能接受8位图像,或者在使用更现代的HDR显示时设备,每个通道10位颜色,这意味着您必须以某种方式将场景中辐射的整个浮点范围表示为有限得多的整数范围。
这种从HDR到LDR的过程通常是通过色调映射完成的,色调映射实际上是一种将值的范围压缩成可以“智能”方式呈现的内容的操作,无论可能是什么。可以将各种因素纳入色调映射,例如曝光,甚至可以从物理模拟的相机属性(例如快门速度,光圈和ISO(因为我们习惯于相机如何捕捉电影和照片中看到的世界))中得出曝光,或者可以粗略地近似于许多电子游戏,或者可以完全忽略。此外,色调映射操作的曲线和“样式”是完全主观的,通常是根据看起来适合所讨论内容的外观来选择的,或者在诸如电影或视频游戏之类的情况下由艺术家专门选择的,这意味着您可以几乎只能选择最适合自己的选项,因为没有一个正确的答案(同样,它通常基于S形曲线胶片的展览,原因是相机在媒体中的广泛使用)。
即使将值范围转换为更适合显示输出的值之后,色彩空间仍可能不正确,并且取决于您将其写入显示设备的方式,可能需要通过将通过OETF(光电传递函数)对这些值进行编码,以将输出的电子信号编码为监视器。通常,您不必担心色彩空间,因为大多数人都在sRGB显示器上工作(Rec。709的细微变化),并直接在其应用程序中使用它,因此除非您不遗余力地创建色彩空间在您的光线追踪器中,除了这些,别无所求。另一方面,伽玛校正通常必须作为OpenGL,Direct3D或Vulkan等API中的默认帧缓冲区来完成,而如果您正在输出,则通常已经在伽玛空间中了(而前面提到的照明数学是在线性空间中完成的)。到图像之类的东西,则根据格式可能不需要它。
不过,作为总结,您几乎只需要应用一个色调映射运算符,并可能对最终的颜色输出进行gamma校正即可获得看起来合理的校正。如果您需要快速而肮脏的产品,可以尝试Li
(也称为Reinhard色调映射),其中x是光线跟踪的输出。如果输出太暗,您也可以将该函数的输入乘以某个任意常数,以进行简单的“曝光”调整。最后,如果您的输出设备在伽玛空间中期望有某种东西,那么您可以获取色调映射的输出并将函数wi
应用于它(请注意,这是对适当的sRGB OETF的略微简化,但是可以使用尽可能长的时间(请牢记这一点)以将其放入伽玛空间,不过,如果要输出到图像,通常也不需要这样做,但仍要记住一点。要注意的另一件事是,色调映射通常会在0-1的范围内输出,因此,如果您确实需要乘以255或例如输出图像格式可能期望的任何形式来转换为8位整数,则应在所有操作之后与之前相比,将其乘以倍数几乎无济于事,除了使场景看起来更加明亮之外。
我还要说一下,如果您打算将这种光线跟踪器进一步开发为更详细的东西,例如路径跟踪器,那么使用Phong照明模型将是不够的,因为它违反了预期的节能特性通过渲染方程式。存在许多BRDF,甚至是一个相对简单的基于Phong的BRDF(进行一些细微的修改即可使其正常运行),因此这种更改将不需要太多的额外代码,但会改善渲染器的视觉保真度并使其更富未来性证明是否曾经实施过更复杂的行为。
答案 1 :(得分:2)
第一个建议:不要使用0.0-255.0表示光源的强度。使用0.0到1.0。扩展和累积将更好地工作。当需要显示时,可以将最终强度映射到0到255范围内的像素值。
如果将“最亮”的光表示为255,并且场景中有一个这样的光,则可以避免这种情况。但是,如果再添加第二个光源,则任何给定像素都可能同时被这两个光源照亮,最终亮度是您可以代表的最亮物体的两倍,这基本上就是您在第一个示例中所发生的情况-大多数像素也是如此亮显示。
要对光进行归一化,您必须使用number_of_lights * 255
进行额外的除法和乘法。太乱了。强度线性缩放并累积,但像素值不线性。因此,请使用强度,最后转换为像素值。
要进行映射,您有几个选择。
查找图像中的最低和最高强度,并使用线性映射将其转换为0到255之间的像素值。
例如,如果最低强度为0.1,最高强度为12.6,则可以像这样计算每个像素值:
pixel_value = (intensity - 0.1) / (12.6 - 0.1) * 255;
(物理上)这不是很现实,但是无论场景中有多少(或很少)光线,都足以获得不错的结果。您实际上是在进行粗略的“自动曝光”。不幸的是,您的“黑暗”场景看起来太亮了,您的明亮场景似乎太暗了。
眼睛和胶片对光强度的实际响应曲线不是线性的。通常是S形曲线,通常可以通过以下方式近似得出:
response = 1 - exp(intensity / exposure);
pixel_value = 255 * response; // clamp to 0 - 255
exposure equation here有一个很好的解释。基本上,添加第二个强度相同的光不应使像素的亮度翻倍,因为这不是我们(或电影)感知亮度的方式。
响应曲线可能更加复杂。老式的感光胶片具有奇怪的特性。例如,长时间使用胶卷曝光可以产生与使用快速快门进行“等效”曝光不同的图像。
当您想要获得超精确度时,您还需要查看诸如观察系统的伽马之类的东西,该系统不仅要考虑感知方面的非线性,还要考虑显示器和传感器的非线性。如果我理解正确,那么HDR最终就是要仔细地将测得的强度映射到显示值,以在各种强度下保持对比度。
最后,虽然它与您所显示的问题没有直接关系,但看起来您在所包含的代码片段中反转了漫反射和镜面反射材料的属性:
final_color = diffuse_color * mat.ks + specular_color * mat.kd;
我假设您打算将mat.ks
用于镜面反射,并将mat.kd
用于漫反射。当您尝试调整这些值时,这可能会使您感到困惑。