色彩与色彩空间理论略

1 伽马矫正

Gamma 校正

Gamma 是指对线性 rgb 值和非线性视频信号之间进行编码和解码的操作。

颜色空间

Pasted image 20221030155028

图中可以看到,sRGB 和 Rec. 709 的色域是差不多的,三原色的位置是相同的,那么它们之间的区别就是:传递函数不同

传递函数

  • 什么是传递函数:知道了颜色的颜色值之后,想要在电子设备上显示,就需要把它转换为视频信号,传递函数就是用来做转换的。

  • 一个传递函数包括两部分:

    • 光转电传递函数(OETF),把场景线性光转到非线性视频信号值。
    • 电转光传递函数(EOTF),把非线性视频信号值转到显示光亮度
    • 一个简单理解:拍照时,将照片存储在内存卡中,就是用视频信号存储的,如果要看这个照片,就把视频信号再转换成光信号。
      Pasted image 20221030155110
      传递函数其实就是 Gamma 校正所使用的函数。

简单定义

线性空间(Linear Space)是 RGB 颜色都处于函数曲线的对角直线状态(下图),处于线性空间的颜色值是线性变化的,而不会有任何非线性改变,用公式来表达是:$x’=f (x) = x$。
1679151996755

与线性空间相对立的是 Gamma 空间,它是有着指数级的函数曲线的变换空间,Gamma 曲线计算公式:
$$x’= f(x) = x^n$$
其中,$x$ 是颜色 $r,g,b$ 任一分量的值,值域是 $[0.0, 1.0]$,$n$ 是 Gamma 校正指数。下图中,分别是 $n$ 取 $0.45$, $1.0$, $2.2$ 的 Gamma 空间色调曲线图。
1679151996861|400

1.0 是线性空间,输入输出值一样;0.45 和 2.2 是 Gamma 曲线,处于此空间的色彩将被提亮或压暗,并且 $0.45 \cdot 2.2 \approx 1.0$,以保证两次 Gamma 校正之后能够恢复到线性空间:

$$x’ = f_{gamma2.2}(f_{gamma0.45}(x)) = ({x^{0.45}})^{2.2} = x^{0.45 \cdot 2.2} = x^{0.99} \approx x$$

从左到右:指数 0.45 的 Gamma 校正图像,1.0 的线性空间的原始图像,指数 2.2 的 Gamma 校正图像。

上图左执行 2.2 的 Gamma 校正后将得到中间图像;同样,上图右执行 0.45 的 Gamma 校正后也可以得到中间图像。

编码和解码的理解

  • 拍到的照片,存在电脑里,就是把自然界中的光信号编码为视频信号 (我们常用的格式如 srgb 都是0.45)
  • 查看照片时,就要把视频信号还原为线性的光信号,进行解码操作 (2.2),显示器进行 Gamma 校正。

用一张图来举例:
Pasted image 20221030155903
gamma 编码:

  • 左图为存在硬盘中,将捕获到的物理数据做一次 gamma 值约为 0.45 的映射,这个过程称为 gamma 编码,图片变亮
    Pasted image 20221030171028

gamma 校正:

  • 中间为显示图像时,需要为每一个像素做一次 gamma 值约为 2.2 的校正,来使的最终结果为正确的物理数据。图片变灰

为什么不用线性的方式存储,而要来回转换呢?

  • ①和人眼的特性有关

  • 人眼对暗部的变化感应更敏感

  • ②非线性转换为了优化存储空间和带宽

  • 我们用于显示图像数据都是 8bit,为了充分利用带宽,就需要使用更多位置去存储暗部值。也就是暗部使用高精度保存,亮部使用相对较低精度保存。

韦伯定律与 CRT

美术上的均匀和物理上的均匀

Pasted image 20221030160526
美术上的中灰色 and 物理上的中灰色
那一个变化更均匀?

  • 正如上边所说,我们人眼对于暗部是更敏感的,视觉上人们会认为上面这条线亮度是均匀增加的,实际在物理上下面这条线亮度均匀增加。
  • 上边是视觉上的均匀变化,而下边是物理量上的均匀变化。
  • 补充:理论上上边的中灰是物理量(下边)的 21.8%,视觉上认为的美术中灰色,大约是物理中灰色的 20%

Gamma 编码曲线

Pasted image 20221030160838

  • gamma 编码的曲线:
    将自然界线性增长的灰阶变化和人心理上感受到的灰阶变化做一个映射,得到 Gamma 编码的曲线。

  • 由图中可以看到

  • 自然界中亮度的 0.2 左右的亮度,对应的就是人眼感受到的中灰色(0.5)

  • 将 0.2 以下看作暗部,0.2 以上看作亮部,可以看到暗部的变化率更高,也就是说人眼对暗部的变化感受更敏感。

韦伯定律(用 gamma 校正的一个原因)

Pasted image 20221030160904
简单来说就是:当所受刺激越大时,需要增加的刺激也要足够大才会让人感觉到明显的变化,但是只适用于中等强度的刺激。

CRT 非线性响应(用 gamma 校正的另外一个原因)

Pasted image 20221030161656

CRT 与转换函数

  • CRT(阴极射线显像管)
    Pasted image 20221030161813
    在物理世界中,如果光的强度增加一倍,那么亮度也会增加一倍,这是线性关系。
    早期 CRT(阴极射线显像管)设备的亮度和电压不成线性关系,而是呈亮度增加量等于电压增加量的 2.2 次幂的非线性关系
    Pasted image 20221030170827
    2.2 也叫做该显示器的Gamma值,现代显示器的 Gamma 值也都大约是 2.2

  • 由于 CRT 的这个物理特性,刚好可以把亮度压暗,也就说,左图变亮的情况下,经过右图显示器的压低亮度校正,结果刚好可以显示正常(有趣的巧合)。

  • CRT 设备的显示原理:通过一个电子束去攻击屏幕上的凝光质图层,发射电子束的电子枪的电压和屏幕上产生的光强成非线性关系。
    Pasted image 20221030162052

    左图(提亮):gamma 编码的曲线:人眼对于物理光强度的一个响应曲线
    右图(压暗):crt 电压与屏幕亮度关系的曲线

中灰值

  • 所谓的中灰值,并非某个固定的具体数值,而是取决于视觉感受

一个例子可以证明:

  • 对于第一张图,可以很明显看到 AB 颜色不同Pasted image 20221030162541|500
    对于下面这张图,只是把 AB 连起来,就可以看到,其实是一种颜色
    Pasted image 20221030162554|500

线性工作流

线性空间与 Gamma 空间

线性空间:相机捕捉到的真实世界光信号
Gamma 空间:对线性空间颜色值编码,把场景线性光转到非线性视频信号值。

线性空间对于计算机图形学中,有着至关重要的作用,它能够保证在 GPU 的 shader 中经过多次复杂的计算后,依然能够得到正确的颜色值;反之,如果是在 Gamma 空间执行 shader 计算,将得到误差很大的画面效果。

正因为线性空间能够得到更加准确的渲染结果,所以主流商业引擎(UE、Unity、CryEngine、Frostbite 等)都支持了线性空间渲染管线。

线性空间渲染管线(下半部分)在 shader 前期去除了 Gamma 校正,在 shader 后期恢复 Gamma 校正。

对于技术美术来说,知道上边所说的还不够,因为很多时候我们会接触到一些图形效果的制作和修改。
这时候就需要一个正确的工作流程。所谓 线性工作流,就是在各个环节正确的使用 gamma 编码/解码,来达到最终输出的数据和最初输入的物理数据一致的目的。
Pasted image 20221030162743|500

如果使用 Gamma 空间的贴图,在传给着色器之前需要从 Gamma 空间转到线性空间

目的是在着色器中做一些渲染计算时会使用线性空间的颜色值,这样就不会出现一些显示错误的结果。

如果不在线性空间下进行渲染工作,可能会产生的问题:

  • ①亮度叠加时
    前文提到,图像存储在硬盘中,会将捕获到的物理数据做一次 gamma 值约为 0.45 的映射,这个过程称为 gamma 编码。
    线性空间下,原本四个亮度为 0.098 的值,进行叠加后应该是 0.196。结果由于在 Gamma 空间下,他们都是 0.0980.45,也就是四个 0.35 叠加,叠加后为 1.4,亮度>1,产生过曝现象。

  • 可以看到非线性空间下亮度叠加出现了过曝(亮度>1 的)的情况

  • 因为 Gamma 空间经过 gamma 编码后的亮度值相对之前会变大
    Pasted image 20221030163030

  • ②颜色混合时

  • 如果在混合前没有非线性的颜色进行转换,就会在纯色的边界出现一些黑边。
    Pasted image 20221030163044

光照计算时

  • 在光照渲染结算时,如果我们把非线性空间下(视觉上的)的中灰色 0.5 当做实际物理光强为 0.5 来计算时,就会出现左边这种情况
  • 在显示空间下是 0.5,但在渲染空间下它的实际物理光强为 0.18(如右图)
    Pasted image 20221030163101

Unity 中的颜色空间

在 Unity 中选择颜色空间

  • 点击菜单 -> Project Settings -> Player 页签 -> Other Settings 下的 Rendering 部分,通过修改 Color Space 可以来选择 Gamma/Linear(线性)

  • Pasted image 20221030164250

  • 当选择 Gamma Space 时

  • Unity 不会做任何操作(默认 Gamma)

  • **当选择 Linear Space 时

  • 引擎的渲染流程在线性空间计算,理想情况下项目使用线性空间的贴图颜色,不需要勾选 sRGB;

  • 如果勾选了 sRGB 的贴图,Unity 会通过硬件特性采样时进行线性转换。

Unity 主要通过以下两个硬件特性来支持

  • sRGB Frame Buffer

    • 将 Shader 的计算结果输出到显示器前做 Gamma 校正
    • 作为纹理被读取时会自动把存储的颜色从 sRBG 空间转换到线性空间
    • 调用 ReadPixels()、ReadBackImage()时,会直接返回 sRGB 空间下的颜色
    • sRBG Frame Buffer 只支持每通道为 8bit 的格式,不支持 float 浮点格式
    • HDR 开启后会先把渲染结果会知道浮点格式的 FB 中,最后绘制到 sRGB FB 上输出。
  • sRGB Sampler

    • 将 sRBG 的贴图进行线性采样的转换
    • 使用硬件特性完成 sRGB 贴图的线性采样和 shader 计算结果的 gamma 校正,比在 shader 里对贴图采样和计算结果的校正要快。

资源导出问题/注意事项

SubstancePainter

Pasted image 20221030164600

  • SubstancePainter 的贴图导出时,其线性的颜色值经过了 gamma 编码,所以颜色被提亮了。
  • 此时这个贴图需要在 Unity 中勾选 sRBG 选项(让 unity 知道这个贴图在 gammar 空间),来让它被采样时能还原回线性值。

PS

Pasted image 20221030164711

  • 如果使用线性空间,一般来说 PS 可以什么都不改,导出的帖图只要勾上 sRGB 就可以了。
  • 如果调整 PS 的伽马值为 1,导出的贴图在 Unity 中也不需要勾选 sRGB 了。
    Pasted image 20221030164917

半透明效果

  • Unity 中

  • Unity 进行半透明混合时,会先将它们转换到一个线性空间下然后再混合

  • PS 中

  • PS 的图层和图层之间做混合时,每个上层的图层都会读取他们的 Color Profile(gamma 值),然后经过一个 gamma 变换再做混合,这样做得结果就会偏暗一些。

  • (可以在它的工作空间的设置中进行更改,选择用灰度系数混合 RGB 颜色,参数设置为一,这样图层才是一个最终直接混合的结果)
    Pasted image 20221030165103

⭐【先看】知乎总结

Gamma、Linear、sRGB 和Unity Color Space,你真懂了吗?
本文将会简单介绍 Gamma、Linear、sRGB 和伽马校正的概念。接着通过实例解析统一到线性空间的步骤,最后介绍如何在 Unity 中实施相应的工作流。

什么是 Linear、Gamma、sRGB 和伽马校正?

在物理世界中,如果光的强度增加一倍,那么亮度也会增加一倍,这是线性关系

而历史上最早的显示器 (阴极射线管)显示图像的时候,电压增加一倍,亮度并不跟着增加一倍。即输出亮度和电压并不是成线性关系的,而是呈亮度增加量等于电压增加量的 2.2 次幂的非线性关系:

Pasted image 20221030172034

2.2 也叫做该显示器的Gamma值,现代显示器的 Gamma 值也都大约是 2.2

[!NOTE]
现在的液晶显示器依然保留了 2.2 方的解码 gamma 校正。但这并不是什么历史遗留问题,也不是因为 CRT 的物理特性,而是现代数据编码上实实在在的需求——对物理线性的颜色编码做 0.45 次方的 gamma 校正,目的是为了让颜色编码的亮度分级与人眼主观亮度感受线性对应。这样,在相同的数据位数下,图像数据可以保留更多人眼敏感的信息。以 8 位色为例:由于人眼对暗色调更加敏感,那就对物理线性的颜色做 0.45 次方的处理,也就是编码 gamma。校正完成后,相当于使用了 0128 的范围来表达原来与物理强度保持线性时 055 的亮度变化。因此,显示器做解码 gamma 的目的是为了让便于保存和传输的颜色编码变回物理线性的形式,以便人眼观察显示器时能得到与观察现实世界时相近的感受。

这种关系意味着当电压线性变化时,相对于真实世界来说,亮度的变化在暗处变换较慢,暗占据的数据范围更广,颜色整体会偏暗。

如图,直线代表物理世界的线性空间(Linear Space),下曲线是显示器输出的Gamma2.2 空间(Gamma Space)
Pasted image 20221030173023

好了,正常情况下,人眼看物理世界感知到了正常的亮度。而如果显示器输出一个颜色后再被你看到,即相当于走了一次 Gamma2.2 曲线的调整,这下子颜色就变暗了。如果我们在显示器输出之前,做一个操作把显示器的 Gamma2.2 影响平衡掉,那就和人眼直接观察物理世界一样了!这个平衡的操作就叫做伽马校正。

在数学上,伽马校正是一个约 0.45 的幂运算(和上面的 2.2 次幂互为逆运算):
Pasted image 20221030173109
Pasted image 20221030173124

左 (Gamma0.45) 中 (Gamma2.2) 右 (线性物理空间)

经过 0.45 幂运算,再由显示器经过 2.2 次幂输出,最后的颜色就和实际物理空间的一致了。

最后,什么是 sRGB 呢? 1996 年,微软和惠普一起开发了一种标准sRGB色彩空间。这种标准得到许多业界厂商的支持。sRGB 对应的是 Gamma0.45 所在的空间

为什么 sRGB 在 Gamma0.45 空间?

假设你用数码相机拍一张照片,你看了看照相机屏幕上显示的结果和物理世界是一样的。可是照相机要怎么保存这张图片,使得它在所有显示器上都一样呢? 可别忘了所有显示器都带 Gamma2.2。反推一下,那照片只能保存在 Gamma0.45 空间,经过显示器的 Gamma2.2 调整后,才和你现在看到的一样。换句话说,sRGB 格式相当于对物理空间的颜色做了一次伽马校正

还有另外一种解释,和人眼对暗的感知更加敏感的事实有关。
Pasted image 20221030173401
如图,在真实世界中(下方),如果光的强度从 0.0 逐步增加到 1.0,那么亮度应该是线性增加的。但是对于人眼来说(上方),感知到的亮度变化却不是线性的,而是在暗的地方有更多的细节。换句话说,我们应该用更大的数据范围来存暗色,用较小的数据范围来存亮色。 这就是 sRGB 格式做的,定义在 Gamma0.45 空间。而且还有一个好处就是,由于显示器自带 Gamma2.2,所以我们不需要额外操作显示器就能显示回正确的颜色

以上内容,看完后还是不懂也没关系,在继续之前你可以先死记住以下几个知识点:

  • 显示器的输出在 Gamma2.2 空间。
  • 伽马校正会将颜色转换到 Gamma0.45 空间。
  • 伽马校正和显示器输出平衡之后,结果就是 Gamma1.0 的线性空间。
  • sRGB 对应 Gamma0.45 空间。

统一到线性空间

现在假设你对上文的概念有一定认识了,我们来讲重点吧。

在 Gamma 或 Linear 空间的渲染结果是不同的,从表现上说,在 Gamma Space 中渲染会偏暗,在 Linear Space 中渲染会更接近物理世界,更真实:

Pasted image 20221030173512

左(Gamma Space),右(Linear Space)

为什么 Linear Space 更真实?

你可以这么想,物理世界中的颜色和光照规律都是在线性空间描述的对吧?(光强度增加了一倍,亮度也增加一倍)。而计算机图形学是物理世界视觉的数学模型,Shader 中颜色插值、光照的计算自然也是在线性空间描述的。如果你用一个非线性空间的输入,又在线性空间中计算,那结果就会有一点“不自然”。

换句话说,如果所有的输入,计算,输出,都能统一在线性空间中,那么结果是最真实的,玩家会说这个游戏画质很强很真实。事实上因为计算这一步已经是在线性空间描述的了,所以只要保证输入输出是在线性空间就行了。

所以为什么你的游戏画面不真实呢?因为你可能对此混乱了,你的输入或输出在 Gamma Space,又没搞清楚每个纹理应该在什么 Space,甚至也不知道有没用伽马校正,渲染结果怎么会真实呢?

现在假设我们的目标是获得最真实的渲染,因此需要统一渲染过程在线性空间,怎么做呢?

:统一在 Linear 空间是最真实的,但不代表不统一就是错的。一般来说,如果是画质要求高的作品(如 3A)等,那么都是统一的。没这方面要求的则未必是统一的,还有一些项目追求非真实的渲染,它们也未必需要统一。

统一到线性空间的过程是看起来是这样的,用图中橙色的框表示(现在看不懂图没关系,跟着后面的步骤来一步步看):
Pasted image 20221030173707
我们从橙色框的左上角出发。

第一步,输入的纹理如果是 sRGB(Gamma0.45),那我们要进行一个操作转换到线性空间。这个操作叫做Remove Gamma Correction,在数学上是一个 2.2 的幂运算 c→c2.2 。如果输入不是 sRGB,而是已经在线性空间的纹理了呢?那就可以跳过 Remove Gamma Correction 了。

:美术输出资源时都是在 sRGB 空间的,但 Normal Map 等其他电脑计算出来的纹理则一般在线性空间,即 Linear Texture。详见后文!

第二步,现在输入已经在线性空间了,那么进行 Shader 中光照、插值等计算后就是比较真实的结果了(上文解释了哦~),如果不对 sRGB 进行 Remove Gamma Correction 直接就进入 Shader 计算,那算出来的就会不自然,就像前面那两张球的光照结果一样。

第三步,Shader 计算完成后,需要进行Gamma Correction,从线性空间变换到 Gamma0.45 空间,在数学上是一个约为 0.45 的幂运算 c→c12.2 。如果不进行 Gamma Correction 输出会怎么样?那显示器就会将颜色从线性空间转换到 Gamma2.2 空间,接着再被你看到,结果会更暗。

第四步,经过了前面的 Gamma Correction,显示器输出在了线性空间,这就和人眼看物理世界的过程是一样的了!

我们再举个例子,我们取 sRGB 纹理里面的一个像素,假设其值为 0.73。那么在统一线性空间的过程中,它的值是怎么变化的?
Pasted image 20221030184116|300
第一步,0.73 (上曲线) * [Remove Gamma Correction] = 0.5 (直线)。( 0.732.2=0.5 )

第二步,0.5 (直线) * [Shader] = 0.5 (直线)(假设我们的 Shader 啥也不干保持颜色不变)

第三步,0.5 (直线) * [Gamma Correction] = 0.73 (上曲线)。( 0.51/2.2=0.73 )

第四步,0.73 (上曲线) * [显示器] = 0.5 (直线)。( 0.732.2=0.5 )

如果不进行 Gamma Correction,就会变暗,因为第三步不存在了,第四步就会变成:

0.5 (直线) * [显示器] = 0.218 (下曲线)。( 0.52.2=0.218 )

再对照上面的图琢磨琢磨?

Unity 中的 Color Space

我们回到 Unity,Editor>Project Setting>Player 中的“Color Space”属性可以选择 Gamma 或 Linear 作为 Color Space:
Pasted image 20221030184401
这两者有什么区别呢?

如果选择了 Gamma,那 Unity 不会对输入和输出做任何处理,换句话说,Remove Gamma Correction 、Gamma Correction 都不会发生,除非你自己手动实现。
手动颜色空间转换:
Pasted image 20230723133314

如果选了 Linear,那么就是上文提到的统一线性空间的流程了。对于 sRGB 纹理,Unity 在进行纹理采样之前会自动进行 Remove Gamma Correction,对于 Linear 纹理则没有这一步。而在输出前,Unity 会自动进行 Gamma Correction 再让显示器输出。

怎么告诉 Unity 纹理是 sRGB 还是 Linear 呢?对于特定用途的纹理,你可以直接设置他们所属的类型:如 Normal Map、Light Map 等都是 Linear,设置好类型 Unity 自己会处理他们。
Pasted image 20221030184638
还有一些纹理不是上面的任何类型,但又已经在线性空间了(比如说 Mask 纹理、噪声图),那你需要取消 sRGB 这个选项让它跳过 Remove Gamma Correction 过程

UE 中的 Color Space

Unreal中关于颜色的技术分享_百科TA说 (baidu.com)
用过 unreal 的小伙伴应该都会注意到,我们在 unreal 里面进行贴图设置的时候,对于 basecolor 都需要勾选上 sRGB。为什么需要勾选?每张贴图都需要勾选么?如果不做勾选会怎么样?这就需要用我们的 gamma 校正和线性空间来破案了。接下来就一起来看这篇 Unreal 中关于颜色的技术分享。
Pasted image 20221030185833

Gamma 校正

首先什么是 gamma 校正。官方解释,RGB 值与功率并非简单的线性关系,而是幂函数关系,这个函数的指数称作 Gamma 值,一般为 2.2(power2.2),而这个换算过程,称为 Gamma 校正。官方来源,开发 gamma 编码是用来抵消阴极射线管(CRT)显示器的输入和输出特性,电子枪的电流,也是光的亮度,与输入的正极电压的变化是非线性的。通过 gamma 压缩来改变输入信号抵消了这个非线性,因此输出图像就能有预期的亮度。

画图来理解就是如下,

如果我们有一张线性的照片,如果我们显示器也是线性的,那经过显示器输出的图像就应该和真实的图像是一样的;
Pasted image 20221030185921
但是实际上我们的显示器根本不按套路来,它的 gamma 值是 2.2,所以如果我们的图片是线性的,那么从 gamma 为 2.2 的显示器中输出出来就是下面这个样子
Pasted image 20221030185928
可以看到结果有明显的色彩失真,所以如果我们把照片的 gamma 值设置成 1/2.2 的话,经过两次调整,结果就是正确的啦

Pasted image 20221030185956
在进行 gamma 校正的方式就是采样进行输入的时候,Gamma=1/2.2,调亮 Gamma;

在显示输出的时候 Gamma=2.2,调暗 Gamma。
Pasted image 20221030190026
线性空间

一般在图片的渲染中存在两个颜色空间,第一个是 Gamma(非线性)的颜色空间;然后是 Linear(线性)颜色空间。Gamma 使用的是进行了校正的颜色表;而 linear 使用的是一个线性的完整颜色表,而且渲染中用到的光线也是线性空间的,所以我们在进行计算的时候要在线性空间中进行,输入和输出需要进行 gamma 校正。

最好的办法就是在图片输入的时候采用 sRGB 格式,目的是为了告诉 linear color space,需要对输入的颜色进行 power2.2 校正切换到线性空间,然后再进行 shader 计算,计算完毕以后再通过 power1/2.2 切换回 gamma 空间。所以解决了我们刚开始提的在 unreal 中的basecolor 需要勾选 sRGB 选项。而非 sRGB 纹理则会直接在 shader 中进行计算,比如 normal 和 mask。

所以以上就解释了我们在导入贴图的时候需要注意到的问题,只有勾选了引擎才会进行正确的像素计算;一般 basecolor 才需要勾选;对于 basecolor 来说,不勾选 GPU 就不会进行 gamma 校正,而直接使用存储的值进行渲染,但同时也不会得到真实的效果。

到底什么纹理应该是 sRGB,什么是 Linear?

关于这一点,我个人有一个理解:所有需要人眼参与被创作出来的纹理,都应是 sRGB(如美术画出来的图)。所有通过计算机计算出来的纹理(如噪声,Mask,LightMap)都应是 Linear

这很好解释,人眼看东西才需要考虑显示特性和校正的问题。而对计算机来说不需要,在计算机看来只是普通数据,自然直接选择 Linear 是最好的。

除了纹理外,在 Linear Space 下,Shaderlab 中的颜色输入也会被认为是 sRGB 颜色,会自动进行 Gamma Correction Removed。

有时候你可能需要想让一个 Float 变量也进行 Gamma Correction Removed,那么就需要在 ShaderLab 中使用[Gamma]前缀:

1
[Gamma]_Metallic("Metallic",Range(0,1))=0

如上面的代码,来自官方的 Standard Shader 源代码,其中的_Metallic 这一项就带了[Gamma]前缀,表示在 Lienar Space 下 Unity 要将其认为在 sRGB 空间,进行 Gamma Correction Removed。

扩展:为什么官方源代码中_Metallic 项需要加[Gamma]?
这和底层的光照计算中考虑能量守恒的部分有关,Metallic 代表了物体的“金属度”,如果值越大则反射 (高光)越强,漫反射会越弱。在实际的计算中,这个强弱的计算和 Color Space 有关,所以需要加上[Gamma]项。

虽然 Linear 是最真实的,但是 Gamma 毕竟少了中间处理,渲染开销会更低,效率会更高。上文也说过不真实不代表是错的,毕竟图形学第一定律:如果它看上去是对的,那么它就是对的

:在 Android 上,Linear 只在 OpenGL ES 3.0 和 Android 4.3 以上支持,iOS 则只有 Metal 才支持。

在早期移动端上不支持 Linear Space 流程,所以需要考虑更多。不过随着现在手机游戏的发展,越来越多追求真实的项目出现,很多项目都选择直接在 Linear Space 下工作。

一旦确定好 Color Space,那么就需要渲染工程师、技术美术和美术商量和统一好工作流了。在中小团队或项目中,这些概念很容易被忽略,导致工作流混乱,渲染效果不尽人意。现在你懂了吗?

2 HDR 和 LDR

基本概念

  • Dynamic Range(动态范围)=最高亮度/最低亮度
  • HDR= High Dynamic Range
  • LDR = Low Dynamic Range(我们日常看到的)
  • ToneMapping:将超高的动态范围(HDR)转换到我们日常显示的屏幕上的低动态范围(LDR)的过程
    Pasted image 20221030192152
  • 一些小芝士:
  •   因为不同的厂家生产的屏幕亮度(物理)实际上是不统一的,那么我们在说**LDR 时,它是一个 0 到 1 范围的值**,对应到不同的屏幕上就是匹配当前屏幕的最低亮度(0)和最高亮度(1)
    
  •   自然界中的亮度差异是非常大的。例如,蜡烛的光强度大约为 15,而太阳光的强度大约为 10w。这中间的差异是非常大的,有着超级高的动态范围。将太阳光显示在屏幕上就是 ToneMapping。
    
  •   我们日常使用的屏幕,其最高亮度是经过一系列经验积累的,所以使用、用起来不会对眼睛有伤害;但自然界中的,比如我们直视太阳时,实际上是会对眼睛产生伤害的。
    
  • 相机是如何将 HDR 映射到 LDR 的
  • 首先将曝光值进行计算,映射到相机可以感应的范围
  • 受光圈、快门、传感器的灵敏度等影响
  • 然后把这个值输入为线性的值,存储到图片中(一般为 raw 格式)
  • 之后会经过一个变化(LUT),通过白平衡、色彩校正、色调映射、伽马校正这个过程,最后的结果烘焙成 LUT(pbr 中 LUT 的图,就是这个过程的结果)
  • 每个相机厂商的 LUT 格式不太一样。

为什么需要 HDR

Pasted image 20221030192615

  • LDR 只能将现实中的颜色压缩再呈现出来
  • HDR 可以由更好的色彩,更高的动态范围和更丰富的细节。
  • 可以有效防止画面过曝,超过 1 的亮度值的色彩也能很好地表现,像素光亮度变得很正常,视觉传达更真实
  • HDR 才有超过 1 的数值,才会有bloom效果,高质量的 bloom 效果能体现出画面的渲染品质
    Pasted image 20221030192649

Unity 中的 HDR

1. Camera 中的 HDR 设置

Pasted image 20221030192735

  • 开启的话,会将场景渲染为 HDR 图像缓冲区
  • 下一步进行屏幕后处理:Bloom 和 ToneMapping
  • 在 ToneMapping 过程中,会把 HDR 转换为 LDR
  • LDR 的图像会发送给显示器

2. Lightmap 的 HDR 设置

  • 选择 High Quality 将启用 HDR 光照贴图的支持,选择 Normal Quality 将切换为使用 RGBM 编码
  • RGBM 编码:将颜色存储在 RGB 通道中,将乘数(M)存储在 Alpha 通道中
    Pasted image 20221030192844

3. 拾色器的 HDR 设置

Pasted image 20221030192922
Pasted image 20221030192919

  • 如果将 Property 的颜色参数的前边加上[HDR]就会将其标识为 HDR
  • 颜色设置为 HDR,那么拾色器中就会出现一个 Intensity 的滑条用来调整强度
  • 滑条每增加 1,提供的光强度增加一倍。

4. HDR 的优点、缺点

  • 优点

  • 画面中亮度超过 1 的部分不会被截掉,增加了亮部的细节,减少了曝光

  • 减少画面暗部的色阶感

  • 更好的支持 bloom 效果

  • 缺点

  • 渲染速度慢,需要更多显存

  • 不支持硬件抗锯齿

  • 部分低端手机不支持

HDR 与 ToneMapping

1. ToneMapping 概念

启用 HDR 的应用程序面临一个问题:应用程序阶段的 HDR 数据格式很可能与显示设备所需的 HDR 格式不一致,或者显示设备压根不支持 HDR,只支持 LDR 格式。

为了解决以上这个问题,就需要引入色调映射(Tone Mapping),它可以将 HDR 颜色变换成 LDR 格式或者另一种 HDR 格式(VDR)。

  • 前边的回顾:LDR 范围为 0 到 1,HDR 可以超过 1,;

  • ToneMapping 的概念:想要在显示器上表现更高动态范围的颜色,就要把 HDR 压缩为 LDR(这个过程就是 ToneMapping),这种映射关系就是色调映射。

  • 下边例子是一个线性的亮度映射,但这并不符合我们对真实世界的理解,因此,基本上所有的映射最后都是通过一个曲线来实现。
    Pasted image 20221030193256|500

2. ACES 曲线

Pasted image 20221102204612

  • Academy Color Encording System 学院颜色编码系统
  • 是最流行、最被广泛使用的 ToneMapping 映射曲线
  • 效果:对比度提高,压暗暗部使暗部变化更不明显。能很好的保留暗部和亮部的细节。之后在这个基础上再进行调色
    Pasted image 20221030193438|500

3. 其他类型的 ToneMapping 算法

Tone mapping进化论

4. LUT(Lookup Table)

Pasted image 20221030193623

  • 简单的理解:就是滤镜,通过 LUT,你可以将一组 RGB 值输出为另一组 RGB 值,从而改变画面的曝光与色彩
  • 和 ToneMapping 不同,LUT 是在 LDR 之间做变化。而 ToneMapping 是对 HDR 做变换的。
  • 调整 RGB 三通道的 LUT 被称为 3D LUT
    • 格式有如下几种
    • Pasted image 20221030193651
  • 小 trick:可以在 PS 中调整 LUT,导出的 LUT 作为滤镜调整画面
    Pasted image 20221030193716
    UE4 的后处理滤镜部分中也有 LUT 相应的位置
    Pasted image 20221030193748

3 HSV 和 RGB 转换

RGB比较常用(负数都显示成黑色)
HSV (Hue, Saturation, Value)
色调(H)、饱和度(S)和明度(V)
Hexadecimal:十六进制
Pasted image 20221003152912|200

转自:Unity Shader - HSV 和 RGB 的相互转换 - 知乎 (zhihu.com)
对于颜色值,RGB 可能是我们接触最多的颜色模型,图像中的任何颜色都是由红色(R)绿色(G)蓝色(B) 这三个通道合成的,这三种颜色可以组合成几乎所有的颜色。

然而,它并不直观,比如我随便说一个 rgb 值,你能猜到他是什么颜色吗?几乎不可能,所以,后面引入了HSVHSL等颜色模型。 HSV 相对于 RGB 来说是一种更加直观的颜色模型,HSV更加符合我们人类视觉。

HSL 和 HSV 概念

HSL 即色相、饱和度、亮度(英语:Hue, Saturation, Lightness)。

HSV 即色相、饱和度、明度(英语:Hue, Saturation, Value),又称 HSB,其中 B 即英语:Brightness。

  • 色相(H)是色彩的基本属性,就是平常所说的颜色名称,如红色、黄色等。
  • 饱和度(S)是指色彩的纯度,越高色彩越纯,低则逐渐变灰,取 0-100%的数值。
  • 明度(V),亮度(L),取 0-100%

HSL 和 HSV 色彩空间比较

二者在数学上都是圆柱,但

HSV 在概念上可以被认为是颜色的倒圆锥体(白色在上底面圆心,黑点在下顶点);

HSL 在概念上表示了一个双圆锥体和圆球体(白色在上顶点,黑色在下顶点,最大横切面的圆心是半程灰色)。
Pasted image 20221024161403|400
v2-33e2c70d429d9079c9b53e2f570032aa_1440w 2
以下函数由国外大神 Inigo Quilez 提供 https://www.shadertoy.com/view/MsS3

HSB/HSV 转 RGB

[!bug]
Color.hlsl 中已经内置了函数,不用自己写了!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// Official HSV to RGB conversion 
vec3 hsv2rgb( in vec3 c )
{
vec3 rgb = clamp( abs(mod(c.x*6.0+vec3(0.0,4.0,2.0),6.0)-3.0)-1.0, 0.0, 1.0 );

return c.z * mix( vec3(1.0), rgb, c.y);
}
// Smooth HSV to RGB conversion
// https://www.shadertoy.com/view/MsS3Wc
vec3 hsv2rgb_smooth( in vec3 c )
{
vec3 rgb = clamp( abs(mod(c.x*6.0+vec3(0.0,4.0,2.0),6.0)-3.0)-1.0, 0.0, 1.0 );

rgb = rgb*rgb*(3.0-2.0*rgb); // cubic smoothing

return c.z * mix( vec3(1.0), rgb, c.y);
}

ShaderLab 版:

1
2
3
4
5
float3 hsb2rgb( float3 c ){
float3 rgb = clamp( abs(fmod(c.x*6.0+float3(0.0,4.0,2.0),6)-3.0)-1.0, 0, 1);
rgb = rgb*rgb*(3.0-2.0*rgb);
return c.z * lerp( float3(1,1,1), rgb, c.y);
}

RGB 转 HSB/HSV

1
2
3
4
5
6
7
8
vec3 rgb2hsb( in vec3 c ){
vec4 K = vec4(0.0, -1.0 / 3.0, 2.0 / 3.0, -1.0);
vec4 p = mix(vec4(c.bg, K.wz),vec4(c.gb, K.xy),step(c.b, c.g));
vec4 q = mix(vec4(p.xyw, c.r),vec4(c.r, p.yzx),step(p.x, c.r));
float d = q.x - min(q.w, q.y);
float e = 1.0e-10;
return vec3(abs(q.z + (q.w - q.y) / (6.0 * d + e)),d / (q.x + e),q.x);
}

ShaderLab 版:

1
2
3
4
5
6
7
8
9
10
float3 RGB2HSV(float3 c)
{
float4 K = float4(0.0, -1.0 / 3.0, 2.0 / 3.0, -1.0);
float4 p = lerp(float4(c.bg, K.wz), float4(c.gb, K.xy), step(c.b, c.g));
float4 q = lerp(float4(p.xyw, c.r), float4(c.r, p.yzx), step(p.x, c.r));

float d = q.x - min(q.w, q.y);
float e = 1.0e-10;
return float3(abs(q.z + (q.w - q.y) / (6.0 * d + e)), d / (q.x + e), q.x);
}

实践

下面我们在 Unity Shader 中来看看

笛卡尔坐标系下的 SHV

由下图我们可以清晰的看到 X 轴决定色相,Y 轴决定饱和度
Pasted image 20221024162348

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
Shader "lcl/shader2D/HSV"
{

SubShader
{
Pass
{
CGPROGRAM
// vert_img 是 UnityCG.cginc 内置的
#pragma vertex vert_img
#pragma fragment frag

#include "UnityCG.cginc"

// 该函数由国外大神 Iñigo Quiles 提供
// https://www.shadertoy.com/view/MsS3Wc
float3 hsb2rgb( float3 c ){
float3 rgb = clamp( abs(fmod(c.x*6.0+float3(0.0,4.0,2.0),6)-3.0)-1.0, 0, 1);
rgb = rgb*rgb*(3.0-2.0*rgb);
return c.z * lerp( float3(1,1,1), rgb, c.y);
}

// ---------------------------【片元着色器】---------------------------
fixed4 frag (v2f_img i) : SV_Target
{
fixed4 col;
// hsb 转换为 rgb
// uv.x 决定 色相,
// uv.y 决定 亮度,
col.rgb = hsb2rgb(float3(i.uv.x, 1, 1-i.uv.y));
return col;
}
ENDCG
}
}
}

极坐标系下的 SHV

在极坐标系下,我们可以看到,角度决定色相,半径决定饱和度,亮度固定
Pasted image 20221024162435

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
Shader "lcl/shader2D/HSVInPolarCoordinate"
{

SubShader
{
Pass
{
CGPROGRAM
// vert_img 是 UnityCG.cginc 内置的
#pragma vertex vert_img
#pragma fragment frag

#include "UnityCG.cginc"

#define TWO_PI 6.28318530718

// 该函数由国外大神 Iñigo Quiles 提供
// https://www.shadertoy.com/view/MsS3Wc
float3 hsb2rgb( float3 c ){
float3 rgb = clamp( abs(fmod(c.x*6.0+float3(0.0,4.0,2.0),6)-3.0)-1.0, 0, 1);
rgb = rgb*rgb*(3.0-2.0*rgb);
return c.z * lerp( float3(1,1,1), rgb, c.y);
}

// ---------------------------【片元着色器】---------------------------
fixed4 frag (v2f_img i) : SV_Target
{
fixed4 col;
// 笛卡尔坐标系转换到极坐标系
float2 center = float2(0.5,0.5)-i.uv;
float angle = atan2(center.y,center.x);
float radius = length(center)*2.0;

// 将角度 从(-PI, PI) 映射到 (0,1)范围
// 角度决定色相, 半径决定饱和度, 亮度固定
col.rgb = hsb2rgb(float3((angle/TWO_PI)+0.5,radius,1.0));
return col;
}
ENDCG
}
}
}

改变贴图颜色

Pasted image 20221024163907
实现对 HSV 的修改,可以通过使用_Time 作为值传入 Add,实现动态变化。