为什么我会想起来写这篇博客呢?缘起于项目中做PBR时,美术来问出贴图是要出线性空间贴图还是伽马空间贴图,经过一番纠结与查证后,项目终于切到了线性空间,而贴图则是能出线性空间优先出线性空间,出不了的出伽马空间也行。至于为什么也行,就是本篇博客所要讲述的内容。
首先要来解决第一个问题,什么是伽马空间,什么是线性空间?线性空间好理解,颜色按照线性渐变的空间即是线性空间,我在网上看到一个举例很有趣,想象一个纯黑墨水的池子,往里面滴一滴白色颜料,随着白色颜料不断的滴入,墨池会越来越白直至变成白色,而记录每次滴入颜料后墨池的颜色变化,即是一个从黑到白的颜色线性渐变过程。既然有线性渐变,那么肯定就有非线性渐变,而伽马空间正是这样一个颜色非线性渐变的空间。为什么会发生这样的事呢?
一个流传甚广的版本表示,这是由CRT显示器引起的。由于CRT显示器对于输入的电压和显示的亮度并不呈线性关系,而是一个类似幂律曲线的关系,一般来说,这个曲线的指数部分称作伽马值,为2.5。显示器有这样的特性后,一个正常颜色输入进去显示出来会变暗,我自己拉了个图来说明下
这是个的幂律曲线,如果颜色输入值为,那么输出值大概为左右,更接近黑色。所以输入的颜色在输出时会变暗。为了解决这个问题,图片在被采集时会做一个逆向操作,如果颜色为的话先逆向然后 就会回到了。这里我们用到了两个伽马值,和,他们分别称为encoding gamma和display gamma,通过下图展示他们的用处。
encoding gamma通常在图片生成时(比如说拍照拍出来的照片,PS新建的图片等)就已经存在,而display gamma又是显示器自带的特性(当然现在的显示器不再是CRT,所以display gamma可能不是2.5,不过为了兼容性厂商还是会把以前的2.5伽马值加入),他俩相乘称作end-to-end gamma,如果是1的话那么真实场景被捕捉的亮度和显示的亮度是成比例的。然而,Real Time Rendering一书指出了乘积为1的问题。一是我们人眼看到的真实场景的亮度与显示器所能显示的亮度差了好几个数量级,说白了显示器所能显示的颜色精度根本达不到真实场景的精度;二是周围环境影响,我们的视野在看真实场景时是由真实场景所填充的,而在看显示器时视野除了被虚拟场景包围,还会被真实场景包围。这样两个差别导致了end-to-end gamma是1的话并不能保证显示的亮度和原始场景的亮度是一致的。书中推荐,电影院那种漆黑的环境为1.5,在明亮的室内为1.125。
我们通常用的sRGB标准的encoding gamma大概为0.45(1/2.2),这是为了配合2.5的display gamma,因为0.45 * 2.5 = 1.125。当然,显示器的display gamma大部分值还是设为2.2(这里有个网站,可以看在不同的伽马值下图片所表现的不同准确的伽玛 2.2 及预设 5 种伽玛值设定),这样1/2.2*2.2 = 1。
当然冯乐乐前辈还提出了来自其他领域对于伽马的解释,以此来论证伽马值存在的必然,不管如何,伽马空间中的颜色并非线性渐变,而是呈一条曲线变化。
那么我们在做PBR时,为什么要纠结到底用伽马空间还是线性空间呢?PBR全称为Physically Based Rendering,既然是基于物理的渲染,那么我们做渲染时对于贴图采样出来的值必定要是和真实环境下相同的值才行,而采用了伽马空间的话贴图中颜色会被encoding gamma所改变,shader中采样出来的颜色值和真实环境下的值是不一样的,这样怎么能称为基于物理的渲染呢?
我们以人的皮肤渲染来举例子,皮肤贴图的r通道的值通常会高于其余两个通道的值,那么在伽马校正后(即对原始值做一个伽马次方的操作),这种差异会被进一步的放大,再做光照计算,你会发现r通道的值提升的异常的高。
上图左边是线性正确值,右边是渲染时带着伽马值,那么提升光的亮度会迅速的曝光。
而当这种差异被拉大后,你会发现在眼皮轮廓的地方会出现蓝黑色的痕迹,如下图
所以在做PBR渲染时,使用线性空间是非常有必要的。
而我们纠结的点在于,把Unity引擎切到线性空间的话,之前所有美术的资源都是在伽马空间下制作的,会不会有问题?实际上是有问题的。
当我们把Unity从伽马空间切换到线性空间时,引擎里面我们需要勾选一个东西,这样伽马空间的资源也能使用了。
勾选了图中的sRGB后,其实引擎为我们做了一个工作,在采样这张图片的时候会调用OpenGL ES3.0里的sRGB Sampler接口,将贴图中被encoding gamma所改变的值还原,这样我们在shader中做的任何计算就是基于物体在真实场景中的颜色了。算完以后当我们要把颜色输出到显示器时,显示器因为自带display gamma,我们无法抹去这个东西,所以引擎又为我们做了一件事,调用OpenGL ES3.0里的sRGB Frame Buffer接口,将计算得出的最终结果用encoding gamma算好,用以抵消display gamma的影响。
那么在线性空间底下使用伽马空间资源会有什么问题呢?透明混合会出问题。我们知道透明混合的时候dst color在frame buffer中,而颜色在线性空间下进入了frame buffer引擎会调用sRGB Frame Buffer接口做一个pow0.45的操作,而透明混合时明显需要线性空间的颜色,因为src color还没进frame buffer,没做过pow4.5的操作。所以这里在把dst color从frame buffer拿出来时,会做一个pow2.2的操作回到线性空间,然后做透明混合,得出结果后再做一遍pow0.45。
假设src color的某个分量为1,alpha为0.5;dst color某个分量为0,那么根据正常的计算为:
而有了sRGB Sampler和sRGB Frame Buffer后:
差异在此产生,并且随着混合次数的增多,差异会越来越大。
理论上最好的解决方案是美术直接在线性空间下制作资源,如官方说的在PhotoShop设置中选择“用灰度系数混合RGB颜色”,参数设置为1。
或者参考网上的解决方案【Unity补完计划】Unity线性空间(Linear)下Alpha的混合问题、Unity手机线性空间下的透明混合(上),但他们都有额外的消耗,对于性能不是那么富裕的项目就显得无能为力了。
参考
Unite 2018 | 浅谈伽玛和线性颜色空间
聊聊Unity的Gamma校正以及线性工作流
【图形学】我理解的伽马校正(Gamma Correction)
Chapter 24. The Importance of Being Linear