From Alpha to Gamma (I)

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)….

posted @ 2013-07-01 01:03  atyuwen  阅读(2749)  评论(0编辑  收藏  举报