ゲームなど、画像を動かすプログラムに欠かせないのが、絵の指定色部分を透かして背景と合成する「スプライト」の処理です。
DirectX を利用せずに GDI 系の API を駆使して自前で処理する場合は、マスク画像とのビット演算で実現します。BitBlt 関数のラスタオペレーションによるソフトウェアスプライトはゲーム制作の基礎技術であり、既にご存じの方も多いでしょう。
しかし、基本的なやり方では、マスク画像をリソースとして用意しなければなりませんし、その際の画像の透過色部分は黒か白でなければなりません。
わがまま | → | こうしたい |
---|---|---|
マスク画像を作るのは面倒 | → | 実行時に自動的に作成 |
透明色は黒か白? | → | どんな色でもよい |
BitBlt には、「転送元と転送先の色数が異なる場合、自動的に色を変換する」という機能があるので、それを利用します。
このようなキャラクター画像を描画するとき、背景(緑色の部分)は不要です。
他の画像と重ねたときにこの部分を透過(無視)するような画像表示のことを、「スプライト転送」と呼びます。テレビなどで利用されるクロマキーが身近な例でしょうか。
+ | → |
もっと簡単に言うと、「アニメーションのセル画のようにしたい」わけです。
セル画のセロファンの部分は透明なので下の背景が透けて見えるのですが、コンピューターの画像には“透明な色”というものがありません。
そのため、“透明色”という色を特別に設定します。
「黒(RGB[0,0,0])」や「青(RGB[0,0,255])」など、任意のひとつの色を透明色として代用します。
API 関数 BitBlt を使う場合は、“0”と“1”のビット演算(二進数の論理演算)を用いてこれを実現します。そのためには、透過したい部分を黒(“0”)か白(“1”)にし、白黒のマスク画像を別途用意します。
+ | + | → |
ビット演算と言っても、実際には BitBlt 関数が実行してくれるので、引数に特定の定数を与えてやることで簡単に実現できます。(ラスタオペレーション)
'BitBlt
Private Declare Function BitBlt Lib "gdi32"
(ByVal DestDC As Long, ByVal DestX As Long, ByVal DestY As Long,
ByVal Width As Long, ByVal Height As Long,
ByVal SrcDC As Long, ByVal SrcX As Long, ByVal SrcY As Long,
ByVal Rop As Long) As Long
Call BitBlt(picDisplay.hDC, 0&,0&, picDisplay.Width, picDisplay.Height, picMask.hDC, 0&, 0&, SRCAND)
Call BitBlt(picDisplay.hDC, 0&, 0&, picDisplay.Width, picDisplay.Height, picSource.hDC, 0&, 0&, SRCPAINT)
Call picDisplay.Refresh 'AutoRefresh = True のときに必要です。
AND | → | OR | → |
定数 | 用途 |
---|---|
SRCCOPY | 転送元画像をそのまま転送する。 |
SRCAND | AND 演算により、転送元の白い部分を透過させる。 |
SRCPAINT | OR 演算により、転送元の黒い部分を透過させる。 |
NOTSRCCOPY | 転送元の色データが全反転する。 |
“透明色を任意の色から選べる”と言っても、ビット演算の際には黒か白でなければなりません。そのため、転送元画像の中から透明色の部分を抜き出し、黒(または白)で塗り替えるという加工作業が必要です。
→ |
従って、マスク画像作成のの処理手順は次のようになります。
実は、この処理が山場であり、最も悩むところです。
“連続する色領域を塗り替える”という API 関数は存在しますが、“ビットマップ全体から指定の色領域を探し、その部分を一度に塗り替える”という都合のいい関数は見あたりません。かと言って、1 ピクセルずつ調べていたのでは、かなり遅くなってしまいます。
そこで。発想を切り替えて、もう一度考え直してみます。
元画像 | ||||||
---|---|---|---|---|---|---|
目的の画像 | = | AND | (キャラクター部分とその下地部分に分解してみると ... 元画像に下地部分を AND 合成した結果である) |
|||
マスク画像 | ||||||
下地部分 | = | NOT | (下地部分は、マスク画像の反転である) |
つまり、マスク画像を作ることができれば、それを反転したものを元画像に AND 合成することで、たとえ透明色がどんな色でも、「透明色部分を黒で塗り替えた画像」を作成できるのです。
マスク画像は、モノクロビットマップです。
モノクロビットマップは、CreateBitmap 関数で作成します。
BitBlt には、転送先と転送元の色数が異なる場合、自動的に色を変換する機能があります。その機能と“背景色の設定”を利用して、目的のモノクロ画像を取り出すことができます。
→ |
'ソース画像の作成
Dim hdcSrc As Long
Dim hbmSrc As Long
Dim hbmSrcDefault As Long
hdcSrc = CreateCompatibleDC(picDisplay.hDC)
hbmSrc = LoadImage(0&, SPRITE_IMGNAME, IMAGE_BITMAP, 0&, 0&, LR_LOADFROMFILE)
hbmSrcDefault = SelectObject(hdcSrc, hbmSrc)
ビットマップには前景色と背景色があり、モノクロビットマップの“黒”は、背景色の色とされています。この仕組みによってマスク画像が描かれます。背景色の設定は SetBkColor 関数で行います。
Dim lngSrcDefaultBkColor As Long
lngSrcDefaultBkColor = SetBkColor(hdcSrc, SPRITE_TRANSPARENTCOLOR)
引数に渡している定数 SPRITE_TRANSPARENTCOLOR は独自の定数で、ここには任意の“透過したい色”を指定します。背景色の設定は後で元に戻すので、返されたディフォルト値を保持しておきます。
'背景が白のマスクを作成
Dim hdcWhiteMask As Long
Dim hbmWhiteMask As Long
Dim hbmWhiteMaskDefault As Long
hdcWhiteMask = CreateCompatibleDC(hdcSrc)
hbmWhiteMask = CreateBitmap(bmSrc.bmWidth, bmSrc.bmHeight, BM_COLORPLANE, BM_MONOCHROME, ByVal 0&)
hbmDefaultWhiteMask = SelectObject(hdcWhiteMask, hbmWhiteMask)
Call BitBlt(hdcWhiteMask, 0&, 0&, bmSrc.bmWidth, bmSrc.bmHeight, hdcSrc, 0&, 0&, SRCCOPY)
CreateBitmap 関数の最後の引数に渡すデータは必要ないので、上のように指定しておきます。他の 2 つの定数は、下のように定義しています。
Private Const BM_COLORPLANE = 1&
Private Const BM_MONOCHROME = 1&
さて、上記の手順で作られた画像は、最終的に転送先と合成するマスクであり、背景部分が白のマスクです。背景部分が黒のマスクを作成し、元画像を加工しなければなりません。
'背景が黒のマスク画像
Dim hdcBlackMask As Long
Dim hbmBlackMask As Long
Dim hbmBlackMaskDefault As Long
hdcBlackMask = CreateCompatibleDC(hdcSrc)
hbmBlackMask = CreateCompatibleBitmap(bmSrc.bmWidth, bmSrc.bmHeight, BM_COLORPLANE, BM_MONOCHROME, ByVal 0&)
hbmDefaultBlackMask = SelectObject(hdcBlackMask, hbmBlackMask)
Call BitBlt(hdcBlackMask, 0&, 0&, bmSrc.bmWidth, bmSrc.bmHeight, hdcWhiteMask, 0&, 0&, NOTSRCCOPY)
キャンバスの作り方は WhiteMask と同じですが、この画像はソース画像に転送するので、ソース画像と同じ色で作ります。WhiteMask を色全反転させたものなので、最後の BitBlt でラスタオペレーションの NOTSRCCOPY を利用します。
Call SetBkColor(hdcSrc, lngSrcDefaultBkColor)
Call BitBlt(hdcSrc, 0&, 0&, bmSrc.bmWidth, bmSrc.bmHeight, hdcBlackMask, 0&, 0&, SRCAND)
元画像の背景色を元に戻し、この黒マスクを AND 合成して透明色部分を塗りつぶします。
以上の手順でマスク画像を作ったら、透明色を黒とした場合と同じように合成して透過転送を行います。
ただし、hdcWhiteMask はモノクロビットマップなので、正常に透過転送するためには、転送先の色と同じキャンバスに変換しておく必要があります。
'転送元画像と同じ色数で白マスクを複製
hdcMask = CreateCompatibleDC(hdcSrc)
hbmMask = CreateCompatibleBitmap(hdcSrc, bmSrc.bmWidth, bmSrc.bmHeight)
hbmMaskDefault = SelectObject(hdcMask, hbmMask)
Call BitBlt(hdcMask, 0&, 0&, bmSrc.bmWidth, bmSrc.bmHeight, hdcWhiteMask, 0&, 0&, SRCCOPY)
'透過転送
Call BitBlt(picDisplay.hDC, X, Y, bmSrc.bmWidth, bmSrc.bmHeight, hdcMask, 0&, 0&, SRCAND)
Call BitBlt(picDisplay.hDC, X, Y, bmSrc.bmWidth, bmSrc.bmHeight, hdcSrc, 0&, 0&, SRCPAINT)
今回も手順が複雑でした。
MSDN Library で各 API の説明も読んでおきましょう。
作成した DC やビットマップの削除も忘れないでください。
マスク作成方法は、MSDN Library 付属のスクリーンセイバーサンプルを見て考えました。
“IE のロゴ(スプライト)がクルクルと飛び回る”というものですが、リソースにマスク画像がありませんでした。「なんで?」と思い、ソースコードを見てみましたが、そこはもう「さすが MS!」の一言。
コードは非常に汚く、読む気も起こらない(笑)。
結局、BitBlt の“背景色の設定を利用した、カラー → モノクロ変換のマジック”を見つけるまでが大変でした。