What we think of as conventional alpha-blending is basically wrong.

--Tom Forsyth

前段时间在Amazon上淘的三本二手书——一本Jim Blinn's Corner系列的Dirty Pixels, 另二本是Andrew Glassner's Notebook——几经周折终于是送到了,当然立刻马上是迫不及待地大致翻了一遍。相比于Andrew Glassner的天马行空笔下千秋(从几何讲到折纸和形状合成,从对称讲到织纹和拼接,甚至还有一大坨似乎是在讲量子计算),我还是更喜欢Jim Blinn的风格,简单实用接地气。在Dirty Pixels中有几章是讲Alpha Compositing的,恰巧我最近也思考了一下这个问题,在看了Jim Bliin的文章后,颇有一些收获,但同时也引出了更多的疑问。后来又细细思索了一番,心中的疑虑基本都得到了解决,也算是给自己了一个交待。这样一个看上去简单到爆的问题居然有这么多微妙的地方,因此决定写一篇博文分享一下,希望也能给大家一些启发。如果你对此有更好的见解,感觉自由去留个评论。

所谓的Alpha Compositing(或者叫Alpha Blending)即将两个或者多个图层根据某个权值(即Alpha)进行混合,最普通不过的形式是这样的:  FinalColor = SrcColor * SrcAlpha + DstColor * (1.0 – SrcAlpha),后文简称这种方式的混合为“普通混合”。一眼看上去,这个公式显然是光荣而绝对正确的,没什么可质疑的地方。但本文将要说明该公式虽然简单但并不是万能的,在有些时候甚至是错误的,而采用另一种形式的混合方式也许是更好的选择。

在进入讨论之前,先来说说我们为什么需要Alpha Compositing。使用Alpha Compositing的目的之一是反走样(Anti-Aliasing),当被绘制的几何图元并没有完全覆盖一整个像素格(screen grid cell)时,可以计算出该图元所占的面积与格子的面积之比,即覆盖率(coverage),然后根据coverage进行融合。另一个需要用到Alpha Compositing的场合是半透明物体的渲染,通常使用一个alpha值来表示前景物体的不透明度(opacity),alpha值越低表示越能够透过前景物体看到背景物体。虽然这样的处理并没有什么物理上的意义(建立半透明的物理模型至少应该考虑到反射、折射、吸收,稍微复杂一点的还需要考虑到散射),便这的确是一个方便而又能欺骗眼睛的有效手段。也有一类方法是将半透明物体的渲染通过coverage转换成不透明物体来处理,比如alpha to coverage或screendoor transparency。所以coverage和opacity这两者有时可以统一起来。

在实际的Compositing处理中,下面两个问题是一定会遇到的:

  1. Filtering. 一张带alpha通道的贴图应该怎么进行filter才是正确的? 一个正确的filter应该能够保证先filter再混合跟先混合再filter的结果一致。特别地,在使用普通混合的情况下, 对alpha贴图进行linear Mipmapping是有效的么?
  2. Associativity. 对于多个图层的混合,是否一定要按由底至顶的顺序来进行? 很多时候我们需要先将上面几层进行合成,最后再与底图混合(比如一个常见的情形是将3D内容渲染到HUD层上)。这在普通混合方式下能否做到?

对于第一个问题,其实稍微考虑一下就能发现可疑之处。比如有两个像素表示成RGBA的形式分别是p = (255, 0, 0, 0), q = (0, 0, 0, 255),假设这两个像素正好被一个box filter重采样至一个像素,那么得到的结果为 r = (p + q) / 2 = (128, 0, 0, 128)。注意,一个全透的红色加一个不透明的黑色搞出来一个半透的暗红色,这个结果当然是错误的,因为p既然是全透明的,那它的RGB值不应该对r造成任何污染。凭感觉也可以知道,真正正确的结果应该是一个半透明的黑色。

至于问题2,在草稿纸上划一下就可以发现,普通混合并不是结合的,即不满足 (A over (B over C)) = ((A over B) over C)。也就是说如果我们想先把两个图层混合起来,再把结果覆盖于第三个图层之上进行混合,这样做并不能得到意想中的结果。事实上对于普通混合来说唯一有意义的顺序就是从不透明的底图开始一层一层往上叠加。这一点从上面的公式也可以看出来:公式中并没有出现DstAlpha,是因为它根本不care,也care不了(所以很多游戏引擎会把Scene Render Target的alpha通道用来干别的事情,比如存depth)。就前面举的3D HUD的例子来说,你当然也不能先把3D场景渲染到一张Render Target上,然后再与你的UI底图混合。一个可行的办法是先将UI底图拷贝至Render Target, 然后再渲染3D内容,但如果你的引擎是工作在linear color space的话,此事又麻烦了,后面讲到gamma的时候再接着讨论。

由上所述,普通的混合公式实际上是带了一个隐含条件,即其混合目标是完全不透明的(DstApha=1.0)。那么正确的混合两个半透明的图层的方式应该是怎样的呢,下面就来推导一下。假设有两个图层颜色分别是(A, a)和(B, b),这里用大写字母表示颜色,小写字母表示不透明度。设这两个图层的混合结果为(C, c),另外再假设还有一个不透明的背景层(G, 1.0), 如果混合正确的话C叠加到G上的结果应该跟B和A依次叠加到G上的结果一致,即 C over G = A over (B over G),将该式展开有:

C * c + G * (1 – c) = A * a + (B * b + G * (1-b)) * (1 – a)  = A * a + B * b * (1 – a) + G * (1 – b) * (1 – a) .

此式应对所有的G都成立,于是得:

c = 1 - (1 – b) * (1 – a) = a + b – ab;

C = (A * a + B * b * (1 – a) ) / c .

上面两个式子才应该是完整的混合公式,而普通混合只是上式在 b = 1.0 时的一个特殊情形。这两个式子乍一看上去有点复杂,特别是第二式还涉及到除法,但略微整理一下后其实很有规律:

c = a + b * (1 – a);

Cc = Aa + Bb * (1 – a);

看出来了没?现在两个式子都是形如 Z = X + Y * (1 – a)的形式。如果我们把颜色的表示从(R, G, B, A)变成(R * A, G * A, B * A, A),即预先将alpha值乘入颜色中,那么整个混合就可以表示成: Final = Src + Dst * (1.0 – SrcAlpha)。简单优雅!这种预先乘上alpha的颜色表示叫做(opacity) associated color,或者叫做premultiplied alpha(相对于此,普通的颜色表示就叫postmultiplied alpha,也有人称为non-multiplied alpha或straight alpha)。我们把使用premultiplied alpha颜色表示的混合叫做“premultiplied alpha混合”,使用premultiplied alpha混合可以很好的解决上面提出的两个问题:

  1. 在premultiplied alpha混合下的filter操作是正确的(这一点请自行验证)。在使用普通混合时所需要担心的mipmap生成或者是采样时的filter等等(如果考虑到贴图的压缩,你还需要担心得更多,参见这篇文章的叙述),在使用premultiplied alpha时都不再是问题。
  2. premultiplied alpha混合是结合(associative)的,这意味着你可以按照任意的顺序进行混合(注意你仅仅可以调换“操作”的顺序而不是“操作数”的顺序,因为混合显然不是交换的)。在某些场合这一性质是必需的,一个例子是我刚才举的3D HUD。另一个常见是例子是粒子系统的优化:为了减轻填充率的压力,可以将粒子渲染到1/2分辨率的render target上,再合成回scene render target,这只有在混合是associative的情况下才是正确的。使用premultiplied alpha甚至还有助于我们开启更多的粒子优化手段.

综上可见premultiplied alpha混合相对于普通混合有N多的优点,而且除了会影响到现有美术流程之外也没有什么缺点。所以我们没有理由还坚持使用传统的混合方式。当你需要更严格的处理多个半透明图层的混合时(特别地,当你所渲染出来的内容还需要跟外部的背景进一步混合时),premultiplied alpha几乎总是更好的选择,比如webgl, flash, silverlight都是默认或者只支持premultiplied alpha混合。一些游戏开发框架比如XNA或者cocos2d也都提供了对premultiplied alpha的原生支持。可能正如Tom Forsyth所说,传统的混合基本上就是一个错误,而要完全纠正这一切也许还需要20年。

未完待续(下一篇讲gamma)….

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

  1. From Alpha to Gamma (II)

    这篇文章被拖延得这么久是因为我没有找到合适的引言 -- XXX 这一篇接着讲Gamma.近几年基于物理的渲染(Physically Based Shading, 后文简称PBS)开始在游戏业界受到关注 ...

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

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

  3. java转换 HTML字符实体,java特殊字符转义字符串

    为什么要用转义字符串? HTML中<,>,&等有特殊含义(<,>,用于链接签,&用于转义),不能直接使用.这些符号是不显示在我们最终看到的网页里的,那如果我们希 ...

  4. MarkDown+LaTex 数学内容编辑样例收集

    $\color{green}{MarkDown+LaTex 数学内容编辑样例收集}$ 1.大小标题的居中,大小,颜色 [例1] $\color{Blue}{一元二次方程根的分布}$ $\color{R ...

  5. Python:基本语法1

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

  6. Html 特殊符号

    HTML特殊符号对照表 特殊符号 命名实体 十进制编码 特殊符号 命名实体 十进制编码 Α Α Α Β Β Β Γ Γ Γ Δ Δ Δ Ε Ε Ε Ζ Ζ Ζ Η Η Η Θ Θ Θ Ι Ι Ι Κ ...

  7. XML中输入特殊符号

    XML中输入特殊符号 周银辉 特殊符号比如 ™, 要在xml中使用的话, 其实和html的转码是一样的, 参考下面这个表(使用十进制编码那一列) 特殊符号 命名实体 十进制编码 特殊符号 命名实体 十 ...

  8. 修改AssemblyInfo.cs自动生成版本号

    一. 版本号自动生成方法 1.把 AssemblyInfo.cs文件中的[assembly:AssemblyVersion("1.0.0.0")]改成[assembly:Assem ...

  9. Html特殊字符表

    原始字符 entity 原始字符 entity " " & & ' ' < < > >     ¡ ¡ ¢ ¢ £ £ ¤ ¤ ¥ ¥ ¦ ...

随机推荐

  1. win10下docker安装和配置镜像仓库

    初学docker记录一下流程 1.首先安装直接官网下载 DockerToolbox 即可,安装过程傻瓜式下一步即可.(这个集成了虚拟机,果然安装过的可以去掉) 2.安装好后双击Docker Quick ...

  2. struts2框架值栈的概述之问题一:什么是值栈?

    1. 问题一:什么是值栈? * 值栈就相当于Struts2框架的数据的中转站,向值栈存入一些数据.从值栈中获取到数据. * ValueStack 是 struts2 提供一个接口,实现类 OgnlVa ...

  3. postman get和post结合

  4. OpenSCManager

    添加服务程序 执行级别:必须管理员

  5. 递归生成treeview树形节点(没有用递归函数之后会有补充,这里只用系统的内置方法去生成)

    using System;using System.Collections.Generic;using System.ComponentModel;using System.IO;using Syst ...

  6. 8.15jsp document 头部声明 区别

    <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/ ...

  7. 从一个流中读数据--fread

    头文件:#include<stdio.h> 函数原型:int fread(void *ptr,int size,int nitems,FILE *stream); 参数说明: ptr:用于 ...

  8. Linux下JDK应该安装在哪个位置

    在百度知道上看到的回答觉得不错:https://zhidao.baidu.com/question/1692690545668784588.html 如果你认为jdk是系统提供给你可选的程序,放在op ...

  9. Netty学习第五节实例进一步学习

    概念理解: Netty是基于NIO的框架  传统IO与NIO的区别:       1.传统IO会造成阻塞点:       2.单一的客户端处理消息 解决阻塞问题:建立线程池,达到收到一个消息就建立一个 ...

  10. UVa 12545 Bits Equalizer (贪心)

    题意:给出两个等长的字符串,0可以变成1,?可以变成0和1,可以任意交换s中任意两个字符的位置,问从s变成t至少需要多少次操作. 析:先说我的思路,我看到这应该是贪心,首先,如果先判断s能不能变成t, ...