Go の image/color パッケージと Pre-multiplied Alpha についての考察

お断り

本稿は筆者が技術書典 3 で頒布した「Ebiten API Reference」の、ある章の抜粋および改変です。

Pre-multiplied Alpha と Straight Alpha

色は Red、 Green、 Blue、 Alpha の 4 つの成分で表されます。 Alpha は不透明度を表す値です。 Red、 Green、 Blue の値に Alpha を乗算した形式を Pre-multiplied Alpha (乗算済みアルファ) と呼びます。逆に乗算する前の形式を Straight Alpha と呼びます。

Go の image/color パッケージは、 Pre-multiplied な色の構造体は例えば RGBA という名前がついているのに対し、 Straight な色の構造体は例えば NRGBA となっています。また Color インターフェイスの RGBA 関数は 16bit の Pre-mutiplied な値を返します。 Pre-multipliedのほうが主であり Straight Alpha が従であるという趣があるようです。それは一体なぜでしょうか。

結論から言ってしまうと、 Pre-multiplied のほうがアルファブレンディングの計算が圧倒的に簡単だからです。本稿ではその理屈について説明します。

なお Go の開発者が実際に本章のようなことを考えて API を設計した、と名言しているわけではありません。ただ恐らくこのような理論が背景にあるからであろう、と筆者は考察しました。

本稿では断りがない限りは各成分は 0〜1 の値で表されます。アルファ値は、 1 のときに完全に不透明、 0 のときに完全に透明になります。

Straight Alpha のアルファブレンディング

Straight Alpha な色 c1=(r1,g1,b1,a1) と色 c2=(r2,g2,b2,a2) があったとします。この 2 つの色のアルファブレンディングについて考えてみましょう。なおここでいうアルファブレンディングは、 Thomas Porter および Tom Duff の論文 Compositing Digital Images - Computer Graphics Project Lucasfilm Ltd. に基づきます。

アルファブレンディングは 2 つの色から 1 つの色を作る演算です。ここでは演算子を とします。 c1c2 をアルファブレンディングした結果の色は c1c2 となるわけです。アルファブレンディングは可換ではありません。すなわち c1c2c2c1 が同じであるとは限りません。これは直感的には、半透明な c1 のフィルムと不透明な c2 のフィルムを組み合わせたときに、結果の色は順序に依存することから分かります。またアルファブレンディングは結合法則を満たします。 c1c2 とは別の色 c3 があったとして、 c1(c2c3)(c1c2)c3 が同じである必要があります。

c1c2 を計算してみましょう。簡単のためにまず c2 のアルファ成分 a2 が 1 のケースを考えます。この場合 c2 は完全に不透明となります。 c1 のAlpha成分 a1 分だけ c1 の RGB 値が影響し、また残り分 c2 の RGB 値が影響します。

c1c2=a1c1+(1a1)c2=(a1r1+(1a1)r2,a1g1+(1a1)g2,a1b1+(1a1)b2,1)

この結果を利用して、アルファ値が 1 でない場合について考えます。結合則を満たすように、各値を計算してみましょう 1。色 c1=(r1,g1,b1,a1),c2=(r2,g2,b2,a2) について、 c=c1c2=(r,g,b,a) を求めます。また別の色 c3=(r3,g3,b3,a3) があったとき、結合法則から cc3=c1(c2c3) でなければなりません。よって

(c1c2)c3=c1(c2c3)cc3=c1(c2c3)

ここで c3 は不透明な色、すなわち a3=1 とします。不透明な色を右にした合成もまた不透明な色なので、 c2c3 の Alpha 成分も 1 になります。こうすると先程ののアルファブレンディングの式が使えるわけです。Red成分に着目すると

ar+(1a)r3=a1r1+(1a1)(a2r2+(1a2)r3)=a1r1+(1a1)a2r2+(1a1)(1a2)r3

r3 の係数に着目すると

1a=(1a1)(1a2)a=1(1a1)(1a2)

前の式に代入して

(1(1a1)(1a2))r=a1r1+(1a1)a2r2r=a1r1+(1a1)a2r21(1a1)(1a2)=a1r1+(1a1)a2r2a1+a2a1a2

ここで a1+a2a1a20 とします。これが 0 の場合は a1=a2=0 となり、 RGB の値は意味を成しません。

Green や Blue でも議論は同じです。以上により

c=(r,g,b,a)=(a1r1+(1a1)a2r2a1+a2a1a2,a1g1+(1a1)a2g2a1+a2a1a2,a1b1+(1a1)a2b2a1+a2a1a2,a1+a2a1a2)

なんともおぞましい計算結果になりました。 Straight Alpha のアルファブレンディングは、このように単純ではないのです。

Pre-multiplied Alpha のアルファブレンディング

Pre-multiplied Alpha は Straight Alpha としての Red、Green、Blue 成分に Alpha 値を乗算した形式です。 Pre-multiplied Alpha な色 C=R,G,B,A があったとして、それに対応する Straight Alpha 形式の色が c=(r,g,b,a) とすると、 C=ar,ag,ab,a となります。前節で求めた式を Pre-multiplied Alpha 形式に変換してみましょう。 c1=(r1,g1,b1,a1),c2=(r2,g2,b2,a2) に対応する Pre-multiplied Alpha 形式の色をそれぞれ C1=R1,G1,B1,A1,C2=R2,G2,B2,A2 として、 Straight Alphaのまま C1,C2 を使った式に変形します。なお、 c=c1c2,C=C1C2 とします。

c=(a1r1+(1a1)a2r2a1+a2a1a2,a1g1+(1a1)a2g2a1+a2a1a2,a1b1+(1a1)a2b2a1+a2a1a2,a1+a2a1a 2)=(R1+(1a1)R2a1+a2a1a2,G1+(1a1)G2a1+a2a1a2,B1+(1a1)B2a1+a2a1a2,a1+a2a1a2)

これを Pre-multiplied Alpha 形式にするには、RGBの値に c のAlpha値である a=a1+a2a1a2 を乗算します。

C=R1+(1a1)R2,G1+(1a1)G2,B1+(1a1)B2,a1+a2a1a2

Straight でも Pre-multiplied でも Alpha 成分の値は変わらず、 a1=A1,a2=A2 なので

C=R1+(1A1)R2,G1+(1A1)G2,B1+(1A1)B2,A1+A2A1A2=R1+(1A1)R2,G1+(1A1)G2,B1+(1A1)B2,A1+(1A1)A2

すべての成分が同じ計算式 X1+(1A1)X2 の形式になりました。よって

C1C2=C1+(1A1)C2

Pre-multiplied Alpha におけるアルファブレンディングは、 Straigh Alpha におけるにそれとは違って綺麗になったことが分かります。

余談: OpenGL での実装

OpenGL においてブレンディングは glBlendFunc 関数で指定できます。テクスチャの色が Pre-multiplied Alpha だとすれば、正しくアルファブレンディングするには、最後の式より GL_ONEGL_ONE_MINUS_SRC_ALPHAを指定すれば良いことが分かります。逆にテクスチャの色が Straight Alphaの場合、あの「おぞましい式」のようにブレンディングが動作すれば良いのですが、そのような指定は存在しません。

よく GL_SRC_ALPHAGL_ONE_MINUS_SRC_ALPHA を組み合わせるという解説がありますが、テクスチャの色が Straight Alpha であろうと Pre-multiplied Aplha であろうと正しく動きません。先程説明したとおり、描画先が完全不透明である場合を除いて正しくならないからです 2

余談 2: リニアフィルター

Straight Alpha だとうまくいかず、 Pre-multiplied Alpha だとうまくいく他の例として、 OpenGL のリニアフィルターがあります。リニアフィルターはテクスチャから取得する色の補完に使われます。ある点の色を描画したいとして、対応するテクセルによっては、テクスチャからそのテクセルに近い部分の色を複数取得し線形補間します。これがリニアフィルターです。逆に全く補完しないで一番近くのテクセルを取るだけの場合もあり、 OpenGL ではニアレストフィルターと呼ばれます。

例えばStraight Alpha形式で赤色 c1=(1,0,0,1) と透明 c2=(0,0,0,0) があったとしましょう。これのちょうど中間の色はどう計算されるでしょうか。単純にRGBA全部の間の値をとったとすると、次のようになります:

c1+c22=(0.5,0,0,0.5)

これは赤成分が1から0.5になっており、少し黒ずんだ赤色になってしまっています。単に透明と補完しただけなのに黒ずむのは問題で、例えば絵のエッジ付近が黒ずんでしまうといったことが起きます。これは完全な透明の色の表現が一意に定まらないことから来ています。たとえば (1,0,0,0)(0.5,0.5,0.5,0) も同じ透明ですが、どの透明の表現と線形補間するのかで結果が変わってきます。

では Pre-multiplied Alpha 形式で赤色 C1=1,0,0,1 と透明 C2=0,0,0,0 のちょうど中間の色を計算するとどうなるでしょうか。これもまた単純に RGBA の間の値をとってみます:

C1+C22=0.5,0,0,0.5

これは Straight Alpha に直すと (1,0,0,0.5) であり、半透明ですがちゃんと赤色が維持されています。 Pre-multiplied Alpha における完全な透明は 0,0,0,0 のみであり、それ以外の表現はありません。よって Straight Alpha における透明色の曖昧さはありません。


  1. 計算方法については WikipediaのAlpha Compositing の記事を参照しました。 

  2. その他この問題を指摘しているスライドに Blends Mode for OpenGL - Mark Kilgard があります。