AndroidSDKにはandroid.graphics.PorterDuff.Modeクラスというものがあります。これは昔PorterさんとDuffさんという人がまとめた2枚の画像の合成方法を、Androidプラグラム上で指定するためのクラスです。メンバとして16種類の合成方法名を持っていて、様々な描画メソッドでこれを指定してやると、その合成方法で描画することができます。そもそも私がPorterDuff.Modeクラスを見つけたのはAndroidで加算半透明描画する方法を探していたからなのですが、公式リファレンスの合成方法を示した表の一番上にそれらしき”ADD”とういうものがあるにもかかわらず、実際にPaintクラスに”ADD”を設定しようとしてもエラーが出てできませんでした…。表の右の方の計算式らしきものが他のものと表記方法が違うところからして、どうやら実装されていないようです。真ん中あたりにある”OVERLAY ”も使用できないようです。う〜ん残念…。しかしせっかくなので他の使用可能な設定でどのような合成ができるのか調べてみました。SDKに入っているサンプルのApiDemosの中にXfermodeのサンプルがあるのでこれを実効してみると、
のような画像の合成例が表示されます。これは合成のソース画像(重ね合わせる画像)となる水色の正方形と、合成先画像(合成の下地となる画像)となるオレンジ色の円を、PorterDuff.Modeで指定できる16の方法で合成した結果です。この例では画像のアルファが0と1で不透明と透明部分がはっきりと分かれていますが、半透明の場合でも公式リファレンスの表の右側の式に従ってアルファ値やカラー値が計算されます。と言いたいところですが、どうもこの表の式の見方がよくわかりません。単純な”SRC”や”DST”等はともかくとして、他のものは自分で適当なビットマップを作って合成してみた後、BitmapクラスのgetPixel()メソッドで色を抜き出して値を見てみても、どうにも計算が合いません。それぞれの式[,]の左側が合成後のアルファ値を求める計算式、右側が合成後のカラー値を求める計算式、Saがソース画像のアルファ値、Scがソース画像のカラー値、Daが合成先画像のアルファ値、Dcが合成先画像のカラー値だと思ったのですが、ネットでいろいろと調べた結果どうも単純にそうではないということがわかりました。まず注意しなけばならないのは、Sa、Da、Sc、Dcがアルファ値、カラー値であるとすると、プログラム上での0〜255範囲でこれらの式で計算するとまずいということです。式中の (1 - Sa) という表現からおそらくアルファ値は0〜1範囲で表現していると予想できますし、合成するとカラーが暗くなる(合成前のカラー値よりも合成後のカラー値が小さくなる)いわゆる乗算合成と思われる”MULTIPLY”のカラー値を求める式が乗算 Sc * Dc であることから、Sc、Dcは1以下の少数であると予想できます。ですので、アルファ、カラー値ともに255で割って、0〜1範囲に変換して使う必要があります。さて式の見方についてですが、[,]の左側が合成後のアルファ値を求める計算式、Saがソース画像のアルファ値、Daが合成先画像のアルファ値であることは間違いないようです。仮に合成後のアルファ値をRaとしておきます。ではカラー値の方はどうかというと、[,]の右側の式で求められるのは確かに合成後のカラー値ですが、カラー値そのものではなくカラー値に合成後のアルファ値Raをかけた値になっています。同様にSc、Dcもカラー値そのものではなく、カラー値にそれぞれのアルファ値をかけた値を使います。まとめると
Sa = ソース画像のアルファ値
Da = 合成先画像のアルファ値
[,]の左側 = Sa、Daから計算される合成後画像のアルファ値 = Ra
Sc = ソース画像のカラー値 × Sa
Dc = 合成先画像のカラー値 × Da
[,]の右側 = 合成後画像のカラー値 × Ra
となります。コンピュータでARGB色指定を扱う場合にはこのようにあらかじめアルファ値をかけたカラー値の形で値を保持していることが多いそうです。また先に述べた値の0〜255範囲での指定を0〜1範囲に変換するのもあり、8Bit整数で指定された各値が少数となり端数が切り捨てられたりして、描画を行っていると微妙に値がずれてきます。実際PorterDuff.Modeのテストをしていて、空のビットマップを作成してdrawColorで塗りつぶした直後にgetPixel()メソッドで色を確認すると、もうすでに値が1〜2ずれていたりするのでそういうものなのでしょう。ともかく、合成後画像のアルファ値とは関係なく純粋なカラー値を知りたければ、[,]の右側を求めた後に各RGBの値をRaで割ってやる必要があります。Raが0の場合は当然割ることができませんが、その場合はカラー値は強制的に全て0になるようです。また”DARKEN”のmin(Sc, Dc)と”LIGHTEN”のmax(Sc, Dc)の表記についてですが、それぞれScとDcの小さい方、大きい方を取るというのは想像できるかもしれませんが、どうもこの中のScとDcが上に述べたScとDcとは違うようで、
min(Sc, Dc) =
min(ソース画像のカラー値 × Sa × Da, 合成先画像のカラー値 × Da × Sa)
max(Sc, Dc) =
max(ソース画像のカラー値 × Sa × Da, 合成先画像のカラー値 × Da × Sa)
のように、それぞれさらに合成相手のアルファ値をかけたものを比較して、その値を使用しなければならないようです。ちなみに”DST_OVER”と”SRC_OVER”のみ、式[,]の右側のカラー値のところに Rc = という表記がされているのは何か意味があるのかと思ったのですが、結局よくわかりませんでした。無視して良いのではないかと思います。PorterDuff.Modeを指定して半透明の画像を合成してみて、どうしてこういう結果になるのかわからないという時には上のことを参考にしていただければと思います。さて実際使用するとなると、サンプルの様に完全透明、不透明の画像で合成をすることが多いかもしれません。よくある使い方は”CLEAR”を使って消しゴムのような使い方をするとか、次に示すように”SRC_ATOP”を用いて描画先画像のアルファはそのままにソースのカラーで塗りつぶす等も便利だと思います。
Resources res = getResources();
//100×100サイズの黄色い星型のアルファ付きビットマップ
Bitmap yellow_star = BitmapFactory.decodeResource(res,R.drawable.star);
//黄色い星型ビットマップのコピーを作成
Bitmap red_star = yellow_star.copy(Bitmap.Config.ARGB_8888,true);
Canvas dstCanvas = new Canvas(red_star);
//コピーした星をSRC_ATOPモードで赤で塗りつぶす
dstCanvas.drawColor(Color.argb(255,255,0,0),
android.graphics.PorterDuff.Mode.SRC_ATOP);
//SurfaceViewに2つの星を描画してみる
myCanvas.drawBitmap(yellow_star,0,0,null);
myCanvas.drawBitmap(red_star,150,0,null);
このように簡単に色違いのアルファ付画像を作成できます。LightingColorFilterクラスを使っても同じことができると思いますが、上の例の方がフィルターのパラメータの設定などしなくてすむ分簡単だと思います。余談ですが、Canvasクラスの塗りつぶし系メソッドdrawARGB()やdrawColor()のモードを指定しないバージョンでは、PorterDuff.Modeの”SRC”で示されるソースカラーでの完全な塗りつぶしではなく、”SRC_OVER”モードでの下地画像との合成となります。キャンバスを初期化する用途で塗りつぶす場合、塗りつぶすカラーが完全不透明なら問題は無いのですが、塗りつぶすカラーが半透明だと(あまりそういうことは無いかもしれませんが)完全な塗りつぶしではなく下地の画像との合成になってしまいますので気をつける必要があります。
|