# 对伽马空间和伽马校正的调研

学习资料来自:

笔记记于 2017/04/25

产生的原因 #

“广义Gamma” 的产生,原因有二:

  • 人眼对自然亮度感知是非线性的。(韦伯定律)
  • 我们用来记录/展示画面的媒介上,灰阶预算是有限的。(无论纸张还是屏幕)

为了在灰阶预算有限的前提下,协调自然亮度和主观灰阶感受这二者的映射关系,Gamma 就产生了。

另一种说法,伽马(gamma)产生的原因有两点:

  • CRT 显示器的历史原因,对显示图像的处理是非线性的;
  • 人眼对明暗色域的辨识度不一样,有时需要对图像进行非线性处理来获得合适的辨识度;

上述所说的非线性处理就叫做伽马纠正或伽马校正(gamma correction)。Unity 默认的空间是伽马空间,在伽马空间下进行渲染会导致很多非线性空间下的计算,从而就会引入一些误差。把伽马空间转换到线性空间,便需要进行伽马校正。

伽马校正就是:对图像的伽马曲线进行编辑,对图像进行非线性色调编辑,检出图像信号中的深色部分和浅色部分,并使两者比例增大,从而提高图像对比度效果。

γ 这个符号读作伽马,特指了幂函数的指数值,这个值除了 1 之外的都是非线性的,见下图:

对应地,不同的 γ 值对图像做伽马校正得到的结果:

据说现实中绝大部分的图片是被 γ 值为 0.45 校正过的图片(纠正了颜色值),是相机和图像编辑软件存储图像时默认的。显示器做了非线性处理,2.2 是 0.45 的倒数,互相抵消得到与原始一致的图像,如果图片没有做伽马校正,显示的将是右图一样颜色被压黑。

另一个形象的例子,如下所示:

保存颜色信息本身矫正称为 Encoding gamma,显示器对颜色的矫正称为 Display gamma,所以一个完整的图形系统中需要两个 Gamma 值,两次矫正刚好在一定程度上抵消(但一般不是完全抵消)。

但是需要注意的是,CRT 之所以利用 Gamma 只是利用它的压缩特性可以来最大化信号的质量,而不管它本身的 Gamma 特性(与人有关的显示特性)。重要的理解:伽马校正一直沿用至今,是由于人眼特性决定的。如果有一天我们对图像的存储空间大大提升,通用格式不再是 8 位而是 32 位的时候,伽马就没用了。因为我们将不需要为了提高精度而把 18 度灰编码成 0.5 像素,我们有足够多的颜色空间可以利用,不需要再考虑人眼的特性。

人眼的特性 #

从真实环境中拍摄一张图片说起,摄像机的原理可以简化为:

把进入到镜头内的光线亮度编码成图像(例如一张 JEPG)中的像素。如果采集到的亮度是 0,则像素就是 0;如果亮度是 1,则像素就是 1;亮度是 0.5,则像素就是 0.5。

如果我们只用 8 位的空间来存储像素的每个通道,就意味着 0~1 区间可以对应着 256 种不同的亮度值。

但是人眼有一个特性:对光的灵敏度在不同亮度上不一样,在正常的光照条件下,人眼对较暗的区域的变化更加敏感,如下图是视频 Youtube: Color is Broken 里的一张图:

根据图所示,亮度上的线性变化在人眼看来是非均匀的,再通俗点,从 0~0.01 亮度人眼可以察觉,但是从 0.99~1.0 亮度,人眼可能觉得就是同一个颜色根本就差别不出来。人眼对暗部的变化更加敏感,对亮部的变化其实并不敏感。

因此,重点的来了:

如果在 8 位图中,我们仍然使用 0.5 亮度编码成 0.5 像素,那么暗部和明部这两个区域我们都使用了 128 种颜色来表示。实际上,亮部区域使用这么多的空间相对于暗部来说是一种存储浪费。因为人眼对暗部的变化更加敏感,对亮部的变化其实并不敏感。

为了尽量避免浪费,我们可以把中灰亮度放在像素值为 0.5 的地方,即 0.18 亮度应该编码成 0.5 像素值。这样存储空间就可以充分利用起来了。所以,摄影设备如果使用了 8 位空间存储照片的话,会用大约为 0.45 的 encoding gamma 来对输入的亮度编码,得到一张图像。0.45 这个值完全是由于人眼的特性测量得到的。

有了一张图片,显示的时候我们还是要把它还原成原来的亮度值进行显示。毕竟,0.454 只是为了充分利用存储空间而已。我们假设一下,当年 CRT 设备的输入电压和产生亮度之间完全是线性关系,我们还是要进行伽马校正的。这是为了把用 0.45 伽马编码后的图像正确重现在屏幕上。巧合的是,当年人们发现 CRT 显示器竟然符合幂律曲线,我们不需要做任何调整就可以让拍摄的图像在电脑上看起来和原来的一样了”。这是个巧合。当年,CRT 的 display gamma 是 2.5,这样导致最后的 end-to-end gamma 大约是 0.45 * 2.5 = 1.125,其实是非 1 的。 直到后来,微软联合爱普生、惠普提供了 sRGB 标准,推荐显示器中 display gamma 值为 2.2。这样,配合 0.45 的 encoding gamma 就可以保证 end-to-end gamma 为 1 了。

sRGB (standard Red Green Blue) 是由 Microsoft 影像巨擎共同开发的一种彩色语言协议,微软联合爱普生、HP 惠普等提供一种标准方法来定义色彩,让显示、打印和扫描等各种计算机外部设备与应用软件对于色彩有一个共通的语言。sRGB 是 RGB 的一种特定类型。sRGB 很流行,但它的色域很有限。它使用的 Encoding Gamma 大约是 0.45(1/2.2),这个值为了配合 Display Gamma 为 2.5 的设备工作的,这样两次 Gamma 矫正后产生偏差为 0.45 * 2.5 = 1.125,从而在视觉上进行了补偿。sRGB 模式是在近代的 GPU 上才有的东西。如果不支持 sRGB,我们就需要自己在 Shader 中进行伽马校正。

和渲染的关系 #

Unity 引擎渲染管线默认采用伽马空间,目前只有 PC、 Xbox 和 PlayStation 平台支持线性空间。对 Unity 进行设置,Edit -> Project Settings -> Player -> Other Settings -> Color Space -> Linear 切换到线性空间后,传递给着色器的将是无伽马纠正的贴图和颜色。如果在开发中的项目进行切换,场景看起来跟切换前会不一样,需要重新调整光照和贴图来达到想要的效果,灯光贴图也需要重新烘焙。

如果同时设置了线性空间和 HDR,那 Unity 所有的后期处理(post effects)全将在线性空间进行。如果只设置了线性空间,那 Unity 仍使用伽马帧缓冲(framebuffer),但当对帧缓冲进行读或写时,Unity 会自动对颜色进行纠正来保证后期处理仍然在线性空间进行。

尽管 Unity 默认对移动平台不支持线性空间。但可以在着色器里来做类似实现:对传进来的贴图做幂为 2.2 的 pow() 函数处理,然后在颜色返回值前做幂为 0.45 的 pow() 函数处理。这种实现有计算开销,不能滥用。

在游戏渲染的时候一定要考虑伽马校正的问题,否则就很难得到非常真实的效果。

在实时渲染管线中,如果采用伽马空间,那么伽马纠正过的贴图在着色器(shader)中直接参与光照计算,得到的图像被显示器伽马纠正后输出显示。这个过程很简单,但物理上是不正确的。在真实世界中,光照行为是线性的,着色器中的数字化的光照计算也是线性的,而输入的贴图和颜色是非线性的,这意味着说着色结果是不准确的。在越来越追求画面沉浸感和真实性的今天,伽马空间的光照计算满足不了需求。

因此,PBR 使用了线性空间,贴图和颜色去除了伽马纠正转换到线性空间,再传递给着色器进行光照计算着色,之后的后期处理(post effects)也同样在线性空间下进行,最后获得的图像再做伽马值 0.45 纠正,以抵消显示器显示时 2.2 的伽马纠正。

下图是渲染管线采用伽马空间和线性空间的流程和结果对比:

上图中简单球体的渲染差异可以注意到:伽马空间高光的更亮,衰减范围更大。这些都是缺乏真实性的渲染结果。

掌握伽马空间和线性空间的不同,这样在实际游戏项目中可以规避一些颜色管理上的常识性错误,从而保证画面质量。总之,线性空间的使用才是获得画面沉浸感和真实性上的重要一环。

End.