VC++でGDI+ そにょ7 〜画像の描画3:ブレンド〜

ブレンド〜ビットマップの操作〜

今までの画像の描画は透明度だけを考慮したアルファブレンドである。しかしながらゲームでは派手なエフェクトのためにスクリーンやオーバーレイなどの合成方式が用いられる。知る限りこれらはGDI+では実装されていない。というわけで自分で実装してみる。
以下のように呼び出してsrcの全体をdstの(x, y)にブレンド描画する関数を書いてみる。簡単のためsrcはdstに収まるものとする。第5引数はブレンドを行う関数へのポインタである。

// Bitmap *dst, *src;
BlendImages(dst, src, 100, 100, BlendScreen);

手順は以下の通り。

  1. 画像のサイズを取得してdst, srcそれぞれの描画領域をRectに記録する。
  2. BitmapDataオブジェクトを用意する。
  3. LockBitsメソッドでBitmapをロックする。
  4. 1行あたりのバイト数Stride*1を求める。
  5. 各画素のデータが記録された配列へのポインタScan0*2を求める。
  6. 各画素について値を計算、更新する。
  7. 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ごと入ってるとか演算面倒だし。