Edited at
GoDay 21

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

More than 1 year has passed since last update.


お断り

本稿は筆者が技術書典 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 な色 $c_1 = (r_1, g_1, b_1, a_1)$ と色 $c_2 = (r_2, g_2, b_2, a_2)$ があったとします。この 2 つの色のアルファブレンディングについて考えてみましょう。なおここでいうアルファブレンディングは、 Thomas Porter および Tom Duff の論文 Compositing Digital Images - Computer Graphics Project Lucasfilm Ltd. に基づきます。

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

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

\begin{align}

c_1 \odot c_2 & = a_1 c_1 + (1 - a_1) c_2 \\
& = (a_1 r_1 + (1 - a_1) r_2, a_1 g_1 + (1 - a_1) g_2, a_1 b_1 + (1 - a_1) b_2, 1)
\end{align}

この結果を利用して、アルファ値が 1 でない場合について考えます。結合則を満たすように、各値を計算してみましょう 1。色 $c_1 = (r_1, g_1, b_1, a_1), c_2 = (r_2, g_2, b_2, a_2)$ について、 $c = c_1 \odot c_2 = (r, g, b, a)$ を求めます。また別の色 $c_3 = (r_3, g_3, b_3, a_3)$ があったとき、結合法則から $c \odot c_3 = c_1 \odot (c_2 \odot c_3)$ でなければなりません。よって

\begin{align}                                                                                                                                                                    

(c_1 \odot c_2) \odot c_3 &= c_1 \odot (c_2 \odot c_3) \\
c \odot c_3 &= c_1 \odot (c_2 \odot c_3)
\end{align}

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

\begin{align}

a r + (1 - a) r_3 &= a_1 r_1 + (1 - a_1)(a_2 r_2 + (1 - a_2)r_3) \\
&= a_1 r_1 + (1 - a_1) a_2 r_2 + (1 - a_1)(1 - a_2) r_3
\end{align}

$r_3$ の係数に着目すると

\begin{align}

1 - a &= (1 - a_1)(1 - a_2) \\
a &= 1 - (1 - a_1)(1 - a_2)
\end{align}

前の式に代入して

\begin{align}

(1 - (1 - a_1)(1 - a_2)) r &= a_1 r_1 + (1 - a_1) a_2 r_2 \\
r &= \frac{a_1 r_1 + (1 - a_1) a_2 r_2}{1 - (1 - a_1)(1 - a_2)} \\
&= \frac{a_1 r_1 + (1 - a_1) a_2 r_2}{a_1 + a_2 - a_1 a_2}
\end{align}

ここで $a_1 + a_2 - a_1 a_2 \neq 0$ とします。これが 0 の場合は $a_1 = a_2 = 0$ となり、 RGB の値は意味を成しません。

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

\begin{align}

c &= (r, g, b, a) \\
&= (\frac{a_1 r_1 + (1 - a_1) a_2 r_2}{a_1 + a_2 - a_1 a_2}, \frac{a_1 g_1 + (1 - a_1) a_2 g_2}{a_1 + a_2 - a_1 a_2}, \frac{a_1 b_1 + (1 - a_1) a_2 b_2}{a_1 + a_2 - a_1 a_2}, a_1 + a_2 - a_1 a_2)
\end{align}

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


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

Pre-multiplied Alpha は Straight Alpha としての Red、Green、Blue 成分に Alpha 値を乗算した形式です。 Pre-multiplied Alpha な色 $C = \langle R, G, B, A \rangle$ があったとして、それに対応する Straight Alpha 形式の色が $c = (r, g, b, a)$ とすると、 $C = \langle a r, a g, a b, a \rangle$ となります。前節で求めた式を Pre-multiplied Alpha 形式に変換してみましょう。 $c_1 = (r_1, g_1, b_1, a_1), c_2 = (r_2, g_2, b_2, a_2)$ に対応する Pre-multiplied Alpha 形式の色をそれぞれ $C_1 = \langle R_1, G_1, B_1, A_1 \rangle, C_2 = \langle R_2, G_2, B_2, A_2 \rangle$ として、 Straight Alphaのまま $C_1, C_2$ を使った式に変形します。なお、 $c = c_1 \odot c_2, C = C_1 \odot C_2$ とします。

\begin{align}

c &= (\frac{a_1 r_1 + (1 - a_1) a_2 r_2}{a_1 + a_2 - a_1 a_2}, \frac{a_1 g_1 + (1 - a_1) a_2 g_2}{a_1 + a_2 - a_1 a_2}, \frac{a_1 b_1 + (1 - a_1) a_2 b_2}{a_1 + a_2 - a_1 a_2}, a_1 + a_2 - a_1 a_\
2) \\
&= ( \frac{R_1 + (1 - a_1) R_2}{a_1 + a_2 - a_1 a_2}, \frac{G_1 + (1 - a_1) G_2}{a_1 + a_2 - a_1 a_2}, \frac{B_1 + (1 - a_1) B_2}{a_1 + a_2 - a_1 a_2}, a_1 + a_2 - a_1 a_2)
\end{align}

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

\begin{align}

C &= \langle R_1 + (1 - a_1) R_2, G_1 + (1 - a_1) G_2, B_1 + (1 - a_1) B_2, a_1 + a_2 - a_1 a_2 \rangle
\end{align}

Straight でも Pre-multiplied でも Alpha 成分の値は変わらず、 $a_1 = A_1, a_2 = A_2$ なので

\begin{align}

C &= \langle R_1 + (1 - A_1) R_2, G_1 + (1 - A_1) G_2, B_1 + (1 - A_1) B_2, A_1 + A_2 - A_1 A_2 \rangle \\
&= \langle R_1 + (1 - A_1) R_2, G_1 + (1 - A_1) G_2, B_1 + (1 - A_1) B_2, A_1 + (1 - A_1) A_2 \rangle
\end{align}

すべての成分が同じ計算式 $X_1 + (1 - A_1) X_2$ の形式になりました。よって

\begin{align}

C_1 \odot C_2 &= C_1 + (1 - A_1) C_2
\end{align}

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形式で赤色 $c_1 = (1, 0, 0, 1)$ と透明 $c_2 = (0, 0, 0, 0)$ があったとしましょう。これのちょうど中間の色はどう計算されるでしょうか。単純にRGBA全部の間の値をとったとすると、次のようになります:

\frac{c_1 + c_2}{2} = (0.5, 0, 0, 0.5)

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

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

\frac{C_1 + C_2}{2} = \langle 0.5, 0, 0, 0.5 \rangle

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





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



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