Skip to content

线性色彩空间

线性色彩空间是 CSP v0.2.3 中引入的新功能,允许 WeatherFX 样式更改 Assetto Corsa 处理着色的方式。默认情况下禁用,因此旧的 WeatherFX 样式看起来与之前一样。

什么是线性色彩空间?

在计算机图形学中,有两种更流行的存储和处理图像数据的方式(即色彩空间)。一种是线性色彩空间,其中存储的亮度值与显示像素的实际亮度成线性比例(例如 RGB=127 的亮度大约是 RGB=255 的一半)。但我们的感知是非线性的:开第二盏灯时我们不会感觉到亮度翻倍,而且在直射阳光下几乎注意不到手电筒的光斑,而在夜晚它可能甚至刺眼。

因此,存在另一种色彩空间叫做 sRGB(或 gamma 色彩空间),它更接近人类对光的感知。以这种格式存储数据可以更高效地利用空间,并有助于消除暗区的色带问题(在 gamma 色彩空间中 RGB=2 和 RGB=1 差别不大,而在线性空间中差距很大)。因此,它几乎是所有计算机图形数据的主要标准。JPEG 和 PNG 以 sRGB 存储数据,Windows 桌面合成器期望图像以 sRGB 返回数据,然后将 sRGB 数据作为 sRGB 发送到显示器。

不幸的是,正因为如此,游戏开发者很容易犯这个错误:实际的 3D 着色也在 sRGB 中进行。但 3D 渲染使用的许多模型如果不在线性色彩空间中进行,效果会很差。即使像"遍历附近的光源并累加它们对给定像素的贡献"这样简单的操作:在线性色彩空间中这是完全合理的,会产生手电筒光斑在直射阳光下逐渐消退的效果;但如果在 gamma 空间中进行,结果会看起来很糟糕。渲染器的几乎所有其他方面也是如此,包括反射(你有没有注意到现实中黑色汽车在照明下似乎比白色汽车更有反射性?)、雾(如果在线性色彩空间中使用正确的散射,被照亮的物体应该比暗物体在更远的距离可见)、光与表面的交互方式等等。

所以,这就是新选项所做的事情。一个简单的配置行,但在其内部切换到了一套全新的着色器,并升级了 Assetto Corsa 渲染的相当多部分。这就是为什么由 WeatherFX 样式来决定是否激活它:以前,样式经过精心调优来尝试解决 AC 中任何与着色相关的计算完全错误的问题,提供各种虚假值,甚至像阳光/环境光比例这样基本的东西。现在有了更准确的渲染器,大部分这些 hack 不再需要。

当然,不仅仅是 WeatherFX 样式经过微调来对抗这个问题,很多例如材质或 PP 滤镜也是以这种方式配置的。但我预计那里的影响不太严重:新的着色器集在后台进行了一些转换以尝试保持外观相似(例如 fresnelMaxLevel 会被平方);当然,不可能完美匹配:至少现在黑色汽车在 AC 中也会显得更有反射性。

当前状态

CSP v0.2.3 应该已经将其几乎所有功能适配到新的色彩空间。一个显著的例外是 Lua 着色器:遗憾的是没有很好的方法添加兼容层,所以着色效果可能会有偏差。透明度也可能存在一些问题:正确的线性色彩空间会改变 Alpha 混合的视觉结果,有时可能导致某些例如汽车仪表板外观的不一致。已经有一些调整来尝试缓解这个问题,但可能还不够。

另一个问题是线性色彩空间会更加突出渲染中的任何一般性问题和捷径,但随着我们继续改进 AC 的视觉方面并升级各种视觉效果,这些问题将在未来得到修复。

另外,线性色彩空间目前尚未应用于展厅或预览生成模式。一些原始效果,如旧版烟雾,在某些光照条件下可能表现异常。

性能影响

新的着色器集有一些额外的指令用于在色彩空间之间转换和调整参数,以及新的双层雾。但旧集有两种雾变体:原始 Kunos 雾和 CSP 雾。因此,一般来说,性能影响应该可以忽略不计。另外,如果你不使用 YEBIS 替换,可能会添加一个额外的后处理步骤,但如果你关心性能,YEBIS 替换可能是一个值得尝试的选项(默认 WeatherFX 样式中使用的自动曝光在 v0.2.3 中有了极大改进)。

材质配置技巧

目前的主要建议是,除非你使用 fuPBR 着色器,否则请考虑暂时以旧色彩空间为目标,而不是为线性色彩空间重新配置材质,特别是如果你使用那些 [INCLUDE: common/material…] 文件中的材质模板。材质参数重映射将来可能会更改为更合适的值,因此你的旧材质可能会改进,而专门为线性色彩空间适配的材质可能会出问题。

当然,所有这些都不适用于 fuPBR 着色器。只需使用常规的 PBR 贴图,无需任何调整。如果视觉效果确实有偏差,请不要编辑反射率或粗糙度贴图,fuPBR 着色器应该更新以尽可能准确。这毕竟是正确 PBR 的全部意义。

另外,关于材质,需要注意的一点是不要让东西太暗。使用常规纹理和材质时,你的 ksAmbient/ksDiffuse 值应该在 0.4…0.6 左右。当然,也请尽量保持它们相似。也有例外:例如,如果你在森林赛道上工作并且由于某种原因不想烘焙顶点 AO,你可以将树下表面的 ksAmbient 设置得更低,但对于常规材质请尽量保持这些值相似。

Lua 脚本技巧

现在编写脚本时有两个关键点需要考虑。首先,如果你在主渲染通道中使用着色器绘制任何内容,你可能需要更新你的着色器。使用 USE_LINEAR_COLOR_SPACE(可以在 #if USE_LINEAR_COLOR_SPACEif (USE_LINEAR_COLOR_SPACE) 中使用)来更改某些逻辑。你也可以使用 toLinearColorSpace()toSrgbColorSpace() 来转换到线性色彩空间和返回。例如,如果你在计算中读取漫反射纹理并将其乘以类似 ksDiffuse 的值,在线性色彩空间中你应该通过 toLinearColorSpace() 传递结果,它应该可以正常工作(不需要分支,如果线性色彩空间被禁用 toLinearColorSpace() 不会做任何事情,只是返回其输入)。

另一个要点是如何处理包含场景截图的输入纹理,例如如果你想将它们输出到 LDR 纹理中的汽车仪表板上。一般来说,确保通过 convertHDR() 传递 HDR 场景输入。在线性色彩空间中 WeatherFX 样式可以将整体场景亮度设置为 0.001 之类以更有效地使用 float16 范围,convertHDR() 会抵消这一点。如果你出于某种原因需要执行相反的操作,将第二个参数设置为 true

此外,你可以使用 Lua 本身的 ac.convertHDRToLDR() 函数访问相同的转换。

WeatherFX 开发技巧

要启用此修复,只需使用 true 参数调用 ac.useLinearColorSpace()。至于它的第二个参数,那是第一个注意事项:AC 和 CSP 几乎为所有渲染目标使用 float16(即 half)格式,虽然总体上很好,但其范围只有大约 1/65000…65000(以及相同的负数)。对于原始的绘制方式来说不算差,但现在有了 resultEmissive = pow(ksEmissive * txDiffuse, 2.2) 这样的转换阶段,很容易就会达到 65k 的边界(白色纹理加 200 的 ksEmissive 就已经达到限制了)。我找到的最佳解决方法是使用场景亮度乘数并将其设置为 0.001 左右,因为我们在千分位有很多未使用的精度。灯光也是如此:为了优化数据交换,它们的颜色以 float16 格式在传递给 GPU 时存储,所以颜色为 200 的灯光会被截断。这就是 ac.useLinearColorSpace() 第二个参数的作用。设置为 100 之类,灯光颜色在发送到 GPU 时会除以 100,LightingFX 的贡献在 GPU 端之后乘以 100。

在更新默认 WeatherFX 样式时,我注意到了相当多的 bug,太多无法用选项修复,所以除了切换色彩空间,该函数还应用了一堆修复,改变了一些函数的行为:

  • 云层现在会考虑天空雾偏移和指数,而之前它们忽略了这些值
  • 远处辉光亮度不会被场景亮度乘数影响两次
  • 白色参考点受场景亮度乘数影响
  • 月亮 mie 在 v2 天空着色器中正常工作(之前由于一个拼写错误,它没有被添加到最终结果中)
  • 计算吸收的函数不再混淆 v2 天空朝向太阳和背离太阳的值(那个错误特别严重,抱歉 🤦‍♂️)
  • 体积光现在使用亮度乘数
  • ac.setBrightnessMult() 现在会缩放其他值(以前,例如你需要先设置亮度乘数然后设置雾颜色才能应用;现在你可以设置雾颜色、更改亮度乘数,雾颜色会自动重新缩放)
  • 天空颜色计算之前完全没有考虑太阳颜色

另一个重大变化是雾函数。旧的着色器集有两种雾实现,一种是 Kunos 风格,另一种是来自这篇文章的基于高度的雾。不幸的是,在正确的线性色彩空间下,很明显它不是我们需要的:由于基于高度的特性,它总是在某一点达到 1(至少在我的版本中是这样,也许我遗漏了什么,但我无法弄清楚如何正确使用它)。正确的雾永远不应该达到 1,这正是使得明亮有光泽的物体在雾中比暗物体在更远的距离可见的原因。因此,它被由 ac.setNearbyFog() 控制的新雾所取代,这是你可以添加到场景上的第二层雾,用于实际的雾和薄雾。旧的新雾被简化了,可以用于远处的薄雾或简单的大气光吸收,所以现在有两层雾可以一起使用。

当然,线性正确的色彩空间也需要一个后处理步骤将线性图像转回 sRGB。默认情况下,CSP 会使用你通过 ac.setHDRToLDRConversionHints() 提供的值,在将图像发送到 YEBIS 进行后处理之前为你完成该步骤。但如果你在开发自定义后处理实现,也许你会想使用一些特殊的东西。例如,对于默认的 WeatherFX 样式,我使用了不同的 linear→sRGB 转换,应该更准确。也许在色调映射步骤之前而不是开始时应用 linear→sRGB 转换也有意义。但如果你不想替换 YEBIS,你总是可以从 ac.onPostProcessing() 返回一个带有 sRGB 数据的画布(参见默认 WeatherFX 样式中的 render_linear.lua 示例)。当然,你也可以使用 YEBIS 中的 gamma 参数,但根据我的经验,它产生的结果差得多。

还有一件事。与其他的一样,WeatherFX Lua 在 AC 已经加载后首次运行,这包括着色器。如果脚本做的第一件事就是切换到线性色彩空间着色器集导致大量延迟,这可能是个问题。为了防止此类问题,请在你的 manifest.ini 中添加 [CORE] LINEAR_COLOR_SPACE_HINT = … 值。它可以是 0、1,或者如果你的线性色彩空间是可选的并由 settings.ini 中的复选框控制,则是一个节和键名。

引用来源