1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

C++を用いたWindowsプログラミングの基礎(第4回)

Posted at

はじめに

前回WM_PAINTメッセージを経由して、ビットマップ全体をウィンドウのクライアント領域に描画する方法を解説しました。
しかし、実際では一つの画面を構成するために、ビットマップの一部分を切り取りたい、複数の画像を重ね合わせたい、ということもあるでしょう。
今回はそういったビットマップの描画について解説していきます。

スプライト描画のサンプル

オフスクリーンの作成

2Dゲームを例に出すと、一つの画面を構成する要素として、背景、自キャラ、敵キャラ、敵弾、宝箱、アイコン等々、様々な画像が使用されています。
これら一つ一つをWM_PAINTで描画しようとすると、ウィンドウ更新が都度行われ、画面のちらつきの原因となります。
WM_PAINTは本来ウィンドウの復元に用いられ、頻繁に呼び出すには不向きです。

そこで、可視化されない仮想的なウィンドウ領域(ここではオフスクリーンと呼称)をメモリに用意します。
オフスクリーンに表示したい内容を書き込み、表示準備が完了しだい画面に描画します。
こうすることで、ウィンドウの再描画の回数を抑え、ちらつきを防ぐことができます。

オフスクリーンの作成について、サンプルのコードをまじえて解説します。

CDib.cpp - CDib24::CreateDibObject

BOOL CDib24::CreateDibObject(LONG lWidth, LONG lHeight)
{
    // BITMAPINFO構造体のメモリ割り当て
    if(AllocBMInfoMem() == FALSE)
    {
        return pcommon->DispErrorMsg(L"DIB生成", L"BITMAPINFO構造体のメモリ割り当てに失敗");
    }

    // BITMAPINFO構造体の初期化
    if(lpbmi != NULL)
    {
        lpbmi->bmiHeader.biSize = sizeof(BITMAPINFOHEADER);
        lpbmi->bmiHeader.biWidth = lWidth;
        lpbmi->bmiHeader.biHeight = lHeight;
        lpbmi->bmiHeader.biPlanes = 1;
        lpbmi->bmiHeader.biBitCount = 24;
        lpbmi->bmiHeader.biCompression = BI_RGB;
    }

    // イメージビットのメモリ割り当て
    UINT uImageSize = ((lWidth * 3 + 3) & ~3) * lHeight;
    if(AllocBitsMem(uImageSize) == FALSE)
    {
        return pcommon->DispErrorMsg(L"DIB生成", L"イメージビットのメモリ割り当てに失敗");
    }

    return TRUE;
}

処理の流れは、前回で取り扱ったBMPファイルの読み込みとほぼ同じです。
ファイル読み込みの場合は、BITMAPFILEHEADERの読み込み、BITMAPINFOの読み込み、イメージビットの読み込みの順で行われました。
オフスクリーン作成の場合は、BMPファイルを入力情報としないため、BITMAPFILEHEADERの処理が不要な点のみ異なります。
まず、BITMAPINFOのメモリを確保し、必要なBITMAPINFOHEADER構造体の情報を自前でセットします。
次にイメージビットのメモリを割り当てます。

この処理で重要な箇所はイメージビットのメモリサイズを算出する部分です。

UINT uImageSize = ((lWidth * 3 + 3) & ~3) * lHeight;

スキャンライン幅は4バイト単位

イメージに必要なメモリのサイズは、単純に幅×高さで算出できると考えるかもしれません。

UINT uImageSize = lWidth * lHeight;

サンプルで取り扱うビットマップは24bitカラーです。そのため、1ピクセルあたり3バイト必要です。

UINT uImageSize = lWidth * lHeight * 3;

しかし、上のコードでも不充分です。これはDIBの仕様に理由があります。
DIBイメージの1行ごとの幅は4バイト単位で取り扱われます。
そのため、1行のバイト数が4で割り切れない場合、不足分をパディングする必要があります。
4バイト境界を考慮したサイズの算出が以下になります。

UINT uImageSize = ((lWidth * 3 + 3) & ~3) * lHeight;

3を加算することで不足分をパディングし、~3(0xFFFFFFFC)の論理積で下位2ビットを切り捨て、4バイト境界にそろえています。

スプライトの描画

スプライトとは、画面表示に必要な部品となる画像データを指します。

スプライトの描画は、スプライト用DIBのイメージビットをオフスクリーン用DIBのイメージビットへコピーすることで行っています。
描画する位置とサイズはRECT構造体で指定しています。
下記サンプルの引数RECT rcSrcには、スプライトとして使用するイメージ全体から描画したい部分だけを座標として指定しています。
RECT rcDstではオフスクリーンのどの座標にコピーするかを指定しています。
RECT型は矩形を指定する構造体です。しかし、実際に描画したいイメージ(例えば、自キャラや敵キャラ等)が矩形であることは稀でしょう。
そこで、イメージのビットコピーを行わないカラー値を事前に設定します。

CDib.cpp - CDib24::CopyDibBits

// スプライトのコピー
void CDib24::CopyDibBits(const CDib24& dibSrc, RECT rcSrc, RECT rcDst, BOOL bTransparent)
{
    // コピー元とコピー先のサイズを比較して、小さい方をコピーサイズとする
    int nCopyWidth, nCopyHeight;

    if((rcDst.right - rcDst.left) < (rcSrc.right - rcSrc.left))
    {
        nCopyWidth = (rcDst.right - rcDst.left) * 3;
    }
    else
    {
        nCopyWidth = (rcSrc.right - rcSrc.left) * 3;
    }

    if((rcDst.bottom - rcDst.top) < (rcSrc.bottom - rcSrc.top))
    {
        nCopyHeight = rcDst.bottom - rcDst.top;
    }
    else
    {
        nCopyHeight = rcSrc.bottom - rcSrc.top;
    }

    // 透明色をRGBに分解
    BYTE byRed, byGreen, byBlue;
    byRed   = (BYTE)( dwTransparentColor & 0x000000FF);
    byGreen = (BYTE)((dwTransparentColor & 0x0000FF00) >> 8);
    byBlue  = (BYTE)((dwTransparentColor & 0x00FF0000) >> 16);

    // イメージビットのコピー
    int i, j;
    int nSrcLine, nDstLine;
    int nSrcHeight, nDstHeight;
    int nSrcIndex, nDstIndex;
    LPBYTE lpSrcBits, lpDstBits;

    nSrcLine = (dibSrc.lpbmi->bmiHeader.biWidth * 3 + 3) & ~3;
    nDstLine = (GetCDibWidth() * 3 + 3) & ~3;

    nSrcHeight = dibSrc.lpbmi->bmiHeader.biHeight - rcSrc.top - 1;
    nDstHeight = GetCDibHeight() - rcDst.top - 1;

    lpSrcBits = dibSrc.lpBits;
    lpDstBits = lpBits;

    for(i = 0; i < nCopyHeight; i++)
    {
        for(j = 0; j < nCopyWidth; j += 3)
        {
            nSrcIndex = (rcSrc.left * 3) + j + (nSrcHeight - i) * nSrcLine;
            nDstIndex = (rcDst.left * 3) + j + (nDstHeight - i) * nDstLine;

            // 透明色を使用しない、または、透明色ではない場合はビットコピー
            if((bTransparent == FALSE) 
            || (byRed != lpSrcBits[nSrcIndex + 2]) || (byGreen != lpSrcBits[nSrcIndex + 1]) || (byBlue != lpSrcBits[nSrcIndex]))
            {
                lpDstBits[nDstIndex]     = lpSrcBits[nSrcIndex];
                lpDstBits[nDstIndex + 1] = lpSrcBits[nSrcIndex + 1];
                lpDstBits[nDstIndex + 2] = lpSrcBits[nSrcIndex + 2];
            }
        }
    }
}

まず、コピー元とコピー先の矩形サイズを合わせ、等倍サイズでコピーが行われるようにします。
次に、メンバ変数dwTransparentColorを赤青緑に分解しています。この透明色として扱うカラー値は事前に設定しておきます。

CExample.cpp - CMyExample::DrawOffScreen

    // 透明色を設定
    pDibSprite->SetTransparentColor(0, 0);
    pDibOffScreen->SetTransparentColor(pDibSprite->GetTransparentColor());

CDib.cpp - CDib24::SetTransparentColor

// 指定座標のカラー値を透明色としてセット
void CDib24::SetTransparentColor(int nCoordX, int nCoordY)
{
    BYTE byRed, byGreen, byBlue;
    int nByteX = nCoordX * 3;
    int nByteY = GetCDibHeight() - nCoordY - 1;
    int nScanLine = (GetCDibWidth() * 3 + 3) & ~3;

    byRed   = lpBits[nByteX + 2 + nScanLine * nByteY];
    byGreen = lpBits[nByteX + 1 + nScanLine * nByteY];
    byBlue  = lpBits[nByteX     + nScanLine * nByteY];

    dwTransparentColor = byRed | (byGreen << 8) | (byBlue << 16);
}

サンプルでは、スプライト用DIBイメージの座標(0,0)にあるカラー値を透明色として扱うように設定しています。

    int nByteX = nCoordX * 3;
    int nByteY = GetCDibHeight() - nCoordY - 1;
    int nScanLine = (GetCDibWidth() * 3 + 3) & ~3;

上のコードの部分は、指定座標からイメージビット配列のインデックスを算出しています。
X座標を3倍しているのは、1ピクセルが3バイトだからです。
また、イメージの1行あたりのバイト数を4で割り切れるよう補正するのも前述のとおりです。
Y座標を使用した算出部分は、イメージの高さから指定Y座標で引き算しています。
サンプルのイメージの高さは64で、指定Y座標は0、座標は0オリジンなので最後に-1をしてnByteYの値は63がセットされます。
人間の感覚では画像の左上隅こそがイメージの起点であり、座標(0,63)では左下を指すように考えてしまうでしょう。
しかし、実際にはイメージの起点は左下()です。そのため、上下を反転させて算出します。

※厳密には、イメージの起点はBITMAPINFOHEADER構造体のbiHeightの値が正か負かで決まります。
正の値なら左下が起点、負の値なら左上が起点となります。
サンプルで使用しているBMPファイルは正の値のため、左下起点を前提としていますが、本来なら正負を考慮して作るべきでしょう。

オフスクリーンへのビットコピー処理も透明色のセット処理と同様に、左下を起点としてイメージビットのインデックスを算出しています。

    nSrcLine = (dibSrc.lpbmi->bmiHeader.biWidth * 3 + 3) & ~3;
    nDstLine = (GetCDibWidth() * 3 + 3) & ~3;

    nSrcHeight = dibSrc.lpbmi->bmiHeader.biHeight - rcSrc.top - 1;
    nDstHeight = GetCDibHeight() - rcDst.top - 1;

算出したインデックス値と透明色のカラー値を用いて、RGB値をオフスクリーンへコピーします。

            // 透明色を使用しない、または、透明色ではない場合はビットコピー
            if((bTransparent == FALSE) 
            || (byRed != lpSrcBits[nSrcIndex + 2]) || (byGreen != lpSrcBits[nSrcIndex + 1]) || (byBlue != lpSrcBits[nSrcIndex]))
            {
                lpDstBits[nDstIndex]     = lpSrcBits[nSrcIndex];
                lpDstBits[nDstIndex + 1] = lpSrcBits[nSrcIndex + 1];
                lpDstBits[nDstIndex + 2] = lpSrcBits[nSrcIndex + 2];
            }

サンプルコードを実行すると、チェック柄の背景の上に、透明色を使用したスプライト画像が画面中央に表示されます。

次回予告

今回の解説で、DIBイメージを用いたスプライト描画ができるようになりました。
しかし、静止画像だけでは物足りません。
サンプルで用意した画像から察しがついている方もいらっしゃるでしょうが、画像をアニメーションさせたくなります。
また、ユーザーの操作で画像を動かしたくもなります。
次回はそのあたりを解説していきます。

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?