汉化标题:通用 – 线性色彩空间
原文页面:General-–-Linear-color-space
原文锚点:7a07801
汉化时间:2026-09-12T19:00:00+08:00
CSP 的线性色彩空间是 0.2.3 引入的新功能,允许 WeatherFX 风格改变 Assetto Corsa 处理着色的方式。该功能默认禁用,因此旧的 WeatherFX 风格外观保持不变。
这是什么?
在计算机图形学中,存储和处理图像数据(即色彩空间)有两种较为流行的方式。其一是线性色彩空间,其中存储的亮度值与显示像素的实际亮度成线性比例关系(也就是说,如果用照度计测量屏幕,RGB=127 的亮度是 RGB=255 的一半)。但问题在于,我们的感知是非线性的:打开第二盏灯时,我们看到的亮度并不会翻倍;在直射阳光下几乎注意不到手电筒的光斑,而在夜里它有时甚至会晃眼。
因此,存在一种称为 sRGB(或 gamma 色彩空间)的替代色彩空间,它更接近人类感知光线的方式。以这种格式存储数据能更高效地利用空间,并有助于消除暗部的色带(在 gamma 色彩空间中 RGB=2 与 RGB=1 差别不大,而在线性空间中差异相当大)。正因如此,它几乎是所有计算机图形数据的主要标准。JPEG 和 PNG 以 sRGB 存储数据,Windows 桌面合成器期望图像以 sRGB 返回数据,再将这些 sRGB 数据以 sRGB 形式传给显示器。
遗憾的是,正因如此,游戏开发者很容易犯下这样的错误:实际的三维着色也在 sRGB 中进行。但三维渲染所用的大量模型若不在线性色彩空间中计算,效果会非常糟糕。即便是“遍历附近光源并把它们对某个像素的贡献求和”这么简单的操作:在线性色彩空间中这样做完全合理,会得到手电筒光斑在直射阳光下逐渐淡出的效果;而如果在 gamma 空间中做,结果看起来就会像垃圾一样。渲染器的几乎所有其他方面也是如此,包括反射(有没有注意到现实中黑色车在受光时似乎比白色车反射更强?)、雾(若在线性色彩空间中配合正确的散射计算,受光物体在雾中应比暗色物体可见距离远得多)、光与表面交互的方式等等。
这个新选项做的就是这件事。只是一行简单的配置,但内部会切换到一套全新的着色器,并升级 Assetto Corsa 渲染的相当多部分。也正因如此,是否激活该功能由 WeatherFX 风格自行决定:以前,各种风格为了绕开“AC 中所有与着色相关的计算都是完全错误的”这一事实,不得不提供各种虚假数值来精心调校,甚至连太阳光/环境光关系这么基础的东西也要如此。现在有了精确得多的渲染器,其中大多数取巧手段都不再需要了。
当然,为对抗这一问题而精心调校的不只是 WeatherFX 风格,许多材质和 PP 滤镜也是按这种方式配置的。不过我预计这方面的影响没那么严重:新着色器集会在后台做一些变换,尽量保持外观相似(例如 fresnelMaxLevel 会被平方);当然,完美匹配是不可能的:至少,现在 AC 里黑色车看起来也会更具反射感。
当前状态
CSP v0.2.3 的几乎所有功能都应该已适配新的色彩空间。不过有一个明显的例外是 Lua 着色器:遗憾的是,没有太好的办法添加兼容层,因此着色效果可能有偏差。透明度方面也可能存在一些问题:正确的线性色彩空间会改变 alpha 混合叠加的视觉结果,有时会导致某些对象的外观出现差异,例如某些车内的仪表盘。目前已经有一些调整手段试图缓解该问题,但可能还不够。
另一个问题是,线性色彩空间会更加凸显渲染中各种普遍存在的问题和偷工减料之处,不过随着我们继续打磨 AC 的视觉部分并升级各种视觉效果,这些问题将来会得到修复。
对了,线性色彩空间尚未应用于展厅或预览图生成模式。另外一些原始效果(例如旧版烟雾)在某些光照条件下可能会表现异常。
性能影响
新着色器集确实多了一些指令,用于在色彩空间之间转换和调整参数,还加上了新的双层雾。但旧着色器集原本就有两种雾变体——Kunos 原版雾和 CSP 的雾。所以总体而言,性能影响应该几乎可以忽略。对了,如果你没有使用 YEBIS 替换,它可能会增加一个额外的后期处理步骤;但如果你确实在乎性能,YEBIS 替换本身就不妨一试(默认 WeatherFX 风格所用的那个 YEBIS 替换,其自动曝光在 0.2.3 中已大幅改进)。
材质配置技巧
目前的主要建议是:除非你使用 fuPBR 着色器,请暂时以旧色彩空间为目标,而不是为线性色彩空间重新调整材质,尤其是当你使用那些 [INCLUDE: common/material…] 文件里的材质模板时。材质参数的映射将来可能会改成更合适的方式,届时旧材质可能会变得更好,而专门为线性色彩空间适配的材质反而可能出问题。
当然,以上这些都不适用于 fuPBR 着色器。它们直接使用常规的 PBR 贴图即可,无需任何调整。如果效果看起来不对,请不要去修改反射率或粗糙度贴图之类的东西,而应更新 fuPBR 着色器本身,使其尽可能精确——毕竟这才是正确的 PBR 的意义所在。
对了,说到材质,有一点要注意:别把东西弄得过暗。对于常规贴图和材质,ksAmbient/ksDiffuse 的值应在 0.4…0.6 左右,并且请尽量让两者接近。也有例外:例如你在做一条森林中的赛道,且出于某种原因不想烘焙顶点 AO,可以把树下表面的 ksAmbient 设得低得多;但对于常规材质,请尽量让这些值保持接近。
Lua 脚本技巧
现在编写脚本时有两个要点需要考虑。首先,如果你在主渲染 pass 中用着色器绘制任何内容,可能需要更新你的着色器。使用 USE_LINEAR_COLOR_SPACE(#if USE_LINEAR_COLOR_SPACE 和 if (USE_LINEAR_COLOR_SPACE) 两种写法都可以)来在修复生效时改变部分逻辑。你还可以用 toLinearColorSpace() 和 toSrgbColorSpace() 转换到线性色彩空间再转回来。例如,如果你在计算中读取漫反射贴图并将其乘以某个相当于 ksDiffuse 的值,那么在线性色彩空间下应把结果传给 toLinearColorSpace() 处理,效果会很好(无需在此写分支:若线性色彩空间被禁用,toLinearColorSpace() 不会做任何事,只会原样返回输入)。
另一个要点是:如果你想输出包含场景画面的输入纹理(例如输出到车内部仪表盘的 LDR 纹理上),该如何处理。一般来说,务必让 HDR 场景输入经过 convertHDR() 处理。在线性色彩空间下,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 左右,因为千分位上有大量未使用的精度。灯光也一样:为了优化数据交换,灯光颜色在传给 GPU 时以 float16 格式存储,因此颜色值为 200 的灯光会被截断(clamp)。这正是 ac.useLinearColorSpace() 第二个参数的用途。把它设成 100 之类,灯光颜色在发送到 GPU 时会除以 100,而 LightingFX 的贡献稍后在 GPU 侧再乘回 100。
在更新默认 WeatherFX 风格时,我发现了不少 bug,多到无法全部用选项解决,因此除了切换色彩空间之外,该函数还会应用一批修复,改变某些函数的行为:
- 云现在会考虑天空雾偏移和指数,此前它们忽略这些值;
- 远处辉光的亮度不会再被场景亮度乘数影响两次;
- 白色参考点现在会受场景亮度乘数影响;
- 月亮的米氏散射(mie)在 v2 天空着色器下正常工作(此前由于一个笔误,它没有被加进最终结果);
- 计算吸收的函数不再混淆 v2 天空朝向太阳和背向太阳的数值(这个 bug 尤其严重,抱歉 🤦♂️);
- 体积光现在使用亮度乘数;
ac.setBrightnessMult()现在会缩放其他数值(例如以前你必须先设亮度乘数、再设雾颜色才能让其生效;现在可以先设雾颜色,再改亮度乘数,雾颜色会随之重新缩放);- 天空颜色计算此前完全没有把太阳颜色考虑在内。
另一个大变化在雾函数上。旧着色器集有两种雾实现:一种是 Kunos 风格的,另一种是来自这篇文章的基于高度的雾,我以前经常在 ShaderToy 上看到它被使用。遗憾的是,在正确的线性色彩空间下,事情变得清楚了:它并不完全符合我们的需要。由于基于高度的特性,它总会在某个距离达到 1(至少我这个版本是这样,也许我漏掉了什么,但我没能弄清怎么正确使用它)。正确的雾绝不应达到 1——正因如此,明亮有光泽的物体在雾中能比暗色物体被看到远得多的距离。于是它被由 ac.setNearbyFog() 控制的新雾所取代,这是可以叠加在场景之上的第二层雾,用于真正的雾和薄霭。而旧式雾在新的着色器集中则被简化,用于远处的霾或简单的大气光吸收,所以现在有两层雾可以搭配使用。
当然,正确的线性色彩空间还需要一个后期处理步骤,把线性图像转回 sRGB。默认情况下,CSP 会在把图像送去 YEBIS 做后期处理之前替你完成这一步,使用的是可通过 ac.setHDRToLDRConversionHints() 提供的数值;但如果你在实现自定义的后期处理,也许会想用一些特别的做法。例如,默认 WeatherFX 风格就用了另一种线性→sRGB 转换,理论上更精确。把线性→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” 中的某个复选框控制时)。