UE 中的 伽马矫正 与 sRGB,统一 Color Space 工作流

发布于 2023-02-28  367 次阅读


UE 中的 伽马矫正 与 sRGB,统一 Color Space 工作流

伽马校正(Gamma correction) 又叫伽马非线性化(gamma nonlinearity)、伽马编码(gamma encoding) 或是就只单纯叫伽马(gamma)。是用来针对影片或是影像系统里对于光线的辉度(luminance)或是三色刺激值(tristimulus values)所进行非线性的运算或反运算

这是维基百科里对伽马矫正的描述,单单看起来好像与游戏开发并没有太大的关系,然而在游戏制作过程中,这确是首要需要考虑好的问题,颜色空间的工作流,可能需要程序与美术之间的合作,在制作之初确立好工作流程。而如果工作流程没有统一好,那么后期就可能会出现渲染效果差、调整效果难等等问题,等到问题出现了再去修补,那面临的就不仅仅是工作流的变更,可能还有大批量的资源需要修正,因此理解伽马矫正的原理,统一好工作的颜色空间是很有必要的。

伽马矫正

当我们计算出场景中所有像素的最终颜色以后,我们就必须把它们显示在监视器上。过去,大多数监视器是阴极射线管显示器(CRT)。这些监视器有一个物理特性就是两倍的输入电压产生的不是两倍的亮度。输入电压产生约为输入电压的2.2次幂的亮度,这叫做监视器Gamma。人类所感知的亮度恰好(巧合的)和 CRT 所显示出来相似的指数关系非常匹配。

这是 learnopengl 中的一段描述,理解起来并不难,因为 CRT 的物理特性,电压增长与亮度增长并非是线性的,而是呈指数的关系,即
Lux={U}^{{2.2}}

“输入电压产生约为输入电压的2.2次幂的亮度”,在这种状态下,可以想象得到,高电压区域会比想象中更亮,那么如何使显示器亮度,与自然界一致呈线性变化呢?答案就是输出显示器亮度前,对其矫正。即为了抵消 CRT 的物理特性,我们才使用了指数 Gamma 来进行这一矫正,在输出最终的颜色之前,应用显示器 Gamma 的倒数。
V_{{{\text{out}}}}=A{V_{{{\text{in}}}}}^{{\gamma }}

这里简单写了个 shader toy,可以观察下 gamma 矫正的表现,也可以参考下面的几个链接协助理解。

关于伽马这一值以及矫正的来源有很多(讨论)传说,看了一下大佬的讨论,总体看来偏向于在显示行业的发展中,针对 CRT 制造工艺观测得到的结果,虽然这一结果也巧合的与我们的生理观测特性相符(人类所感知的亮度恰好和CRT所显示出来相似的指数关系非常匹配)。

目前 CRT 设备已经不常见了,但是为了兼容显示影像资料统一显示标准,人们仍在硬件上做了调整来提供兼容性,并使用微软联合爱普生、惠普制定的 sRGB 颜色空间标准,推荐显示器的显示伽马值为 2.2。

这些在本文就不再赘述了,读者有兴趣可以查阅下方链接,高兴大佬的文章看起来还是比较权威的,也是显示行业内的权威人士:

本文主要还是讨论下,伽马矫正对于游戏带来的影响

对游戏的影响

伽马矫正的传说这么多,和游戏又有什么关系呢,到底为什么需要伽马矫正呢?如果没有正确的进行伽马矫正,会怎样影响渲染的效果呢?

上面六个圆是在 shader toy 中模糊边缘,在线性空间与伽马空间下的效果表现。
可以看的出来,右侧三个圆的交界位置似乎明显变暗了,这似乎与我们学过的颜色原理不太一致,左侧看起来就较为正常一些,红绿交界过渡期间产生了黄色,究其原因,就是因为伽马矫正导致的接缝颜色变暗。

以红绿色圆形交界为例,颜色混合时,mix 对红绿颜色进行混合,我们以中间均匀混合位置计算示例,输出的颜色应该为:
\frac{(1.0,0.0,0.0) + (0.0,1.0,0.0)}{2} = (0.5,0.5,0.0)
在我们没有进行 gamma 矫正时,由于 CRT 的物理特性,那么我们看到的颜色就变成了:
((0.5,0.5,0.0))^{2.2} = (0.2176,0.2176,0.0)
也就是说,我们看到的颜色与预期的颜色比较而言变暗了,这也就是在不使用伽马矫正时交界处接缝颜色变暗的原因。
为了抵消显示器 CRT 物理特性导致的颜色变暗,我们就需要在将线性的颜色输出到显示器之前,进行一次 gamma 矫正,应用显示器 Gamma 的倒数,即:
((0.5,0.5,0.0))^{\frac{1}{2.2}} = (0.7297,0.7297,0.0)
将此颜色输出到显示器后,真实显示的颜色就变成了:
(0.7297,0.7297,0.0)^{2.2} = (0.5,0.5,0.5)
才会与真实环境中的表现一致。


这一现象同样也出现在很多的存在图像运算的软件里,比如在 PS 里,当我们使用 PS 的柔边圆工具画出红绿两色时,可以看出,发生了上文中类似的颜色混合变暗的问题。从这里我们也可以看出,默认状态下 PS 的一些工具也并未能将计算统一到线性空间之中(我们常用的高斯模糊、自由变换等也在其列)。

线性工作流

上文简单的使用一个混合的例子举例了 gamma 矫正的影响,然而,在游戏开发过程中,这一现象可能会更加混乱。
比如美术工作在 sRGB 空间下,产出了一些纹理,这也就意味着这些纹理的采样结果是已经经过了一次 gamma 0.45 矫正的,如果在后续的材质处理中,直接使用了这些纹理,那也就造成了在非线性空间进行的计算,这些计算毫无疑问是错误的,会使得渲染结果偏离现实,并且也很难通过后续的调整矫正这一效果。

解决这一问题的办法,首先就是要确保所有的运算都基于线性空间进行,

如果产出的 Texture 是基于 sRGB 空间,那么在进行纹理的计算之前,就需要先移除已经进行了的 Gamma 0.45 矫正,将纹理结果统一到线性空间,再进行后续的计算。
反之,如果 Texture 产出在线性空间上,那么后续的工作就应该继续工作在线性空间上,不应进行多余的矫正。


对于 UE 而言,这一步骤可以通过导入材质的勾选项来处理,如果是工作在 sRGB 空间产出的 Texture,需要勾选这一选项,反之则要取消这一勾选项。

ExpectedSamplerType = Texture->SRGB ? SAMPLERTYPE_Color : SAMPLERTYPE_LinearColor;

在 UE 的材质编辑器中,当我们采样时,对于非 sRGB 的 Texture,采样器细节面板的 SamplerType 会默认勾选使用线性颜色空间,对于勾选了 sRGB 的 Texture,采样器细节面板的 SamplerType 会默认勾选使用 Color,这里的 Color 其实就是 sRGB 颜色空间。这样在后续的计算中,就可以针对不同颜色空间的 Texture,决定是否需要移除伽马矫正,统一转到线性颜色空间来进行采样与计算。

这里我们来简单测试一下:
使用 PS 32位/通道 导出一张美术中灰纹理(拾色值为 (188,188,188) = (0.7372,0.7372,0.7372)),导入 UE 两次,分别勾选 sRGB 与 不勾选 sRGB,并在材质编辑器中采样。

预览结果中,勾选 sRGB 中灰表现正常拾色值为 (188,188,188) = (0.7372,0.7372,0.7372) ,而未勾选 sRGB 的预览拾色值为 (223,223,223) = (0.8745,0.8745,0.8745),恰好为原色值经过一次 gamma 1/2.2 矫正的结果((0.7372,0.7372,0.7372)^\frac{1}{2.2} = (0.87,0.87,0.87)),这里就是 UE 的材质编辑器为了保证预览正确,在预览前进行了一次 gamma 矫正操作导致的色值改变。后续的连线也可以证明,实际计算采用的色值是小于 0.8 的,并非显示出来的 \frac{223}{255} = 0.87,这也就保证了,只要正确的勾选了 sRGB 选项,材质编辑器中的运算均会在线性颜色空间中进行。

因此,合理的方案应当规划好产出 Texture 时使用的颜色空间,并在导入 UE 时,正确的勾选 sRGB 选项,这样才能保证后续计算的正确性。

统一线性工作流

规划好产出 Texture 时使用的颜色空间,势必要有一套相关的规则,那么如何规定产出的规则呢?

按照一些专业软件的规则,例如 Substance Painter 中:

  • 对于提供色彩以供显示的贴图是以 sRGB 形式导出的,这样也便于直接查看贴图的颜色,
  • 而对于法线、金属度、粗糙度等用于存储数值信息、不会用于直接显示的贴图,则不使用 sRGB 编码,仅仅用作存储数据。

然而,规则虽如此,执行起来却还有很多的问题。
在笔者与一些行业内人员沟通中,发现有时可能会使用 PS 等修整制作纹理效果,或手动组合不同通道而非修改 sp 的导出配置,或其他种种,再加之一般也很少有工作在 RGB 32位/通道上的相关人员,在这些操作过程中,或许就已经破坏了线性工作流。

比如上文中笔者导出的中灰纹理,如果 ps 工作在默认模式即 RGB 8位/通道 环境下导出的中灰拾色值就会是(128,128,128),而这一通过肉眼能够看到的颜色导出的数据,势必也会作为 sRGB 导入 UE 并勾选 sRGB 选项。(如果较为了解中灰或许会将 cmyk k值调整为 50 %,此数值已经是正确的,但是是 gamma 2.0 空间下的标准中灰值)
导入后,也就存在了这一错误的纹理,后续的工作流程又何谈正确呢?

PS RGB 8位/通道 与 RGB 32位/通道 (128,128,128) 拾取色值。

小伙伴问为什么拾色值为 (188,188,188) 才是中灰。找了找引擎中的中灰,顺带写在这里。
路径 Engine/Content/ArtTools/RenderToTexture/Textures/127grey.uasset ,此 127° 灰是引擎 ArtTools 中的纹理,其拾色值为 (126,127,126) ,但是其未勾选 sRGB,因此当我们导入材质编辑器采样时,预览拾取色值为 (185,186,185) ,与导出 (188,188,188) 并勾选 sRGB 的结果基本一致。

当然,这里并非是探讨中灰的色值到底应该是多少,这也并没有意义,笔者只是导出这一色值举例。
从引擎中的这一资产中也看也看出,工作在 RGB 8位/通道 下的 PS 无疑并非工作在线性空间,在其他的一些软件中也存在着类似的问题。
或许对一个项目而言,这些问题的解决需要有能够牵头把控的人,做出规范的流程、产出限制,将图程、美术、特效、TA 打通,真正将游戏制作的颜色空间统一到线性空间工作流中。

参考链接:
https://www.youtube.com/watch?v=2sshGdMgJxQ
https://zhuanlan.zhihu.com/p/33637724
https://zhuanlan.zhihu.com/p/66558476
https://blog.csdn.net/WPAPA/article/details/123989752
https://blog.csdn.net/candycat1992/article/details/46228771?spm=1001.2014.3001.5501
https://zhuanlan.zhihu.com/p/142377883
https://learnopengl-cn.github.io/05%20Advanced%20Lighting/02%20Gamma%20Correction/
https://www.zhihu.com/question/27467127

如堕五里雾中
最后更新于 2024-08-01