Linerspace gamma space

/

在unity的这个路径下Edit->Project Settings->Player->Other Settings,可以选择linear空间或者gamma空间。这两种空间会发生什么事,见下。

当ColorSpace选择LinearSpace时
第一张图:shader中tex2D读取颜色参与计算。
LinearSpace时,除非对指定图片选择了bypass sRGB,否则所有纹理都会变成sRGB格式。
对于sRGB的纹理,GPU会自动将colorG0(偏亮)转换到linear space,即colorL0(偏暗)。也就是说,在此转换之前,存储在纹理中的颜色colorG0是在gamma space的(偏亮)。

shader中tex2D前后发生的事情
第二张图:shader计算结果到写入color buffer。
所有计算应该发生在linear space,计算结束后需GPU会将该像素颜色再次转换到gamma space,再写入color buffer。


第三张图:显示器把color buffer显示到眼睛。
color buffer中的颜色和人眼看到的不同,这是显示器做的事,这个步骤叫display transfer,目前就掌握到这个程度。

显示器把color buffer显示到眼睛
当ColorSpace选择gammaSpace时
选择gammaSpace时,整个流程中GPU不再做颜色空间转换。纹理中的颜色直接参与计算,而显示器还会正常display transfer。至于存储在纹理中的颜色colorG0应当是在什么space中,不得其解。
第一张图+第二张图:

gammaSpace时
第三张图:显示器把color buffer显示到眼睛。 (和上面没区别)

显示器把color buffer显示到眼睛

//

人们使用伽马曲线来进行显示最开始是源于一个巧合:在早期,CRT几乎是唯一的显示设备。但CRR有个特性,它的输入电压和显示出来的亮度关系不是线性的,而是一个类似幂律(pow-law)曲线的关系,而这个关系又恰好跟人眼对光的敏感度是相反的。这个巧合意味着,虽然CRT显示关系是非线性的,但对人类来说感知上很可能是一致的。

我来详细地解释一下这个事件:在很久很久以前(其实没多久),全世界都在使用一种叫CRT的显示设备。这类设备的显示机制是,使用一个电压轰击它屏幕上的一种图层,这个图层就可以发亮,我们就可以看到图像了。但是,人们发现,咦,如果把电压调高两倍,屏幕亮度并没有提高两倍啊!典型的CRT显示器的伽马曲线大致是一个伽马值为2.5的幂律曲线。显示器的这类伽马也称为display gamma。由于这个问题的存在,那么图像捕捉设备就需要进行一个伽马校正,它们使用的伽马叫做encoding gamma。所以,一个完整的图像系统需要2个伽马值:

- encoding gamma:它描述了encoding transfer function,即图像设备捕捉到的场景亮度值(scene radiance values)和编码的像素值(encoded pixel values)之间的关系。
- display gamma:它描述了display transfer function,即编码的像素值和显示的亮度(displayed radiance)之间的关系。

如下图所示:

这里写图片描述

而encoding gamma和display gamma的乘积就是真个图像系统的end-to-end gamma。如果这个乘积是1,那么显示出来的亮度就是和捕捉到的真实场景的亮度是成比例的。

上面的情景是对于捕捉的相片。那么对于我们渲染的图像来说,我们需要的是一个encoding gamma。如果我们没有用一个encoding gamma对shader的输出进行校正,而是直接显示在屏幕上,那么由于display gamma的存在就会使画面失真。

龚大认为全部的问题都出在CRT问题上,跟人眼没有任何关系。

但是,在《Real-time Rendering》一书中,指出了这种乘积为1的end-to-end gamma的问题。看起来,乘积为1的话,可以让显示器精确重现原始场景的视觉条件。但是,由于原始场景的观察条件和显示的版本之间存在两个差异:1)首先是,我们能够显示的亮度值其实和真实场景的亮度值差了好几个数量级,说通俗点,就是显示器的精度根本达不到真实场景的颜色精度(大自然的颜色种类几乎是无穷多的,而如果使用8-bit的编码,我们只能显示256^3种颜色);2)这是一种称为surround effect的现象。在真实的场景中,原始的场景填充了填充了观察者的所有视野,而显示的亮度往往只局限在一个被周围环境包围的屏幕上。这两个差别使得感知对比度相较于原始场景明显下降了。也就是我们一开始说的,对光的灵敏度对不同亮度是不一样的。如下图所示(来源: Youtube: Color is Broken):

这里写图片描述

为了中和这种现象,所以我们需要乘积不是1的end-to-end gamma,来保证显示的亮度结果在感知上和原始场景是一致的。根据《Real-time Rendering》一书中,推荐的值在电影院这种漆黑的环境中为1.5,在明亮的室内这个值为1.125。

个人电脑使用的一个标准叫sRGB,它使用的encoding gamma大约是0.45(也就是1/2.2)。这个值就是为了配合display gamma为2.5的设备工作的。这样,end-to-end gamma就是0.45 * 2.5 = 1.125了。

这意味着,虽然CRT的display gamma是2.5,但我们使用的encoding gamma应该是1.125/2.5 = 1/2.2,而不是1/2.5。这样才能保证end-to-end gamma为1.125,从而在视觉上进行了补偿。

虽然现在CRT设备很少见了,但为了保证这种感知一致性(这是它一直沿用至今的很重要的一点),同时也为了对已有图像的兼容性(之前很多图像使用了encoding gamma对图像进行了编码),所以仍在使用这种伽马编码。而且,现在的LCD虽然有不同的响应曲线(即display gamma不是2.5),但是在硬件上做了调整来提供兼容性。

重要:上面的说法主要来源于Real-time Rendering》一书。

事情的起因可以从在真实环境中拍摄一张图片说起。摄像机的原理可以简化为,把进入到镜头内的光线亮度编码成图像(例如一张JEPG)中的像素。这样很简单啦,如果采集到的亮度是0,像素就是0,亮度是1,像素就是1,亮度是0.5,像素就是0.5。这里,就是这里,出现了一点问题!如果我们假设只用8位空间来存储像素的话,以为着0-1可以表示256种颜色,没错吧?但是,人眼有的特性,就是对光的灵敏度在不同亮度是不一样的。还是这张图Youtube: Color is Broken:

这里写图片描述

这张图说明一件事情,即亮度上的线性变化在人眼看来是非均匀的,再通俗点,从0亮度变到0.01亮度,人眼是可以察觉到的,但从0.99变到1.0,人眼可能就根本差别不出来,觉得它们是一个颜色。也就是说,人眼对暗部的变化更加敏感,而对亮部变化其实不是很敏感。也就是说,人眼认为的中灰其实不在亮度为0.5的地方,而是在大约亮度为0.18的地方(18度灰)。强烈建议去看一下Youtube上的视频, Color is Broken。

那么,这和拍照有什么关系呢?如果在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了。当然,上一节提到的两个观察差异,有些时候我们其实更希望end-to-end gamma非1的结果,例如,在电影院这种暗沉沉的环境中,end-to-end gamma为1.5我们人看起来更爽、更舒服,而在明亮的办公室这种环境中1.125的end-to-end gamma值更舒服、更漂亮。所以,我们可以根据环境的不同,去选择使用什么样的display gamma。

总之, 伽马校正一直沿用至今说到底是人眼特性决定的。你会说,伽马这么麻烦,什么时候可以舍弃它呢?如果有一天我们对图像的存储空间能够大大提升,通用的格式不再是8位的时候,例如是32位的时候,伽马就没有用了。因为,我们不需要为了提高精度而把18度灰编码成0.5像素,因为我们有足够多的颜色空间可以利用,不需要考虑人眼的特性。

这和渲染有什么关系

其实,对伽马传说的理解就算有偏差,也不会影响我们对伽马校正的使用。我们只要知道,根据sRGB标准,大部分显示器使用了2.2的display gamma来显示图像。

前面提到了,和渲染相关的是encoding gamma。我们知道了,显示器在显示的时候,会用display gamma把显示的像素进行display transfer之后再转换成显示的亮度值。所以,我们要在这之前,像图像捕捉设备那样,对图像先进行一个encoding transfer,与此相关的就是encoding gamma了。

而不幸的是,在游戏界长期以来都忽视了伽马校正的问题,也造成了为什么我们渲染出来的游戏总是暗沉沉的,总是和真实世界不像。

回到渲染的时候。我们来看看没有正确进行伽马校正到底会有什么问题。

以下实验均在Unity中进行。

光照

我们来看一个最简单的场景:在场景中放置一个球,使用默认的Diffuse材质,打一个平行光:

Gamma

 看起来很对是吗?但实际上,这和我们在真实场景中看到的是不一样的。在真实的场景中,如果我们把一个球放在平行光下,它是长这个样子的:

Linear

假设球上有一点B,它的法线和光线方向成60°,还有一点A,它的法线和光线方向成90°。那么,在shader中计算diffuse的时候,我们会得出B的输出是(0.5, 0.5, 0.5),A的输出的(1.0, 1.0, 1.0)。

在第一张图中,我们没有进行伽马校正。因此,在把像素值转换到屏幕亮度时并不是线性关系,也就是说B点的亮度其实并不是A亮度的一半,在Mac显示器上,这个亮度只有A亮度的1/1.8呗,约为四分之一。在第二章图中,我们进行了伽马校正,此时的亮度才是真正跟像素值成正比的。

混合


混合其实是非常容易受伽马的影响。我们还是在Unity里创建一个场景,使用下面的shader渲染三个Quad:

Shader "Custom/Gamma Correction For Quad" {
    Properties {
        _MainTex ("Base (RGB)", 2D) = "white" {}
        _Color ("Color", Color) = (1, 1, 1, 1)
    }
    SubShader {     
        Tags
        {
            "Queue" = "Transparent"
            "IgnoreProjector" = "True"
            "RenderType" = "Transparent"
        }

        Pass {
//          Blend One One
            Blend SrcAlpha OneMinusSrcAlpha

            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #include "UnityCG.cginc"

            sampler2D _MainTex;
            float4 _Color;

            struct v2f {
                float4 pos : SV_POSITION;
                float4 uv : TEXCOORD0;
                float4 normal : TEXCOORD1;
            };

            v2f vert(appdata_base i) {
                v2f o;
                o.pos = mul(UNITY_MATRIX_MVP, i.vertex);
                o.uv = i.texcoord;

                return o;
            }

            float4 circle(float2 pos, float2 center, float radius, float3 color, float antialias) {
                float d = length(pos - center) - radius;
                float t = smoothstep(0, antialias, d);
                return float4(color, 1.0 - t);
            }

            float4 frag(v2f i) : SV_Target {
                float4 background = float4(0.0);
                float4 layer1 = circle(i.uv, float2(0.5, 0.5), 0.3, _Color.rgb, 0.2);

                float4 fragColor = float4(0.0);
                fragColor = lerp(fragColor, layer1, layer1.a);

//              fragColor = pow(fragColor, 1.0/1.8);
                return fragColor;
            }

            ENDCG
        }
    } 
    FallBack "Diffuse"
}

上面的shader其实很简单,就是在Quad上画了个边缘模糊的圆,然后使用了混合模式来会屏幕进行混合。我们在场景中画三个这样不同颜色的圆,三种颜色分别是(0.78, 0, 1),(1, 0.78, 0),(0, 1, 0.78):

这里写图片描述

看出问题了吗?在不同颜色的交接处出现了不正常的渐变。例如,从绿色(0, 1, 0.78)到红色(0.78, 0, 1)的渐变中,竟然出现了蓝色。

正确的显示结果应该是:

这里写图片描述

第一张图的问题出在,在混合后进行输出时,显示器进行了display transfer,导致接缝处颜色变暗。

非线性输入

shader中非线性的输入最有可能的来源就是纹理了。

为了直接显示时可以正确显示,大多数图像文件都进行了提前的校正,即已经使用了一个encoding gamma对像素值编码。但这意味着它们是非线性的,如果在shader中直接使用会造成在非线性空间的计算,使得结果和真实世界的结果不一致。

Mipmaps

在计算纹理的Mipmap时也需要注意。如果纹理存储在非线性空间中,那么在计算mipmap时就会在非线性空间里计算。由于mipmap的计算是种线性计算——即降采样的过程,需要对某个方形区域内的像素去平均值,这样就会得到错误的结果。正确的做法是,把非线性的纹理转换到线性空间后再计算Mipmap。

扩展

由于未进行伽马校正而造成的混合问题其实非常常见,不仅仅是在渲染中才遇到的。

Youtube上有一个很有意思的视频,非常建议大家看一下。里面讲的就是,由于在混合前未对非线性纹理进行转换,造成了混合纯色时,在纯色边界处出现了黑边。用数学公式来阐述这一现象就是:

所以,在处理非线性纹理时一定要格外小心。

进行伽马校正

我们的目标是:保证所有的输入都转换到线性空间,并在线性空间下做各种光照计算,最后的输出在通过一个encoding gamma进行伽马校正后进行显示。

在Unity中,有一个专门的设置是为伽马校正服务的,具体可以参见官方文档(Linear Lighting)。

简单来说就是靠Edit -> Project Settings -> Player -> Other Settings中的设置:

它有两个选项:一个是Gamma Space,一个Linear Space。

- 当选择Gamma Space时,实际上就是“放任模式”,不会对shader的输入进行任何处理,即使输入可能是非线性的;也不会对输出像素进行任何处理,这意味着输出的像素会经过显示器的display gamma转换后得到非预期的亮度,通常表现为整个场景会比较昏暗。
当选择Linear Space时,Unity会背地里把输入纹理设置为sRGB模式,这种模式下硬件在对纹理进行采样时会自动将其转换到线性空间中;并且,也会设置一个sRGB格式的buffer,此时GPU会在shader写入color buffer前自动进行伽马校正。如果此时开启了混合(像我们之前的那样),在每次混合是,之前buffer中存储的颜色值会先重新转换回线性空间中,然后再进行混合,完成后再进行伽马校正,最后把校正后的混合结果写入color buffer中。这里需要注意,Alpha通道是不会参与伽马校正的。
sRGB模式是在近代的GPU上才有的东西。如果不支持sRGB,我们就需要自己在shader中进行伽马校正。对非线性输入纹理的校正通常代码如下:

float3 diffuseCol = pow(tex2D( diffTex, texCoord ), 2.2 );

在最后输出前,对输出像素值的校正代码通常长下面这样:

fragColor.rgb = pow(fragColor.rgb, 1.0/2.2); return fragColor;

但是,手工对输出像素进行伽马校正在使用混合的时候会出现问题。这是因为,校正后导致写入color buffer的颜色是非线性的,这样混合就发生在非线性空间中。一种解决方法时,在中间计算时不要对输出进行伽马校正,在最后进行一个屏幕后处理操作对最后的输出进行伽马校正,但很显然这会造成性能问题。

还有一些细节问题,例如在进行屏幕后处理的时候,要小心我们目前正在处理的图像到底是不是已经伽马校正后的。

总之,一切工作都是为了“保证所有的输入都转换到线性空间,并在线性空间下做各种光照计算,最后的输出(最最最最后的输出)进行伽马校正后再显示”。

虽然Unity的这个设置非常方便,但是其支持的平台有限,目前还不支持移动平台。也就是说,在安卓、iOS上我们无法使用这个设置。因此,对于移动平台,我们需要像上面给的代码那样,手动对非线性纹理进行转换,并在最后输出时再进行一次转换。但这又会导致混合错误的问题。

在Unity中使用Linear Space

如果我们在Edit -> Project Settings -> Player -> Other Settings中使用了Linear Space,那么之前的光照、混合问题都可以解决(这里的解决是说和真实场景更接近)。但在处理纹理时需要注意,所有Unity会把所有输入纹理都设置成sRGB格式,也就说,所有纹理都会被硬件当成一个非线性纹理,使用一个display gamma(通常是2.2)进行处理后,再传递给shader。但有时,输入纹理并不是非线性纹理就会发生问题。

例如,我们绘制一个亮度为127/255的纹理,传给shader后乘以2后进行显示:

 为了告诉Unity,“嘿,这张纹理就是线性的,不用你再处理啦”,可以在Texture的面板中设置:

 上面的“Bypass sRGB Sample”就是告诉Untiy要绕过sRGB处理,“它是啥就是啥!”。

这样设置后,就可以得到正确采样结果了。

GPU Gems上的一段话可以说明伽马校正的重要性:

This is one reason why most (but not all) CG for film looks much better than games—a reason that has nothing to do with the polygon counts, shading, or artistic skills of game creators. (It’s also sometimes a reason why otherwise well-made film CG looks poor—because the color palettes and gammas have been mismatched by a careless compositor.)


最后,给出GPU Gems中的一段总结,以下步骤应该在游戏开发中应用:

1. 假设大部分游戏使用没有校正过的显示器,这些显示器的display gamma可以粗略地认为是2.2。(对于更高质量要求的游戏,可以让你的游戏提供一个伽马校正表格,来让用户选择合适的伽马值。)
2. 在对非线性纹理(也就是那些在没有校正的显示器上看起来是正确的纹理)进行采样时,而这些纹理又提供了光照或者颜色信息,我们需要把采样结果使用一个伽马值转换到线性空间中。不要对已经在线性颜色空间中的纹理,例如一些HDR光照纹理、法线纹理、凹凸纹理(bump heights)、或者其他包含非颜色信息的纹理,进行这样的处理。对于非线性纹理,尽量使用sRGB纹理格式。
3. 在显示前,对最后的像素值应用一个伽马校正(即使用1/gamma对其进行处理)。尽量使用sRGB frame-buffer extensions来进行有效自动的伽马校正,这样可以保证正确的混合。

所幸的是,在Unity中,上面的过程可以通过设置Edit -> Project Settings -> Player -> Other Settings->Color Space轻松地完成,需要注意的是对纹理的处理。但不幸的是,不支持移动平台。

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


http://http.developer.nvidia.com/GPUGems3/gpugems3_ch24.html
《Real-Time Rendering, Third Edition》5.8. Gamma Correction

/

Gamma矫正

一切事情的起点在于显示器的一个特性:颜色信息作为电压输出给显示器的时候,显示器的亮度并不是随着电压线性增加,而是一个曲线

上图中[0,1]范围内,颜色即电压是1.0那条红线,但是显示器的亮度是2.2那条蓝线,这就会导致一个问题:显示器实际显示出来的颜色比我要显示的真实图像的颜色要一些。

从数学上,这个关系就是

  • u:电压,也可以理解为颜色值。
  • l:亮度

那么问题来了,我如果想在显示器上显示出真正的颜色该怎么办?

很显然的,我要是在颜色输出之前做这么个操作,实际上这是一步提亮的操作,我就应该能在显示器上看到正确的颜色了。这步操作叫做gamma correction,即gamma矫正

sRGB色彩空间

既然我需要进行一步提亮的操作,那么把这个信息记录在图片中是不是更好?于是就有了这个sRGB色彩空间。

由于从数学上看

 

所以sRGB色彩空间我们也可以管它叫做Gamma0.45空间,它是一个将图片中的颜色整体提亮后的空间。 于是在显示器上显示一张sRGB的图片,因为显示器会自动进行一次^2.2的操作,我们就能得到:

 

这里的Gamma1指的就是线性空间的颜色,是我真正想看到的原色,即我想让显示器显示线性空间的颜色。

线性空间

在渲染的时候,我希望shader在Gamma1空间中进行运算,这样运算的结果才最准确,于是我一旦有一张sRGB的图片,由于这张图本身的颜色已经是gamma0.45空间下的了,即已经经过gamma矫正了,我需要做的是remove gamma correction,即

 

在Unity的Shader中,一旦项目设置为linear space,由于Unity支持sRGB的图片格式,在shader进行采样的时候已经隐含地执行了这个操作,例如使用tex()采样函数,或者语义为color的参数。

shader进行完计算后,因为显示器会自动进行一次^2.2的操作,所以在这之前渲染系统会做这么一件事

 

即在shader结果输出给显示器之前,做了一次gamma矫正。在Unity中,这步也是它帮你做了。

线性空间纹理和sRGB纹理

unity当中纹理可以设置是线性空间纹理还是sRGB纹理,sRGB纹理会经过上述流程,而线性空间纹理本身是在gamma1空间,因此就不再需要remove gamma correction。

一般而言,颜色纹理都是sRGB纹理,而像法线等纹理是在线性空间的。

整体流程

/

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

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

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

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

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

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

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

 

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

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

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

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

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

在数学上,伽马校正是一个约0.45的幂运算(和上面的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的幂运算 

 。如果输入不是sRGB,而是已经在线性空间的纹理了呢?那就可以跳过Remove Gamma Correction了。

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

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

第三步,Shader计算完成后,需要进行Gamma Correction,从线性空间变换到Gamma0.45空间,在数学上是一个约为0.45的幂运算 

 。如果不进行Gamma Correction输出会怎么样?那显示器就会将颜色从线性空间转换到Gamma2.2空间,接着再被你看到,结果会更暗。

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

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

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

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

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

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

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

0.5(直线) * [显示器] = 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,那么就需要渲染工程师、技术美术和美术商量和统一好工作流了。在中小团队或项目中,这些概念很容易被忽略,导致工作流混乱,渲染效果不尽人意。

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值