• From Alpha to Gamma (II)


    这篇文章被拖延得这么久是因为我没有找到合适的引言

    -- XXX

    这一篇接着讲Gamma。近几年基于物理的渲染(Physically Based Shading, 后文简称PBS)开始在游戏业界受到关注并迅速流行。10年、12年以及今年的siggraph都专门开了一个course来介绍基于物理的渲染的基本理论和工程实践,这在很大程度上推动了PBS的普及。如果你对PBS还不太了解的话,不妨先看一看龚大写的系列文章。在从传统渲染管线切换到PBS管线的过程中,一个非常基础而又重要的概念叫做伽马正确(Gamma Correct),为了保证伽马正确需要做的一个操作叫做伽马校正(Gamma Correction)。Gamma Correction其实就是一个颜色的变换,它将输入的颜色从Nonlinear Color Space转换到Linear Color Space,然后在输出到Frame Buffer时又重新转换到Nonlinear Color Space。到这里问题来了,为什么我们需要Gamma Correction? 为什么输入的颜色会是Nonlinear而不是Linear的? 为什么再将图像送到Frame Buffer进行显示时又需要重新转换到Nonlinear Color Space?为了解释这些问题,先来谈一谈什么是Gamma。

    网上有很多介绍Gamma的文章,但是其中一些写得啰里吧嗦大家不愿意看(比如维基百科),另一些又解释得不十分准确(比如百度百科)。其实要理解Gamma,只需要抓住下面两点:

    1. 历史原因: Gamma Correction首先是为了补偿老式CRT显示器的输入电压(Voltage)到输出强度(Intensity)的非线性效应;
    2. 编码的效率问题 : 在使用定点数编码(比如最常用的R8B8G8格式)的情况下,将颜色表示在更符合视觉响应曲线的Nonlinear Color Space有助于提高bits的利用率,避免出现肉眼可观察的色阶断层(Banding)。

    如果你之前就已经对Gamma Correction有所了解,看到上面两点应该就已经很明白了。如果还是不太理解,没关系,下面就再来详细解释一下。首先来看第一点。CRT显示器在显示一幅图像时,从Frame Buffer里面的RGB颜色值到Voltage的转化是线性的,而从Voltage到最终输出的Intensity是非线性的,这个非线性关系近似于 $I = alpha V^{gamma_d}$。这个$gamma_d$叫做Display Gamma, 其值通常在2.35和2.55之间($gamma_d$和$alpha$的值还可以手动进行微调,分别对应于显示器的亮度和对比度参数)。现在的LCD显示器本来不存在这个问题,但是为了与CRT显示器保持兼容,也都采用了这样一种非线性的转换关系。因此当我们计算出或者捕捉到一幅图像时,为了让最终的显示效果正确,需要将颜色值预先pow一个$gamma_e$以补偿这个非线性效应,这个过程叫做Gamma Encoding,$gamma_e$也叫做Encoding Gamma。理论上,为了完全抵消掉了Display Gamma的pow关系,应该有$gamma_e gamma_d = 1$,但事实上并非如此,比如一般所采用的$gamma_e$在0.45左右,而$gamma_d$约在2.5左右,这使得$gamma_t = gamma_e gamma_d =  1.125 $。这里的$gamma_t$叫做End-to-End Gamma,其值通常不会是1,这是为了补偿观察环境不同造成的人眼对颜色的感知变化(Surround Effect)。

    现在说第二点,来考察一下人眼的非线性效应与编码的效率之间的关系。显示器输出的Intensity与人眼所感知到的亮度(Lightness)也是非线性的(近似于一个对数关系),我们用函数$y = v(x)$来表示。如果假设没有显示器的非线性效应,没有Gamma Correction,那从真实的物理上的亮度(Radiance)到被人眼观察到形成视觉信号(Lightness)的完整过程是这样的:

    1. 输入图像,用$x$来表示;
    2. 将$x$进行编码(比如按照R8B8G8表示为24位色),这实际上是一个量化的过程,得到$q(x)$。因为编码的位深是有限的,因此在这一步会出现误差;
    3. 将$q(x)$送入Frame Buffer,并在显示器上输出,因为这里没有非线性效应,所以输出还是$q(x)$;
    4. 人眼进行观察,最终观察到的Lightness为$(v cdot q) (x)$.

    分析一下因为量化而造成的Banding。因为我们是使用的定点数编码,这意味着量化抽样是均匀分布的。所以$q(x)$的Banding即为抽样的间隔$delta$,而人眼所感受到的Lightness的Banding可以近似表示为$K(v)delta$,其中$K(v)$表示$v(x)$的最大斜率。现在如果加上Gamma Correction,也就是在$x$量化前后分别进行一次Gamma Encoding(用$e(x)$表示)和Display Transfer(用$d(x)$表示),那么此时Lightness的表达式为$(v cdot d cdot q cdot e) (x)$。注意误差的扩大总是由$q$左边的项所引起,因此最终的Banding即为$K(v cdot d)delta$。一个幸运的巧合时,人眼的亮度感知曲线$v$与Display Transfer的曲线刚好差不多相反,于是有$v cdot d approx identity$,因此$K(v cdot d)delta approx delta < K(v) delta$。也就是说,虽然人眼感知的非线性效应会将量化误差扩大化,但是Display Transfer正好将此抵消掉了。所以为了减轻Banding,Display Transfer是很有必要的,相应的Gamma Encoding当然也必不可少。


    好了,前面说了一通Gamma,现在我们再回到Alpha。Alpha跟Gamma有什么关系?答案是没有关系。在上一篇中我们说了Alpha的两种意义,一种是表示Coverage,一种是表示Opacity,但是后者并没有物理上的意义,如果非要找一个的话,它其实还是Coverage。而Coverage按理来说自然与Gamma Encoding或者Tone Mapping都没什么关系,或者说,Alpha Blending应该发生在这二者之前。但是,图像处理软件的默认颜色空间一般是sRGB,混合也是在sRGB空间中进行的,这在下图中表现得很明显,

        half_trans

    上图的opacity对应于PS中的图层不透明度。如果你的显示器正常的话,你应该可以看到checkerboard(50% coverage)大概与73% opacity看上去一样(73%约等于pow(50%, 0.45))。在PS中将上图缩小至一半大小,

        half_trans1

    此时checkerboard又变得跟50% opacity完全相同。有点奇怪是不是? 这是因为PS中的filter也是在sRGB空间中进行的。我们当然可以认为在sRGB空间进行filter或者alpha compositing都是错的,但是现在美术的Workflow大多都还是基于sRGB的,“理论正确”的洁癖在这里没有什么意义。sRGB的Workflow与Linear RGB的渲染管线共存是目前必须面对的局面(Fox Engine对Linear Workflow的探索似乎指出了未来发展的方向),这样带来的一个小麻烦就是美术与程序对Alpha的感觉不太一样,就如上图所示的那样,美术认为的73% opacity在程序的眼中应该是50%(注意这里的不透明度换算关系仅当背景是黑色的才有效)。那么怎么解决这一问题呢?分两种情况,

    1. 如果是带Alpha通道的3D贴图,opacity有些不一样关系并不会太大,有美术抱怨的话向他们解释一下原因,必要时让他们参照实际渲染出来的效果对贴图的Alpha相应做一下调整即可,
    2. 如果是UI贴图,因为需要准确地还原在DCC软件中的效果,唯一合理的做法是将UI的渲染放到Gamma Correction之后,关闭sRGB Read/Write直接写到LDR Buffer。相信你的引擎肯定也的确是这么干的。有些引擎比较奇葩一点(比如 Unreal),它使用sRGB Read,但并不开启sRGB Write而是自己来做Gamma Correction,注意sRGB space != pow(1/2.2) space,在这种情况下简单的一个pow(2.2)的精确度是不够的,正确的做法可以参考sRGB spec

    另外再说一下在前一篇中提到的3D UI问题。当时说的一个方案是先将底层的UI渲染到Linear的Render Target上,然后再往上面渲染3D内容。 但是经过前面的讨论你应该也可以感受到把UI渲染到Linear Buffer上会有多么的麻烦。如果你的UI底图有多层的话,那么它们应该先在sRGB空间中混合; 另外在拷贝到Linear Buffer上的时候同样也需要注意到sRGB space != pow(1/2.2) space的问题,如果你最后的Gamma Correction是pow(1/2.2),那这里就不能使用sRGB Read而是需要手动pow(2.2); 如果还有Tone Mapping的话,这个地方也需要有相应的处理; 如果Tone Mapping还是自动exposure的或者还有Color Grading,这就基本上搞不定了。所以还是别纠结了,换另一种方案,用Premultiplied Alpha吧。


    上一篇中讲到,Premultiplied Alpha混合比传统混合更强大更优雅。那么当Premultiplied Alpha遇上Gamma会发生什么事呢?其实并没有什么特别的地方,只是在将sRGB空间中的颜色$(sR, sG, sB, A)$转换成Premultiplied Alpha表示时需要注意一下,如果直接按照与Linear RGB相同的方式处理,即:

        $C_1 = (sR cdot A, sG cdot A, sB cdot A, A)$

    在渲染时,将$C_1$按照sRGB纹理进行读取,得到

        $C_2 = (R cdot A^{2.2}, G cdot A^{2.2}, B cdot A^{2.2}, A)$

    因此若想还原得到Linear的$(R cdot A, G cdot A, B cdot A, A)$,还需要先计算出$A^{2.2}$,然后再用它来除一下$C_2$的RGB通道,相当繁琐。这里主要的麻烦在于那个$A^{2.2}$,为了避免它,可以预先将$A$也转换到sRGB空间,

        $C_3 = (sR cdot sA, sG cdot sA, sB cdot sA, A)$

    这样对$C_3$进行sRGB decoding之后就直接得到$(R cdot A, G cdot A, B cdot A, A)$。

  • 相关阅读:
    Leetcode Reverse Words in a String
    topcoder SRM 619 DIV2 GoodCompanyDivTwo
    topcoder SRM 618 DIV2 MovingRooksDiv2
    topcoder SRM 618 DIV2 WritingWords
    topcoder SRM 618 DIV2 LongWordsDiv2
    Zepto Code Rush 2014 A. Feed with Candy
    Zepto Code Rush 2014 B
    Codeforces Round #245 (Div. 2) B
    Codeforces Round #245 (Div. 2) A
    Codeforces Round #247 (Div. 2) B
  • 原文地址:https://www.cnblogs.com/atyuwen/p/alpha_gamma_2.html
Copyright © 2020-2023  润新知