EternalWindows
リージョン / ビットマップ型のリージョン

ゲーム等に登場するオブジェクトをビットマップで表現する場合、 オブジェクトの正確な当たり判定を実装するのはなかなか難しいものです。 たとえば、次のような(96, 96)のサイズを持ったビットマップがあったとします。

このビットマップの緑色の部分は背景色であり、 こうした部分は当たり判定の際に考慮されるべきではないといえます。 しかし、(96, 96)という長方形で当たり判定を行ってしまうと、 こうした背景色の部分も考慮されますから、 背景色の部分を除いたビットマップ型のリージョンを作成する必要があります。

ビットマップ型のリージョンを作成するには、背景色でないピクセルの1つずつを1個のリージョンと見なし、 これを結合していくことになります。 リージョンを結合するには、CombineRgnを呼び出します。

int CombineRgn(
  HRGN hrgnDest,
  HRGN hrgnSrc1,
  HRGN hrgnSrc2,
  int fnCombineMode
);

hrgnDestは、結合結果を割り当てるリージョンのハンドルを指定します。 このリージョンは、事前に作成されている必要があります。 hrgnSrc1は、結合元のリージョンのハンドルを指定します。 hrgnSrc2は、結合元のリージョンのハンドルを指定します。 fnCombineModeは、リージョンの結合方法を定義する定数を指定します。

次に示すCreateBitmapRgnは、第1引数のビットマップから第4引数の色を除いたリージョンを作成します。

HRGN CreateBitmapRgn(HBITMAP hbmp, int nWidth, int nHeight, COLORREF crTransparent)
{
	int    x, y;
	HRGN   hrgn;
	HRGN   hrgnTmp;
	BYTE   r, g, b;
	LPBYTE lp;

	b = (BYTE)(GetBValue(crTransparent));
	g = (BYTE)(GetGValue(crTransparent));
	r = (BYTE)(GetRValue(crTransparent));
		
	hrgn = CreateRectRgn(0, 0, 0, 0);
		
	for (y = 0; y < nHeight; y++) {
		lp = GetBits(hbmp, 0, y);
		for (x = 0; x < nWidth; x++) {
			if (lp[0] != b || lp[1] != g || lp[2] != r) {
				hrgnTmp = CreateRectRgn(x, y, x + 1, y + 1);
				CombineRgn(hrgn, hrgn, hrgnTmp, RGN_OR);
				DeleteObject(hrgnTmp);
			}

			lp += 3;
		}
	}

	return hrgn;
}

hrgnが、最終的にビットマップ型のリージョンを表すことになります。 CreateRectRgnで空のリージョンを作成しているのは、 CombineRgnの第1引数に指定するリージョンが事前に作成されている必要があるからです。 ループ内では、GetBitsという自作関数でビットマップのピクセルにアクセスし、 そのピクセルが背景色であるかどうかを調べています。 背景色でない場合は、このピクセルの部分をリージョンの一部にしなければなりませんから、 CreateRectRgnで1ピクセルのリージョンを作成し、これをCombineRgnでhrgnと結合します。 第4引数のRGN_ORは、第2引数と第3引数のリージョンを結合する意味を持ちます。

今回のプログラムは、ビットマップ型のリージョンを作成します。 これを利用した当たり判定の効果を実感するために、今回は当たり判定モードというものを用意し、 各種モード毎の判定の正確さを確認します。

#include <windows.h>

#pragma comment (lib, "msimg32.lib")

enum MODE {
	mode_rgn, 
	mode_boundingbox,
	mode_rect
};
typedef enum MODE MODE;

HRGN CreateBitmapRgn(HBITMAP hbmp, int nWidth, int nHeight, COLORREF crTransparent);
LPBYTE GetBits(HBITMAP hbmp, int x, int y);
LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam);

int WINAPI WinMain(HINSTANCE hinst, HINSTANCE hinstPrev, LPSTR lpszCmdLine, int nCmdShow)
{
	TCHAR      szAppName[] = TEXT("sample");
	HWND       hwnd;
	MSG        msg;
	WNDCLASSEX wc;

	wc.cbSize        = sizeof(WNDCLASSEX);
	wc.style         = 0;
	wc.lpfnWndProc   = WindowProc;
	wc.cbClsExtra    = 0;
	wc.cbWndExtra    = 0;
	wc.hInstance     = hinst;
	wc.hIcon         = (HICON)LoadImage(NULL, IDI_APPLICATION, IMAGE_ICON, 0, 0, LR_SHARED);
	wc.hCursor       = (HCURSOR)LoadImage(NULL, IDC_ARROW, IMAGE_CURSOR, 0, 0, LR_SHARED);
	wc.hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH);
	wc.lpszMenuName  = NULL;
	wc.lpszClassName = szAppName;
	wc.hIconSm       = (HICON)LoadImage(NULL, IDI_APPLICATION, IMAGE_ICON, 0, 0, LR_SHARED);
	
	if (RegisterClassEx(&wc) == 0)
		return 0;

	hwnd = CreateWindowEx(0, szAppName, szAppName, WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, NULL, NULL, hinst, NULL);
	if (hwnd == NULL)
		return 0;

	ShowWindow(hwnd, nCmdShow);
	UpdateWindow(hwnd);
	
	while (GetMessage(&msg, NULL, 0, 0) > 0) {
		TranslateMessage(&msg);
		DispatchMessage(&msg);
	}

	return (int)msg.wParam;
}

LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
	static COLORREF crTransparent = RGB(0, 255, 0);
	static MODE     mode = mode_rgn; // mode_boundingbox mode_rect  
	static RECT     rcBmp = {0};
	static RECT     rcBlock = {0};
	static HRGN     hrgnBmp = NULL;
	static HRGN     hrgnBlock = NULL;
	static HDC      hdcMem = NULL;
	static HBITMAP  hbmpMem = NULL;
	static HBITMAP  hbmpMemPrev = NULL;
	
	switch (uMsg) {

	case WM_CREATE: {
		HDC hdc;
		BITMAP bm;

		hdc = GetDC(hwnd);
		
		hdcMem = CreateCompatibleDC(hdc);
		hbmpMem = (HBITMAP)LoadImage(NULL, TEXT("sample.bmp"), IMAGE_BITMAP, 0, 0, LR_CREATEDIBSECTION | LR_LOADFROMFILE);
		if (hbmpMem == NULL) {
			ReleaseDC(hwnd, hdc);
			return -1;
		}

		hbmpMemPrev = (HBITMAP)SelectObject(hdcMem, hbmpMem);

		ReleaseDC(hwnd, hdc);
		
		GetObject(hbmpMem, sizeof(BITMAP), &bm);
		SetRect(&rcBmp, 0, 0, bm.bmWidth, bm.bmHeight);
		hrgnBmp = CreateBitmapRgn(hbmpMem, bm.bmWidth, bm.bmHeight, crTransparent);
		if (hrgnBmp == NULL)
			return -1;

		SetRect(&rcBlock, 100, 100, 130, 130);
		hrgnBlock = CreateRectRgnIndirect(&rcBlock);
		if (hrgnBlock == NULL)
			return -1;

		return 0;
	}

	case WM_KEYDOWN: {
		int  dx, dy;
		BOOL bRevision = FALSE;

		dx = dy = 0;

		switch (wParam) {
		case VK_LEFT:   dx = -1; break;
		case VK_UP:     dy = -1; break;
		case VK_RIGHT:  dx = +1; break;
		case VK_DOWN:   dy = +1; break;
		default: return 0;
		}

		OffsetRect(&rcBmp, dx, dy);
		OffsetRgn(hrgnBmp, dx, dy);

		if (mode == mode_rgn) {
			HRGN hrgnTmp;

			hrgnTmp = CreateRectRgn(0, 0, 0, 0);

			if (CombineRgn(hrgnTmp, hrgnBmp, hrgnBlock, RGN_AND) != NULLREGION)
				bRevision = TRUE;

			DeleteObject(hrgnTmp);
		}
		else if (mode == mode_boundingbox) {
			RECT rctmp;
			RECT rcBounding;
			
			GetRgnBox(hrgnBmp, &rcBounding);

			if (IntersectRect(&rctmp, &rcBounding, &rcBlock))
				bRevision = TRUE;
		}
		else {
			RECT rctmp;

			if (IntersectRect(&rctmp, &rcBmp, &rcBlock))
				bRevision = TRUE;
		}

		if (bRevision) {
			OffsetRect(&rcBmp, -dx, -dy);
			OffsetRgn(hrgnBmp, -dx, -dy);
		}
		else
			InvalidateRect(hwnd, NULL, TRUE);

		return 0;
	}

	case WM_PAINT: {
		HDC         hdc;
		BITMAP      bm;
		PAINTSTRUCT ps;

		GetObject(hbmpMem, sizeof(BITMAP), &bm);

		hdc = BeginPaint(hwnd, &ps);
		
		FillRgn(hdc, hrgnBlock, (HBRUSH)GetStockObject(BLACK_BRUSH));

		TransparentBlt(hdc, rcBmp.left, rcBmp.top, bm.bmWidth, bm.bmHeight, hdcMem, 0, 0, bm.bmWidth, bm.bmHeight, crTransparent);

		if (mode == mode_boundingbox) {
			RECT rcBounding;
			
			GetRgnBox(hrgnBmp, &rcBounding);
			FrameRect(hdc, &rcBounding, (HBRUSH)GetStockObject(BLACK_BRUSH));
		}
		else if (mode == mode_rect)
			FrameRect(hdc, &rcBmp, (HBRUSH)GetStockObject(BLACK_BRUSH));
		else
			;

		EndPaint(hwnd, &ps);

		return 0;
	}

	case WM_DESTROY:
		if (hdcMem != NULL) {
			if (hbmpMem != NULL) {
				SelectObject(hdcMem, hbmpMemPrev);
				DeleteObject(hbmpMem);
			}
			DeleteDC(hdcMem);
		}
		if (hrgnBmp != NULL)
			DeleteObject(hrgnBmp);
		
		if (hrgnBlock != NULL)
			DeleteObject(hrgnBlock);
		
		PostQuitMessage(0);
		return 0;

	default:
		break;

	}

	return DefWindowProc(hwnd, uMsg, wParam, lParam);
}

HRGN CreateBitmapRgn(HBITMAP hbmp, int nWidth, int nHeight, COLORREF crTransparent)
{
	int    x, y;
	HRGN   hrgn;
	HRGN   hrgnTmp;
	BYTE   r, g, b;
	LPBYTE lp;

	b = (BYTE)(GetBValue(crTransparent));
	g = (BYTE)(GetGValue(crTransparent));
	r = (BYTE)(GetRValue(crTransparent));
		
	hrgn = CreateRectRgn(0, 0, 0, 0);
		
	for (y = 0; y < nHeight; y++) {
		lp = GetBits(hbmp, 0, y);
		for (x = 0; x < nWidth; x++) {
			if (lp[0] != b || lp[1] != g || lp[2] != r) {
				hrgnTmp = CreateRectRgn(x, y, x + 1, y + 1);
				CombineRgn(hrgn, hrgn, hrgnTmp, RGN_OR);
				DeleteObject(hrgnTmp);
			}

			lp += 3;
		}
	}

	return hrgn;
}

LPBYTE GetBits(HBITMAP hbmp, int x, int y)
{
	BITMAP bm;
	LPBYTE lp;
	
	GetObject(hbmp, sizeof(BITMAP), &bm);

	lp = (LPBYTE)bm.bmBits;
	lp += (bm.bmHeight - y - 1) * ((3 * bm.bmWidth + 3) / 4) * 4;
	lp += 3 * x;

	return lp;
}

MODEという列挙型は、当たり判定モードを表しています。 以下の図は、各MODEの当たり判定度合いを示しています。 黒いブロックにどれだけ接近できているかに注目してください。

mode_rgn mode_boundingbox mode_rect

mode_rgnによる当たり判定が最も正確なのは、一目瞭然です。 mode_boundingboxのboundingbox(バウンディングボックス)とは、 リージョンを囲む最小の長方形のことです。 これは、GetRgnBoxという関数で取得することができます。 mode_rectは、ビットマップの本来の幅と高さで判定を行います。 このビットマップのサイズは(96, 96)なので、軽々と背景部分も判定の対象となり、 明らかに正確さに欠けています。 なお、ビットマップを囲う黒い枠は、長方形の範囲を分かりやすくするために 明示的に描画したものです。

ビットマップ型のリージョンやブロックのリージョンは、WM_CREATEで作成されています。

GetObject(hbmpMem, sizeof(BITMAP), &bm);
SetRect(&rcBmp, 0, 0, bm.bmWidth, bm.bmHeight);
hrgnBmp = CreateBitmapRgn(hbmpMem, bm.bmWidth, bm.bmHeight, cr);
if (hrgnBmp == NULL)
	return -1;

SetRect(&rcBlock, 100, 100, 130, 130);
hrgnBlock = CreateRectRgnIndirect(&rcBlock);
if (hrgnBlock == NULL)
	return -1;

rcBmpは、mode_rect時の当たり判定に使われます。 rcBlockは、mode_boundingboxとmode_rect時の当たり判定に使われます。 このブロックというのは、当たり判定用の黒い長方形のことです。 hrgnBmpは、ビットマップ型のリージョンであり、 hrgnBlockはブロックのリージョンです。 mode_rgn時では、リージョン同士の当たり判定にこれらが使用されます。

ビットマップは、WM_KEYDOWNで移動することができ、 各モードによって個別の当たり判定を行われます。 mode_rgn時では、リージョンによる当たり判定が行われます。

if (mode == mode_rgn) {
	HRGN hrgnTmp;

	hrgnTmp = CreateRectRgn(0, 0, 0, 0);

	if (CombineRgn(hrgnTmp, hrgnBmp, hrgnBlock, RGN_AND) != NULLREGION)
		bRevision = TRUE;

	DeleteObject(hrgnTmp);
}

CombineRgnの最後の引数がRGN_ANDを指定します。 この定数は、2つリージョンの重なり合う領域を新しいリージョンとする意味ですが、 重ならなかった場合は、戻り値がNULLREGIONとなります。 よって、戻り値を調べることにより、当たり判定が可能です。 bRevisionがTRUEになったときは、ブッロクと接触したことを意味します。 このときは、移動しなかったことにしなければならないので、 後ほどOffsetRectとOffsetRgnで修正作業が行われることになります。 続いて、mode_boundingboxとmode_rectによる当たり判定を確認します。

else if (mode == mode_boundingbox) {
	RECT rctmp;
	RECT rcBounding;
	
	GetRgnBox(hrgnBmp, &rcBounding);

	if (IntersectRect(&rctmp, &rcBounding, &rcBlock))
		bRevision = TRUE;
}
else {
	RECT rctmp;

	if (IntersectRect(&rctmp, &rcBmp, &rcBlock))
		bRevision = TRUE;
}

これら2つのモードは、長方形による当たり判定を行うので、 IntersectRectが使えます。 GetRgnBoxは、リージョンを囲む最小の長方形を返します。 この関数は、リージョンが空であるかどうかの判定に使われることもあります。

次に、WM_PAINTの処理を確認します。

FillRgn(hdc, hrgnBlock, (HBRUSH)GetStockObject(BLACK_BRUSH));

TransparentBlt(hdc, rcBmp.left, rcBmp.top, bm.bmWidth, bm.bmHeight, hdcMem, 0, 0, bm.bmWidth, bm.bmHeight, cr);

if (mode == mode_boundingbox) {
	RECT rcBounding;
	
	GetRgnBox(hrgnBmp, &rcBounding);
	FrameRect(hdc, &rcBounding, (HBRUSH)GetStockObject(BLACK_BRUSH));
}
else if (mode == mode_rect)
	FrameRect(hdc, &rcBmp, (HBRUSH)GetStockObject(BLACK_BRUSH));
else
	;

まず、FillRgnでブロックを描画し、次にTransparentBltでビットマップを描画します。 BitBltではなくTransparentBltを呼び出しているのは、ビットマップを透過して描画するためです。 mode_boundingboxとmode_rect時では、ビットマップを囲う長方形を表示するため、 FrameRectを呼び出して長方形の枠を描画しています。


戻る