32ビットDIBを作る

概要:最も基本的なDIBを作って表示します

DIBもDDB同様にBMPの一種です。
DIBは自由度が高くゲームや画像処理に欠かせません。
DIBを自在に操れるようになる事こそ
この「マルチメディアプログラミング」の最重要事項ですので、
是非ともマスターして下さい!

DIBは Device Independent Bitmap (デバイス非依存ビットマップ)の略です。

■DIBの特徴

・原点は左下
・画素の色が格納されているメモリ領域に直接アクセスできる
・GDI関数は使えない


左上が原点のトップダウンを当たり前と思っていた皆さんには、
左下が原点というのに驚いたかもしれません。
このボトムアップは、x方向は右が正、y方向は上が正で、数学の座標系と同じです。

そしてDIB最大の利点である「画素の色が格納されているメモリ領域に直接アクセスできる」というのは、
まだイメージしづらいかもしれませんが、焦らず以降の解説をお読み下さい。
DDBでは難しかったけどDIBなら簡単にできるという例を挙げるなら、
特定な色の画素だけ違う色に置き換えるという処理が考えられます。
DDBならGDI関数の GetPixel 関数で色を調べて SetPixel 関数で描画しましたが、
DIBなら画素の色が格納されているバッファに直接アクセスして書き換える事ができるのです。
GetPixel 関数と SetPixel 関数は非常に遅いので、
このような複雑な操作が必要な場合はDIBを使います。
また、DIB同士なら特定の色をコピーしないことで簡単に透過処理が実現できます。
ただしその自由度の高さと引き替えにGDI関数を使って描画することはできません。
そしてメモリ参照エラーにも十分気を付ける必要があります。

■画素の色を格納するバッファを確保する

DDBなら「幅640、高さ480ピクセル、32ビット」みたいに注文すればビットマップを作ることができました。
これがDIBだと「1228800バイト下さい(640*480*4)」になります。
ビットマップ作成を依頼するのではなく、普通のメモリ領域を自ら確保します。
計算式の *4 は 32 ビットDIBを作成するからです。

バッファの確保には malloc 関数や calloc 関数、HeapAlloc 関数など何でも構いません。
ただし malloc 関数は 0 に初期化されない事を注意して下さい。
今回は HeapAlloc 関数を使うことにします。

LPVOID HeapAlloc(
  HANDLE hHeap,   // プライベートヒープブロックのハンドル
  DWORD dwFlags,  // ヒープの割り当て方法の制御
  SIZE_T dwBytes   // 割り当てたいバイト数
);


hHeap には GetProcessHeap 関数の戻り値を指定します。

HANDLE GetProcessHeap(VOID);

dwFlags に HEAP_ZERO_MEMORY を指定すると 0 に初期化されます。

#define WIDTH   200
#define HEIGHT  100
static LPDWORD lpPixel;

lpPixel=(LPDWORD)HeapAlloc(GetProcessHeap(),HEAP_ZERO_MEMORY,WIDTH*HEIGHT*4);

このバッファに各画素のRGB成分を表す数値を格納していきます。
以降はこのバッファをピクセル列と呼びます。

初期色は 0 に初期化したので黒です。
もし 0 に初期化しなければ黒じゃない色が初期色になるかもしれません( malloc 関数を使ってみよう)。

■DIBの情報を設定する

現時点ではピクセル列の内容をどのように解釈したらいいのか全くの不明なので、
幅や高さ、一画素のビット数などをプログラムに教えてあげなければなりません。
それには BITMAPINFO 構造体を使います。

typedef struct tagBITMAPINFO {
  BITMAPINFOHEADER bmiHeader;
  RGBQUAD bmiColors[1];
} BITMAPINFO;


BITMAPINFO 構造体は BITMAPINFOHEADER 構造体と
RGBQUAD 型をメンバーに持ちますが、
RGBQUAD 型はカラーテーブルを使う時に設定します。
これらについては8ビットDIB作成の時に解説します。
カラーテーブルを使わない32ビットや24ビットDIBは RGBQUAD 型を無視して問題ありません。
重要なのは BITMAPINFOHEADER 構造体です。

typedef struct tagBITMAPINFOHEADER {
  DWORD biSize;
  LONG biWidth;
  LONG biHeight;
  WORD biPlanes;
  WORD biBitCount;
  DWORD biCompression;
  DWORD biSizeImage;
  LONG biXPelsPerMeter;
  LONG biYPelsPerMeter;
  DWORD biClrUsed;
  DWORD biClrImportant;
} BITMAPINFOHEADER;


biSize はこの構造体のバイト数。
biWidth はDIBの幅。
biHeight はDIBの高さ。
biPlanes は1を指定するように決められています。
biBitCount は一画素のビット数。
 8ビットより大きい時はカラーテーブル(BITMAPINFO構造体のRGBQUAD型メンバー)を無視します。
biCompression は圧縮されているか、いないかです。BI_RGB を指定して下さい。
他のメンバーには 0 を指定しておけば問題ありません。

static BITMAPINFO bmpInfo;

//DIBの情報を設定する
bmpInfo.bmiHeader.biSize=sizeof(BITMAPINFOHEADER);
bmpInfo.bmiHeader.biWidth=WIDTH;
bmpInfo.bmiHeader.biHeight=HEIGHT;
bmpInfo.bmiHeader.biPlanes=1;
bmpInfo.bmiHeader.biBitCount=32;
bmpInfo.bmiHeader.biCompression=BI_RGB;

今回の BITMAPINFO 型変数 bmpInfo は静的変数なので、自動的に 0 に初期化されます。

ところで、誤解しないで欲しいのは、
BITMAPINFO 構造体は必ずしも必要なものではありません。
ただピクセル列の内容をいじって、他のピクセル列にコピーするだけなら
プログラマの脳内解釈が通用するでしょう。
BITMAPINFO 構造体はあくまでも、ピクセル列の内容をプログラムに教えなければいけない時のみ必要なものです。

■32ビットの利点

DIBを作る時は使いやすさを重視して32ビットにする事が殆どです。
ただし色指定に必要なのは24ビットだけです。
残りの8ビットは色に影響せず、自由に使う事ができます。
ファイルに保存する時は24ビットに変換しますが、
3バイトの型というのは存在しないので、1バイト型を3つ使う事になります。
それに対して、1画素を1つの変数で参照できる32ビットが好んで使われるのです。

■描画

描画はピクセル列の数値を書き換える事で行います。
数値で色を表現します。
ピクセル列にはRGB成分の数値が列挙されていると考えて下さい。

それではDIBの神髄を体験するために(25,25)から(50,50)までの範囲を赤くしてみましょう。

int x,y;

//描画
for(y=25;y<=50;y++){
    for(x=25;x<=50;x++){
        lpPixel[x+y*WIDTH]=0x00ff0000;    //赤
    }
}

ピクセル列に直接アクセスして数値を書き換えているのがわかりますね。
座標の計算式がちょっと見慣れないものかもしれませんが、
x+y*WIDTH は (x,y) と同義です。

■色

32ビットの構成は次のようになっています。

0xeeRRGGBB

ee は使われません。逆に言えばどう使っても構わないという事。
RR は赤成分。
GG は緑成分。
BB は青成分。


ところで何気なく16進数を使って表現しましたが、
DIBの色指定にはよく16進数が用いられるので、
10進数の方が好きという方も16進数を使えるようになって下さい。

■表画面へ転送する

DIBも裏画面ですからそのままでは全く見えません。
表示するには表画面へ転送してやる必要があります。

よく使われるのは StretchDIBits 関数です。

int StretchDIBits(
  HDC hdc,                  // デバイスコンテキストのハンドル
  int XDest,                  // コピー先長方形の左上隅の x 座標
  int YDest,                  // コピー先長方形の左上隅の y 座標
  int nDestWidth,               // コピー先長方形の幅
  int nDestHeight,              // コピー先長方形の高さ
  int XSrc,                  // コピー元長方形の左上隅の x 座標
  int YSrc,                  // コピー元長方形の左上隅の x 座標
  int nSrcWidth,               // コピー元長方形の幅
  int nSrcHeight,              // コピー元長方形の高さ
  CONST VOID *lpBits,          // ビットマップのビット
  CONST BITMAPINFO *lpBitsInfo,  // ビットマップのデータ
  UINT iUsage,               // データの種類
  DWORD dwRop              // ラスタオペレーションコード
);


これはDDBの転送に使った StretchBlt 関数のDIB版です。
従って拡大縮小に対応しています。

拡大縮小はコピー先の幅と高さで指定して、
コピー元の幅と高さは参照するコピー元の画像の範囲になります。
ただしコピー元はDIBなので左下を原点とするボトムアップ座標系で考えなければいけません。
まあ普通に表示する以外の機会なんて滅多にありませんから、
(0,0)と(WIDTH,HEIGHT)を指定しておけば気にする事もないでしょう。

lpBits にはピクセル列のポインタ、
lpBitsInfo には BITMAPINFO 構造体のポインタを指定します。
BITMAPINFO 構造体を渡す事でピクセル列の数値の羅列を、プログラムが解釈できるようになります。

iUsage にはカラーテーブルに格納されている数値の意味を指定します。
DIB_PAL_COLORS ならカラーパレットのインデックス、
DIB_RGB_COLORS なら RGB 値そのものである事を意味します。
カラーテーブルを使わない時も DIB_RGB_COLORS を指定します。
カラーテーブルについては8ビットDIB作成の時に解説します。

dwRop には SRCCOPY を指定して下さい。
他の値を指定すれば様々な効果が得られますが利用する機会はあまり無いでしょう。

転送関数には他にも SetDIBitsToDevice 関数があります。
ややこしい仕様なので私は使いませんが。

int SetDIBitsToDevice(
  HDC hdc,               // デバイスコンテキストのハンドル
  int XDest,               // 転送先長方形の左上隅の x 座標
  int YDest,               // 転送先長方形の左上隅の y 座標
  DWORD dwWidth,          // 転送元長方形の幅
  DWORD dwHeight,         // 転送元長方形の高さ
  int XSrc,                // 転送元長方形の左下隅の x 座標
  int YSrc,                // 転送元長方形の左下隅の y 座標
  UINT uStartScan,          // 配列内の最初の走査行
  UINT cScanLines,          // 走査行の数
  CONST VOID *lpvBits,      // DIB ビットからなる配列
  CONST BITMAPINFO *lpbmi,  // ビットマップ情報
  UINT fuColorUse          // RGB 値またはパレットインデックス
);


XSrc と YSrc と uStartScan と cScanLines で転送元の参照範囲を指定するのですが、
かなりややこしいですし、やっぱり普通に表示する以外の機会なんて滅多にありませんから解説しません。
興味があれば自分であれこれいじって研究してみて下さい。

//表画面へ転送
SetDIBitsToDevice(hdc,0,0,WIDTH,HEIGHT,
    0,0,0,HEIGHT,lpPixel,&bmpInfo,DIB_RGB_COLORS);
StretchDIBits(hdc,250,0,WIDTH,HEIGHT,
    0,0,WIDTH,HEIGHT,lpPixel,&bmpInfo,DIB_RGB_COLORS,SRCCOPY);

ご覧の通り原点が左下にあるボトムアップ座標系であることが確認できます。

■バッファの解放

当たり前ですが、確保したバッファは解放しなければなりません。
DIBがどうのこうのという以前の問題です。

今回はバッファの確保に HeapAlloc 関数を使ったので
解放には HeapFree 関数を使います。

BOOL HeapFree(
  HANDLE hHeap,   // ヒープのハンドル
  DWORD dwFlags,  // ヒープ解放オプション
  LPVOID lpMem   // メモリへのポインタ
);


hHeap には GetProcessHeap 関数の戻り値を指定します。
dwFlags には 0 を指定して下さい。

HeapFree(GetProcessHeap(),0,lpPixel);

■DIBのススメ

DIBは概念を理解して、ややこしい座標系に慣れれば大変素晴らしいものです。
ゲームではなかなかエフェクトまで手が回らないかもしれませんが、
画像処理にはDIBが絶対必要です。
例えば、自分の画素を周囲の画素の平均で置き換える平滑化処理などは簡単に実現できます。

★☆ ソースファイル表示 ☆★


戻る / ホーム