Gamma、Linear、sRGB 和Unity Color Space,你真懂了吗?

“为什么我渲染出来的场景,总是感觉和真实世界不像呢?”

游戏从业者或多或少都听过Linear、Gamma、sRGB和伽马校正这些术语,互联网上也有很多科普的资料,但是它们似乎又都没有讲很"清楚"。

游戏界(特别是中小团队)很容易忽略这些概念造成的影响。长远来看,作为游戏从业者的你应该理解这些术语的含义,理解它们的本质联系,理解选择Linear或Gamma 空间带来的工作流变化。

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

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

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

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

l = u^{2.2}\qquad(l\in[0,1],u\in[0,1])\\

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

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

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

横坐标表示电压,纵坐标表示亮度

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

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

c_o = {c_i}^\frac{1}{2.2}\\

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

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


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

为什么sRGB在Gamma0.45空间?

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

还有另外一种解释,和人眼对暗的感知更加敏感的事实有关。

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

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

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

统一到线性空间

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

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

左(Gamma Space),右(Linear Space)

为什么Linear Space更真实?

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

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

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

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

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

统一到线性空间的过程是看起来是这样的,用图中橙色的框表示(现在看不懂图没关系,跟着后面的步骤来一步步看):

我们从橙色框的左上角出发。

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

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

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

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

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


我们再举个例子,我们取sRGB纹理里面的一个像素,假设其值为0.73。那么在统一线性空间的过程中,它的值是怎么变化的?

第一步,0.73(上曲线) * [Remove Gamma Correction] = 0.5(直线)。( 0.73^{2.2} = 0.5

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

第三步,0.5(直线) * [Gamma Correction] = 0.73(上曲线)。( 0.5^\frac{1}{2.2} = 0.73

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

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

0.5(直线) * [显示器] = 0.218(下曲线)。( 0.5^{2.2}=0.218

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

Unity中的Color Space

我们回到Unity,在ProjectSetting中,你可以选择Gamma 或 Linear作为Color Space:

这两者有什么区别呢?

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

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

怎么告诉Unity纹理是sRGB还是Linear呢?对于特定用途的纹理,你可以直接设置他们所属的类型:如Normal Map、Light Map等都是Linear,设置好类型Unity自己会处理他们。

还有一些纹理不是上面的任何类型,但又已经在线性空间了(比如说Mask纹理、噪声图),那你需要取消sRGB这个选项让它跳过Remove Gamma Correction过程:

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

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

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


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

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

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

编辑于 05-08

文章被以下专栏收录