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

-- 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空间中进行的,这在下图中表现得很明显,

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

此时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)$。

From Alpha to Gamma (II)的更多相关文章

  1. From Alpha to Gamma (I)

    What we think of as conventional alpha-blending is basically wrong. --Tom Forsyth 前段时间在Amazon上淘的三本二手 ...

  2. 雷达无线电系列(三)经典CFAR算法门限因子alpha计算(matlab)

    前言 本文汇集CA.SO.GO.OS.杂波图等恒虚警算法的门限因子求解方法及其函数 1,CA-CFAR [非常简单,可以直接求解] %% 均值恒虚警_门限因子计算公式 %% 版本:v1 %% 时间:2 ...

  3. Python:基本语法1

    I.Python中的转义符注意情况 如果'本身是一个字符,则可将其用" "括起来: 如果字符串内部既有',又有",则可用转义字符\,比如: 'I\'m\"OK\ ...

  4. HTML 列表 <ol><ul><li><dl><dt><dd>

    <ol>标签-有序列表 定义和用法: <ol>标签定义有序列表. HTML 与 XHTML 之间的差异 在 HTML 4.01 中,ol 元素的 "compact&q ...

  5. css内容样式属性

    设置元素的最大高度.最小高度.最大宽度.最小宽度,用max-height.min-height.max-width.min-width. visibility:设置元素是否可见.visible和hid ...

  6. HTML前端——CSS样式

    使用CSS样式的方式: HTML<!DOCTYPE> 声明标签 内链样式表<body style="background: green; margin: 0; paddin ...

  7. (八)学习CSS之line-style-type属性

    参考:http://www.w3school.com.cn/cssref/pr_list-style-type.asp 设置不同的列表样式: ul.circle {list-style-type:ci ...

  8. 【转】LaTeX 符号命令大全

    函数.符号及特殊字符 声调 语法 效果 语法 效果 语法 效果 \bar{x} \acute{\eta} \check{\alpha} \grave{\eta} \breve{a} \ddot{y} ...

  9. list-style-type -- 定义列表样式

    取值:disc | circle | square | decimal | decimal-leading-zero | lower-roman | upper-roman | lower-greek ...

随机推荐

  1. SqlServer 查询死锁,杀死死锁进程*转载

    原文: -- 查询死锁 select request_session_id spid, OBJECT_NAME(resource_associated_entity_id) tableName fro ...

  2. linux用户和组

    1.用户隶属于用户组的. 2.用户与用户组配置文件 1)用户组配置文件 /etc/group 第一列:用户组的组名 第二列:组密码(真正的密码存储在了gshadow中) 第三列:用户组组ID,用户组唯 ...

  3. jQuery中animate()对Firefox无效的解决办法

    在使用 animate()做返回顶部的动画时,会出现对Firefox无效的情况,如: $('body').animate({scrollTop:'0'},500); 它对Chrome,IE,Opera ...

  4. Java jdk 8 新特性

    list 统计(求和.最大.最小.平均) 第一种方式 int suma = listUsers.stream().map(e -> e.getAge()).reduce(Integer::sum ...

  5. linux永久关闭防火墙

  6. 简明PR教程

    注意:本文供培训使用且仅为第一版 作者也不打算继续更新 本篇文章最早是在为内部培训时所编写的文章 有些疏漏且没有进行校正等工作 我尽力用最简单通俗的语言给大家介绍PR的使用方法 简明PR教程 1.编辑 ...

  7. 开始Java之旅

      从今天起,cgg将给大家讲讲Java这种神奇的东西.   至于配置环境变量,大家可以看看我的博客:环境变量上面有详细解释.   下面先给大家一个公式:    public class [文件名]{ ...

  8. DevExpress TextEdit Focus问题

    在标签切换时设置第一个TextEdit获取输入焦点无效,需要采用消息Post方式设置 //标签切换事件 xtraTabControl1.Selected += (s, e) => { if (e ...

  9. IntelliJ IDEA 2017版 导入项目项目名称为红色

    1.导入的项目全部是红色的,原因是版本控制问题,所以修改如下:(File--->settings) 2.找到如图位置的字样,选中当前项目,选择铅笔位置 选择铅笔 弹出对话框(默认选择的是proj ...

  10. php mysql增删查改

    php mysql增删查改代码段 $conn=mysql_connect('localhost','root','root');  //连接数据库代码 mysql_query("set na ...