画像の透過転送とマスクの自動生成

 ゲームなど、画像を動かすプログラムに欠かせないのが、絵の指定色部分を透かして背景と合成する「スプライト」の処理です。

DirectX を利用せずに GDI 系の API を駆使して自前で処理する場合は、マスク画像とのビット演算で実現します。BitBlt 関数のラスタオペレーションによるソフトウェアスプライトはゲーム制作の基礎技術であり、既にご存じの方も多いでしょう。

しかし、基本的なやり方では、マスク画像をリソースとして用意しなければなりませんし、その際の画像の透過色部分は黒か白でなければなりません。

わがままこうしたい
マスク画像を作るのは面倒実行時に自動的に作成
透明色は黒か白?どんな色でもよい

BitBlt には、「転送元と転送先の色数が異なる場合、自動的に色を変換する」という機能があるので、それを利用します。

目次

スプライトの基礎

ビットマップ画像

 このようなキャラクター画像を描画するとき、背景(緑色の部分)は不要です。
他の画像と重ねたときにこの部分を透過(無視)するような画像表示のことを、「スプライト転送」と呼びます。テレビなどで利用されるクロマキーが身近な例でしょうか。

スプライト転送
スプライト + 背景 スプライト合成

もっと簡単に言うと、「アニメーションのセル画のようにしたい」わけです。
セル画のセロファンの部分は透明なので下の背景が透けて見えるのですが、コンピューターの画像には“透明な色”というものがありません。

そのため、“透明色”という色を特別に設定します。
「黒(RGB[0,0,0])」や「青(RGB[0,0,255])」など、任意のひとつの色を透明色として代用します。

API 関数 BitBlt を使う場合は、“0”と“1”のビット演算(二進数の論理演算)を用いてこれを実現します。そのためには、透過したい部分を黒(“0”)か白(“1”)にし、白黒のマスク画像を別途用意します。

白黒スプライト
透明色が黒のビットマップ画像 + 背景 + マスク画像 スプライト合成

BitBlt とビット演算

 ビット演算と言っても、実際には 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 マスク画像 マスク AND 背景 OR イメージ 転送結果
主要ラスタオペレーション定数
定数用途
SRCCOPY転送元画像をそのまま転送する。
SRCANDAND 演算により、転送元の白い部分を透過させる。
SRCPAINTOR 演算により、転送元の黒い部分を透過させる。
NOTSRCCOPY転送元の色データが全反転する。

マスク画像作成のアルゴリズム

 “透明色を任意の色から選べる”と言っても、ビット演算の際には黒か白でなければなりません。そのため、転送元画像の中から透明色の部分を抜き出し、黒(または白)で塗り替えるという加工作業が必要です。

色変換
下地が緑色 下地が黒色

従って、マスク画像作成のの処理手順は次のようになります。

  1. 転送元画像の透明色部分を黒で塗り替える
  2. そのマスクを作る(透明色部分は白、キャラクター部分は黒)

転送元画像の透明色部分を黒で塗り替えるには?

 実は、この処理が山場であり、最も悩むところです。
“連続する色領域を塗り替える”という API 関数は存在しますが、“ビットマップ全体から指定の色領域を探し、その部分を一度に塗り替える”という都合のいい関数は見あたりません。かと言って、1 ピクセルずつ調べていたのでは、かなり遅くなってしまいます。

そこで。発想を切り替えて、もう一度考え直してみます。

黒で塗り替えるには...
元画像元画像
目的の画像 目的の画像 = キャラクター部分 AND 下地部分 (キャラクター部分とその下地部分に分解してみると ...
元画像に下地部分を AND 合成した結果である)
マスク画像マスク画像
下地部分 下地部分 = 下地部分 NOT マスク画像 (下地部分は、マスク画像の反転である)

つまり、マスク画像を作ることができれば、それを反転したものを元画像に AND 合成することで、たとえ透明色がどんな色でも、「透明色部分を黒で塗り替えた画像」を作成できるのです。

マスク画像の作成

 マスク画像は、モノクロビットマップです。
モノクロビットマップは、CreateBitmap 関数で作成します。

BitBlt には、転送先と転送元の色数が異なる場合、自動的に色を変換する機能があります。その機能と“背景色の設定”を利用して、目的のモノクロ画像を取り出すことができます。

  1. 転送元画像の背景色を、指定された透明色に設定
  2. CreateBitmap 関数でマスク画像の“モノクロキャンバス”を作成
  3. 転送元画像をモノクロキャンバスにコピー転送
  4. 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 の“背景色の設定を利用した、カラー → モノクロ変換のマジック”を見つけるまでが大変でした。

2000年 4月29日(土) 公開
2001年12月 7日(金) 更新