Unity的技术工程师许春,负责面向客户技术支持工作,解决第一线的难题,今天他将分享关于伽马空间和线性空间内容,帮助大家理解以及答疑解惑。
什么是伽马颜色空间
通常物体呈现出来的颜色和我们用眼睛看到屏幕上的最终颜色是不一样的。
下图01中有三张图片。左边偏亮的才是真实的颜色。这里的真实颜色指的是物体的颜色波长是多长,在光谱中对应的信息是什么样子的,眼睛对这个颜色的反馈是什么样子的。实际上我们在屏幕上看到的都是右边的颜色,显示器在显示的时候做了伽马变换,我们把这个指数叫做伽马。
中间的图,当伽马值是1的时候,和原来的颜色是一样的,伽马从1到2.2的时候,这个颜色是逐渐加深的,但是纯黑(0,0,0)和纯白(1,1,1)是不会变的,只是让中间颜色的亮度有变化。因为我们从来没有见过真实的颜色是什么样的,都是通过屏幕上看到的,所以我们理所应当地认为右边是真实的颜色。
再看一个更形象的情况,图02中有二张图片。左边是我们眼睛看到的或者是照相机看到的,右边是我们实际在显示器上看到它是什么样子的。右边的图片其实是原始图,左边图片经过了伽马校正,我们在显示器里看到的东西比它本身的亮度暗一点,显示器内部有伽玛变换。
那么我们看现在这个图变暗了,如果想让它正常一点,为了解决这个问题,我们会怎么办?我们可以反向做一个伽马校正,把暗的部分校正到变亮的部分,它们的值是倒数的关系,完全相同的还原回这个亮度本身的值。
这时又出现了另一个问题。例如:当我们输出到屏幕的时候已经变暗了,这时再做校正,是找不到任何一种方法能在这中间插一个步骤来校正它。我们最后想了一个办法,如果在输出的时候没法把它校正,能不能把之前的颜色提前校正回来?
如图03所示,这是一个对比图,首先它是完全没有校正过的,最左侧是它本身的颜色,中间这个是可以拿照相机或者拿摄像机,或者你自己眼睛看到真实的亮度,照相机把真实亮度存下来,不做任何的处理,这是中间图的样子,但是经过屏幕显示的时候变暗了。
我们应该怎么做才能让它不变暗呢? 把最左边原始的亮度存储的时候,我们提前做一些反向的伽马校正,把伽马校正这个阶段提前到图片存储的时候,这个时候显示器又要让这个画面变暗,一亮一暗抵消了之后就是原来的亮度了,看到所有的图片也就正常了,不会普遍的偏暗,我们看着比较舒服的样子。
这样就解决了伽玛变换产生的问题。示例中伽马值是2.2,每个显示器的伽马值都是可能不一样的,图片到底应该用哪个伽马值做校正?数据预先校正完之后保存在图片中了,无法对所有的显示器分别做校正。所以人们发明了Color Profile,可以称为颜色空间或者色彩空间。
颜色空间的概念是很大的,在这里我们狭义地理解它为:它解释了数值到波长的对应关系。当以RGB保存图片的时候,图06中的的颜色空间说明了RGB数值如何变换的颜色波长;反过来,显示器也有Color Profile,它说明了一个特定波长的颜色要怎么样用它屏幕的RGB亮度表现出来。当显示一张图片时,根据图片的Color Profile把它的颜色变换到波长,显示器显示该波长时,也知道应该如何设置它的RGB亮度来表示这个波长的颜色,这样一张图片可以在不同的显示器上看起来都相同。
Color Profile一般也会定义伽马值,来定义一个伽马变换,这个伽马值是多少,也是包含在这个Color Profile里边的,通过伽马值就知道它的变换曲线是什么样子的。这是从显示器那一端可以拿到当前的Color Profile,甚至可以指定用哪一种Color Profile,你可以看到选不同配置的时候,桌面或者图片显示出来的颜色是不一样的。
通过这种配置,图片中的数据对应到颜色亮度的变换是可以不一样的。系统设置里一般会把伽马值翻译成响应曲线,你可以看到RGB分别有响应曲线,下面是对应的曲线,这个值不同的时候,这个曲线有变化,和刚才看到的现象是一样的。
sRGB是一种最常用的Color Profile,是由微软和惠普联合制定的一个规范,它的使用如此广泛以至于我们经常把sRGB等同于了伽马颜色空间。如果显示一个颜色,所有的显示设备都用了sRGB的颜色空间,存储一个图片或者保存一个视频也好或者其它图片工具也使用sRGB的编码,因为都在一个颜色空间下,所以输入和显示是可以免去转换的。
在不同设备上显示颜色要把图片颜色转成波长,再把波长放在显示器上,根据显示器的Color Profile可以得出显示器应该怎么显示这个值,才能体现出它的原本颜色,这中间有一个计算过程,如果我们都使用同一种Colo Profile,例如:sRGB,就没有这个过程了,图片存的时候就是sRGB的颜色空间,显示器接收的时候已经是同样的颜色空间了,RGB数值可以直接输出,省略了转换的过程。
sRGB也有伽玛变换,它的伽马值近似等于2.02。图07中右边图颜色部分解释了它的伽马值,伽马值其实不是一个固定值,而是一条曲线。
下面我们看一下Unity里边用伽马颜色空间时到底做什么?如果不做任何的伽马校正,一般的流程是什么样子的?
如图08所示,最左边是贴图,大家看着可能觉得有点暗,这是正常的,因为显示端做了伽玛变换。这张图在Unity里参与光照计算,计算结果保存到Frame Buffer,这也是正常的结果。但是显示器输出Frame Buffer的时候,结果看起来变暗了,我们看到后觉得这不是我们想要的结果。
如图09所示,这就是我们提到的保存图片时的伽玛校正,可以看到,原始的这张图经过伽马校正提亮,传到Unity里面做光照计算。通过Frame Buffer可以看到它的计算结果是特别亮的,经过显示器的伽马变换之后,这个亮度被压了下来,你就会觉得这个效果是正常的。但是这个亮度是让人觉得正常的,代表它的结果是正确的吗?其实不是这样的,后面我们再来详谈。
什么是线性颜色空间
什么是线性颜色空间呢?这里要提到二个底层图形API:sRGB Frame Buffer和sRGB Sampler。
我们拿刚才的渲染流程对比一下,上面分支的流程里输出是偏暗的,正常的亮度被显示器压低了,我们没有办法在显示器输出前进行校正。但是sRGB Frame Buffer可以,如果使用sRGB Frame Buffer,它能够在结果输出到显示器这个阶段做sRGB伽玛校正,如图10所示,最后的显示是正常的。
sRGB Frame Buffer是由硬件支持的,就像刚才所做的变换用Shader也可以实现,sRGB Frame Buffer的转换速度要比它快。安卓手机只有OpenGL ES3.0才可以支持它,所以你会看到开启线性空间必须指定Graphics API为OpenGL ES3.0。需要要注意的是Alpha值不做任何变换的,它只支持每通道8位的格式。为什么只支持8位格式?我们后面会说到。
你目前能用到的大部分作图软件都是在sRGB的空间下制作的,软件内的预览和图片的保存都经过了伽玛校正。这个在伽玛颜色空间下没问题,但是在线性颜色空间里,贴图的颜色应该是线性的。所以当使用线性空间时,经过sRGB校正的贴图应该被还原。有二种方法,可以把图退回给美术重做,或者你可以使用sRGB Sampler。
sRGB Sampler也是硬件支持的一个特性,如果勾选sRGB选项,硬件会认为图是sRGB编码的,在采样颜色的时候会先做一次sRGB反向转换,再把结果返回,所以Shader读到的颜色是校正之前的颜色。
如果贴图是线性存储的,则不需要勾选sRGB选项,采样得到的颜色值就是直接从贴图中取得。
为什么要用伽马颜色空间?
那么为什么我们之前要用伽马颜色空间?
有一个历史原因,以前的显示器是模拟信号,电子打到显示屏上,就能看到像素发光。它的电压和亮度并不是线性的增长关系,而是近似于调伽马曲线,所以以前的显示器自带伽马变换的。
另一个重要的原因和精度有关,就像我们刚才说,RGB 8位的贴图才有sRGB格式,sRGB格式的精度会更高。
那么同样是8位的表示方式,为什么精度会有区别呢?这和人眼有关系,并不是说绝对精度会变高,而是说人眼对不同亮度的区域的反应是不一样的。
例如:图15中的渐变图是经过伽马变换的,下边那一个是没有经过任何校正的,也就是说下面这个图从左到右亮度是均匀变化的。你会发现下面这张图右边基本上是纯白,但是它其实是有均匀变化的,这说明我们人眼对亮部的识别特别差,对暗部的识别高一些。
假如说对右边这部分区域也用8位做编码的话,亮度值的渐变对人眼来说是看不到任何变化的,人眼识别不了那么清晰的渐变过程,位数少一些没有关系。对暗度反而有比较高的敏感度,需要更多的位数。这就是为什么说sRGB精度高,是因为我们可以从图像中看到更多信息。
精度如何变高的呢?如图16所示,这是经过伽马校正以后的曲线,纵轴是sRGB格式的数据,横轴是原始数据。原始值和编码后的值基本是一样的,原始数据较小的区间,编码后的阈值变得比较大。
这是我们期望的,同样跨度的编码值可以表示更细微的过渡。这就是为什么要用伽马颜色空间非常重要的原因,我们制作贴图时对精度就是有要求的。
那么线性空间的好处在哪里?是因为光照计算会出错,在伽马颜色空间下做光照计算分两步。举个简单乘法的例子,第一步:Shader计算结果,计算方式为贴图颜色×光照系数,贴图颜色是经过伽玛校正的,所以它要带着幂运算。第二步:输出计算结果到屏幕,屏幕颜色等于是输出的颜色。
我们想要的正确的结果是什么样子?我们并不是要在贴图颜色上做校正,贴图颜色就是直接乘以光照系数得到输出的颜色,只有在输出到屏幕的时候,把输出颜色做伽马校正。
这二种方式比较相似,做一个对比,你就能看到,左边是做贴图上的伽马校正,并且在伽马颜色空间下的计算结果,右边是正确的计算结果,二者是不一样的。线性颜色空间的计算是和正确的计算方法一致的。
图20中的这几张图片更能说出问题。贴图一般都不是灰度图,RGB通道间的差异是不一样的,比如任的皮肤贴图,红通道的值会高于其它二个通道。经过伽马校正之后,红通道和其他通道的差异就会被放大,用它做光照计算,会发现红通道会提升得异常的高。
二张图中左边是线性正确值,右边的曝光结果不对,因为这种色温比较温和的颜色,会迅速的曝光,存的这个值在经过sRGB变换了之后已经比原来的值大了。注意右边上眼皮轮廓应用的地方,你会发现有一些黑、有一些蓝色,也是一样的问题,当你做光照计算的时候,冷色调和暖色调的差别已经拉开了,把光照系数加上去,差异变得更大,这就是你发现结果不对的原因。
选择颜色空间
你刚才看到只有在光照计算的时候,伽马空间的弊端会暴露的特别明显,什么时候分别采用合适的颜色空间呢?
在光照效果来说,只有要求真实光照的话,才要考虑线性空间,真实光照并不是说看起来好,而是说数值上就是正确的。要求数值正确是指要求在不同的光照环境下光照结果都正常。
如果要求的是物理写实的效果,才只能考虑使用线性空间。其实在其他的情况下,二种方式都是可以的,用伽马空间也是可以的。
2D游戏推荐使用伽马空间,这不是Unity的考虑,而是美术工作流的考虑,需要花很长时间培训美术,转到线性空间做图方式做图,这是成本比较高的,所以推荐伽马空间。3D游戏推荐使用线性空间,尤其使用PBR时线性空间是前提,伽马空间是做不了的。
常见问题
下面是我们收集反馈整理的一些常见问题。
▲ Substance Painter的效果和Unity中不一样
首先这是肯定的,Substance Painter中的光源信息是从环境贴图中得到的,Unity没办法这么做,因为环境贴图中的光源在Unity中不够强。一些精度的处理SP也做得更好,因为它对性能的要求不苛刻。我们只能做到尽量让二者效果接近,而且现在的效果也确实很接近了。
在Unity中一定要用线性空间,Substance Painter中导出的贴图都是经过sRGB编码的,所以每张图都要勾选sRGB选项。Unity中的环境贴图也非常重要,可以把SP的环境贴图导入到Unity中使用,Substance从环境贴图里面拿到的主光源是特别强的,在Unity里面没有那么多,所以Unity中的主光源是用来模拟环境贴图上面的那个光源方向,你需要调整到光源方向一致。然后调整环境光的强度到合适的值。
图25是Substance Painter和Unity中制作流程的说明,我们可以看一下,当你在Substance设置一个颜色,比如说(0.5、0.5、0.5),如果仔细观察调色板,我们会发现它并不在调色板正中间,而是偏上的位置,是因为你看到这个预览颜色的时候,它已经经过sRGB编码了。当Substance的贴图导出时,线性的颜色值经过伽马变换,颜色被提亮了,所以你需要在Unity中勾选sRGB选项,让它在采样时能还原回线性值。
▲ PhotoShop中导出的图片应该如何设置
PhotoShop导出的图片应该怎么设置?PhotoShop对颜色管理特别精确。在Photoshop看到的颜色,比Unity里面看到的颜色更亮,因为Unity里看到的颜色要经过显示器的伽玛变换,而PhotoShop不会。PhotoShop会读取显示器的Color Profile,反向补偿回去,也就是说,在Photoshop里边如果输到128,看到的就是128。
然而实际上经常不是这样的,因为PhotoShop有第二个Color Profile,叫做Document Color Profile。通常它的默认值就是sRGB Color Profile,和显示器的Coor Profile一致,颜色是被这个Color Profile压暗了,所以PhotoShop中看到的结果才和Unity中一样。
如果使用线性空间,一般来说Photoshop可以什么都不改,导出的贴图只要勾上sRGB就可以了。你也可以调整PhotoShop的伽玛值为1,导出的贴图在Unity中也不需要勾选sRGB了。二种流程都可以,因为我们要求它的预览效果和实际效果是一样的,所以只要保证Photoshop Unity两边的变换结果是一致的就可以。
▲ 线性空间中的半透明图片怎么制作
线性空间中的半透明图片应该制作呢?我们需要知道Photoshop的图层和图层之间做混合的时候,每个上层图层都经过了伽马变换,然后才做了混合,这个方式是设置中的默认值。
你需要在设置中更改它,选择“用灰度系数混合RGB颜色”,参数设置为1,这样图层直接就是直接混合的结果。但是如果你修改了这个选项,你可能只有选择上面的Linear流程制作贴图了。
记得我们之前提到的,sRGB编码是为了增加8位颜色的精度,如果你是用了32位浮点数的贴图格式,PhotoShop自动使用的是线性空间,没有做任何伽玛变换的。
▲ 伽玛空间下Lightmap和实时光照效果不一样
这个是一个已知问题,暂时还没有明确的修复时间,对于这个问题可能大家只能不断地烘焙然后调整结果了。
▲ 线性空间下截图和游戏画面有差异
这是因为ReadPixels()这样的函数是通过CPU端读取Frame Buffer的,这种方式返回的颜色是经过sRGB编码的数据被直接返回,而没有还原到线性值。所以这一点要注意,当你得到返回颜色之后,保存贴图要以线性方式保存,但是目前默认的保存方式是又经过一次伽玛校正的。
▲ HDR和线性空间同时开启的结果
因为像我们之前提到的,32位浮点格式的Frame Buffer是没有sRGB扩展的。这一点Unity已经为你做好了处理,浮点格式的Frame Buffer在输出前会先绘制在8位的sRGB Frame Buffer上,然后输出到屏幕,伽玛校正在这个阶段自动完成。
本文转自: Unity官方平台,作者:许春,转载此文目的在于传递更多信息,版权归原作者所有。