VC++でGDI+ そにょ7 〜画像の描画3:ブレンド〜
ブレンド〜ビットマップの操作〜
今までの画像の描画は透明度だけを考慮したアルファブレンドである。しかしながらゲームでは派手なエフェクトのためにスクリーンやオーバーレイなどの合成方式が用いられる。知る限りこれらはGDI+では実装されていない。というわけで自分で実装してみる。
以下のように呼び出してsrcの全体をdstの(x, y)にブレンド描画する関数を書いてみる。簡単のためsrcはdstに収まるものとする。第5引数はブレンドを行う関数へのポインタである。
// Bitmap *dst, *src; BlendImages(dst, src, 100, 100, BlendScreen);
手順は以下の通り。
- 画像のサイズを取得してdst, srcそれぞれの描画領域をRectに記録する。
- BitmapDataオブジェクトを用意する。
- LockBitsメソッドでBitmapをロックする。
- 1行あたりのバイト数Stride*1を求める。
- 各画素のデータが記録された配列へのポインタScan0*2を求める。
- 各画素について値を計算、更新する。
- UnlockBitsメソッドでロックを解除する。
void BlendImages(Bitmap* dstBitmap, Bitmap* srcBitmap, INT x, INT y, byte (*blendFunction)(UINT,UINT,UINT)) { int srcWidth = srcBitmap->GetWidth(); int srcHeight = srcBitmap->GetHeight(); BitmapData dstBD; BitmapData srcBD; Rect dstRect(x, y, srcWidth, srcHeight); Rect srcRect(0, 0, srcWidth, srcHeight); dstBitmap->LockBits(&dstRect, ImageLockModeRead|ImageLockModeWrite, PixelFormat32bppARGB, &dstBD); srcBitmap->LockBits(&srcRect, ImageLockModeRead, PixelFormat32bppARGB, &srcBD); int dstStride = dstBD.Stride; int srcStride = srcBD.Stride; UINT* dstPixel = (UINT*)dstBD.Scan0; UINT* srcPixel = (UINT*)srcBD.Scan0; byte* pDst = (byte*)dstPixel; byte* pSrc = (byte*)srcPixel; byte dstByte; byte srcByte; byte alpha; for(int y=0; y<srcHeight; y++) { for(int x=0; x<srcWidth; x++) { // アルファ値 alpha = pSrc[4*x+3+y*srcStride]; // RGB for(int p=0; p<3; p++) { dstByte = pDst[4*x+p+y*dstStride]; srcByte = pSrc[4*x+p+y*srcStride]; pDst[4*x+p+y*dstStride] = blendFunction(dstByte, srcByte, alpha); } } } dstBitmap->UnlockBits(&dstBD); srcBitmap->UnlockBits(&srcBD); return; }
まずはLockBitsメソッド。
// Status LockBits(const Rect *rect, UINT flags, PixelFormat format, BitmapData *lockedBitmapData);
dstBitmap->LockBits(&dstRect, ImageLockModeRead|ImageLockModeWrite, PixelFormat32bppARGB, &dstBD);
srcBitmap->LockBits(&srcRect, ImageLockModeRead, PixelFormat32bppARGB, &srcBD);
第1引数は描画に使用する領域を記録したRect。
第2引数ではBitmapのロック方法をImageLockMode列挙型で指定する。srcは読むだけなのでImageLockModeReadを指定する。dstは読み書きを行うのでImageLockModeRead|ImageLockModeWriteを渡す。
第3引数ではピクセルのフォーマットをImage Pixel Format定数で指定する。ここでは24bitカラー+8bitアルファのPixelFormat32bppARGBを指定する。
第4引数はBitmapDataオブジェクトである。これは出力で、このメソッドが実行されたときに、このBitmapDataに後で必要になる各種の情報が記録される。
int dstStride = dstBD.Stride;
UINT* dstPixel = (UINT*)dstBD.Scan0;
byte* pDst = (byte*)dstPixel;
BitmapDataオブジェクトから情報を得る。
Strideは描画領域の1行あたりのバイト数である。今、ARGB32bitで展開しているので、この値は描画幅の4倍になる。
Scan0はBitmapのデータが納められた配列へのポインタである。まずUINT(32bit)型のポインタにキャストする。これが各pixelごとのARGB値である。もし色間での演算を行いたい場合はこちらを使えばよい*3。いまは各画素ごとに演算を行いたいのでbyte(8bit)型のポインタに変換する。
for(int y=0; y<srcHeight; y++) for(int x=0; x<srcWidth; x++)
ループは高さ、幅で回す。
dstByte = pDst[4*x+p+y*dstStride]; srcByte = pSrc[4*x+p+y*srcStride]; pDst[4*x+p+y*dstStride] = blendFunction(dstByte, srcByte, alpha);
各画素のデータを取得する。今各pixelあたり32bitで展開しているのでxは4倍する必要がある。pは0≦p≦3で、順にRGBAと並んでいる。先にalphaを取り出して、残りRGBについてブレンドを行う。
// Status UnlockBits(BitmapData *lockedBitmapData);
dstBitmap->UnlockBits(&dstBD);
srcBitmap->UnlockBits(&srcBD);
UnlockBitsメソッドでロックを解除する。
ブレンド関数
次にブレンドを行う関数を書く。今回はアルファスクリーン合成を行った。コメントアウトしているのが通常のスクリーン合成である。
byte BlendScreen(UINT dstByte, UINT srcByte, UINT alpha) { UINT blendedSrcByte = ((255-alpha)*dstByte+alpha*(srcByte+dstByte-srcByte*dstByte/255))/255; // UINT blendedSrcByte = srcByte+dstByte-srcByte*dstByte/255; if(blendedSrcByte>255) { return (byte)255; } else if(blendedSrcByte<0) { return (byte)0; } return (byte)blendedSrcByte; }
こんな感じの関数をいっぱい書く。インターフェイスとして関数ポインタのまま渡すのはアレなので、ブレンド方法を列挙型にして、関数の始めにswitch文でブレンド関数を設定してあげれるのがよい。forループ中に書くのは各画素を参照するたびにswitch文の処理が行われてしまう上にコードも見にくいだろうからやめたほうがいい。
合成方式と計算方法
0≦dst, src≦255 である。超える場合や下回る場合は適宜補正する。加算以降は計算値とdstでアルファ合成すればアルファほげほげブレンドが実現できる。
合成方法 | 英名 | 計算式 |
---|---|---|
アルファ | Alpha | (1-a)*dst+a*src |
加算アルファ | AddAlpha | (1-a)*dst+src |
加算 | Additive | dst+src |
減算 | Substructive | dst+src-255 |
乗算 | Multiplicative | src*dst/255 |
スクリーン | Screen | src+dst-src*dst/255 |
オーバーレイ | Overlay | if(dst<128) 2*src*dst/255 else 255-2*(255-src)*(255-dst)/255 |
ハードライト | Hardlight | if(src<128) 2*src*dst/255 else 255-2*(255-src)*(255-dst)/255 |
ソフトライト | Softlight | if(src<128)(2*src*dst)/255+dst*dst*(1-2*src*src)/255 |
〃 | 〃 | else sqrt(dst)*(2*src*src-1)/255+2*dst*(1-src)/255 |
覆い焼きカラー | ColorDodge | dst/(255-src) |
焼き込みカラー | ColorBurn | 255-(255-dst)/src |
比較(明) | Lighten | if(dst |
比較(暗) | Darken | if(dst |
差の絶対値 | Difference | if(dst |
除外 | Exclusion | dst+src-2*dst*src/255 |
性能
一回の描画に数ms〜数十msを要するので60FPSで動かすようなゲームには向かない。
*1:Offset, in bytes, between consecutive scan lines of the bitmap. If the stride is positive, the bitmap is top-down. If the stride is negative, the bitmap is bottom-up.
*2:Pointer to the first (index 0) scan line of the bitmap.
*3:もちろんforループ内でdstByteRとかの変数に読み込んでそれらを使ってもよい。たぶんUINTに8bitごと入ってるとか演算面倒だし。