ホームに戻る
DirectDrawメモ
1、はじめに
DirectDraw はハードウェア処理によって2D描画の高速化を図る。
ビデオカードが DirectDraw に対応していなければ、
ソフトウェアエミュレートされるので高速化が見込めなくなる。
DirectDraw の利点
VRAM内でのデータ転送による高速化。
カラーパレットと透過処理、マスク処理。
垂直同期にあわせたダブルバッファ画面の入れ替え。
残念ながらアルファブレンド(半透過)はサポートされないので、
自力で処理を書くか Direct3D を使うことになるでしょう。
ウィンドウモードでは、ダブルバッファを用いた Flip が使えず、
BltFast、SetDisplayMode、GetAttachedSurface が使えません。
バックバッファ相当の作業サーフェスを
プライマリサーフェスに Blt するのが適当でしょう。
ここでは 640×480×8(256色)フルスクリーンでの処理を考えます。
追記:
VRAMとはビデオカード上の記憶領域のことである。
通常、PCはシステムメモリの情報をVRAMに転送し、
VRAMの情報をディスプレイに表示している。
DirectDraw ではVRAMに一枚絵をあらかじめ用意しておき、
CPUからはどの位置に表示するかを指示するだけでディスプレイに表示される。
こうすることの利点は、システムメモリからVRAMへの絵データの転送を省略できる点にある。
加えて、ビデオカード上にはGPUと呼ばれる処理機構が搭載されているので、
PC側のCPUを使用せずにCPU相当の処理ができる。
パレットや透過はGPUによって高速に処理される。
利点をまとめると3点、
1、システムとVRAM間のデータの移動を減らせる。
2、描画に特化したGPUで効率よく描画処理が行える。
3、VRAMやGPUを使うことでシステムメモリやCPUに余裕ができる。
以上の考えによると、動かない数枚の絵を入れ替えて表示することによって、
動くように見せる方法は DirectDraw のやり方にかなっているが、
ロックとアンロックで絵を描き換えながら動かす方法は、
頻繁にやると処理を遅くさせる可能性があるということになる。
半透過に関しては DirectDraw の機能の対象外なので高速な処理は見込めない。
Direct3D は半透過をビデオカードがサポートするので速い。
いまや半透過を使用しないゲームはほとんど無いと言って良いほどなので、
DirectDraw を使用するよりは Direct3D で2D表示する方法をお勧めする。
2、初期化と終了
// DirectDrawオブジェクト作成
LPDIRECTDRAW lpDD = NULL;
if(DirectDrawCreate(NULL, &lpDD, NULL) != DD_OK){
return FALSE;
}
// フルスクリーン設定(ウィンドウモードは DDSCL_NORMAL)
if(lpDD->SetCooperativeLevel(hWnd, DDSCL_FULLSCREEN | DDSCL_EXCLUSIVE) != DD_OK){
return FALSE;
}
// 画面モードの設定
if(lpDD->SetDisplayMode(640, 480, 8) != DD_OK){
return FALSE;
}
終了時には必ず開放の処理が必要になります。
lpDD->Release();
3、ダブルバッファ用サーフェス
メモリ上の描画データ領域を「サーフェス」と呼びます。
ダブルバッファでの更新をフリップ処理するため2つのサーフェスを用意。
Release はバックサーフェスに対しては行わないように注意。
// 複合プライマリサーフェイスの作成
DDSURFACEDESC ddsd;
DDSCAPS ddscaps;
LPDIRECTDRAWSURFACE lpFront = NULL;
LPDIRECTDRAWSURFACE lpBack = NULL;
ZeroMemory(&ddsd, sizeof(ddsd));
ddsd.dwSize = sizeof(ddsd);
ddsd.dwFlags = DDSD_CAPS | DDSD_BACKBUFFERCOUNT;
ddsd.ddsCaps.dwCaps = DDSCAPS_PRIMARYSURFACE | DDSCAPS_FLIP | DDSCAPS_COMPLEX;
ddsd.dwBackBufferCount = 1; // バックバッファは1つ
if(lpDD->CreateSurface(&ddsd, &lpFront, NULL) != DD_OK){
return FALSE;
}
// バックサーフェスの取得
ddscaps.dwCaps = DDSCAPS_BACKBUFFER;
if(lpFront->GetAttachedSurface(&ddscaps, &lpBack) != DD_OK){
return FALSE;
}
尚、ウィンドウモードではバックサーフェスを作成できません。
以下のようにプライマリサーフェスのみを作成します。
DDSURFACEDESC ddsd;
DDSCAPS ddscaps;
LPDIRECTDRAWSURFACE lpFront = NULL;
ZeroMemory(&ddsd, sizeof(ddsd));
ddsd.dwSize = sizeof(ddsd);
ddsd.dwFlags = DDSD_CAPS;
ddsd.ddsCaps.dwCaps = DDSCAPS_PRIMARYSURFACE;
if(lpDD->CreateSurface(&ddsd, &lpFront, NULL) != DD_OK){
return FALSE;
}
また、ウィンドウモードでは画面のクリッピングが必要です。
LPDIRECTDRAWCLIPPER clipper = NULL;
lpDD->CreateClipper(0, &clipper, NULL);
clipper->SetHWnd(0, hWnd);
lpFront->SetClipper(clipper);
4、作業用サーフェス
作業用サーフェスはビットマップなどの情報を格納するサーフェス。
プライマリーサーフェスより大きなものは作成できないとか?
// 作業用サーフェイスの作成
DDSURFACEDESC ddsd;
LPDIRECTDRAWSURFACE lpWork = NULL;
ZeroMemory(&ddsd, sizeof(ddsd));
ddsd.dwSize = sizeof(ddsd);
ddsd.dwFlags = DDSD_CAPS | DDSD_WIDTH | DDSD_HEIGHT;
ddsd.ddsCaps.dwCaps = DDSCAPS_OFFSCREENPLAIN;
ddsd.dwWidth = 640;
ddsd.dwHeight = 480;
if((lpDD->CreateSurface(&ddsd, &lpWork, NULL)) != DD_OK){
return FALSE;
}
以上でVRAMに作業用サーフェスを作成します。
VRAMが不足していれば自動的にシステムメモリに確保されます。
意図的にシステムメモリに確保したい場合は次の行を以下のようにします。
ddsd.ddsCaps.dwCaps = DDSCAPS_OFFSCREENPLAIN | DDSCAPS_SYSTEMMEMORY;
サーフェスがどのメモリを使用しているかは以下のように調べることができます。
DDSCAPS ddscaps;
lpWork->GetCaps(&ddscaps);
if(ddscaps.dwCaps & DDSCAPS_SYSTEMMEMORY){
// システムメモリを使用している
}
5、パレット
256色のとき必要になります。
パレット 0 と 255 は 黒(0, 0, 0)と白(255, 255, 255)で固定。
パレットの設定は初期化時など安定した状態で行うことが望ましい。
終了時に Release が必要。
// パレットの作成 パレット 1 に赤色をセット
PALETTEENTRY peEntry[256];
LPDIRECTDRAWPALETTE lpPalette;
if(lpDD->CreatePalette(DDPCAPS_8BIT, peEntry, &lpPalette, NULL) != DD_OK){
return FALSE;
}
lpFront->SetPalette(lpPalette);
peEntry[1].peRed = 255;
peEntry[1].peGreen = 0;
peEntry[1].peBlue = 0;
peEntry[1].peFlags = 1; // 必ず 1
lpPalette->SetEntries(0, 1, 1, &peEntry[1]); // パレット番号 1 から 1 つ登録
6、カラーキー
透過色のことを「カラーキー」と呼ぶ。
// カラーキーの指定
DDCOLORKEY ddck = {1, 1};
lpWork->SetColorKey(DDCKEY_SRCBLT, &ddck);
パレット 1 からパレット 1 までをカラーキーに指定。
DDCKEY_SRCBLT は指定した転送元の色は転送しない、という意味。
指定した転送先の色にのみ転送する DDCKEY_DESTBLT もある。
参考として文字の背景の透過には以下のような方法がある。
SetBkMode(hdc, TRANSPARENT);
7、画面の塗りつぶし
// パレット 0 の色で全画面を塗りつぶす
DDBLTFX ddbltfx;
ZeroMemory(&ddbltfx, sizeof(DDBLTFX));
ddbltfx.dwSize=sizeof(DDBLTFX);
ddbltfx.dwFillColor = 0;
lpBack->Blt(NULL, NULL, NULL, DDBLT_COLORFILL | DDBLT_WAIT, &ddbltfx);
8、画像の転送
画像のサーフェス間の転送は BltFast を用いる。
作業用サーフェスの rect の範囲をバックサーフェスの(32, 32)に転送。
// 透明色無し
lpBack->BltFast(32, 32, lpWork, &rect, DDBLTFAST_NOCOLORKEY | DDBLTFAST_WAIT);
// 透明色有り
lpBack->BltFast(32, 32, lpWork, &rect, DDBLTFAST_SRCCOLORKEY | DDBLTFAST_WAIT);
作業用サーフェスの rect1 の範囲をバックサーフェスの rect2 の範囲に転送。
BltFast より遅いが拡大縮小処理ができる。
lpBack->Blt(&rect2, lpWork, &rect1, DDBLT_KEYSRC | DDBLT_WAIT);
9、GDI関数による描画
GDI関数による描画が可能である。
デバイスコンテキストを得たらあとはWin32APIのやり方と同等。
速度は期待できないが文字や図形の描画が簡単。
処理時間に余裕がある場合の利用が望ましい。
HDC hdc;
lpBack->GetDC(&hdc);
TextOut(hdc, x, y, str, strlen(str));
lpBack->ReleaseDC(hdc);
文字の背景透過は以下のように。
SetBkMode(hdc, TRANSPARENT);
10、ロック、アンロックによるドット描画
VRAM上のサーフェスにはこの方法では早さは見込めない。
システムメモリ上のサーフェスに対して行うのが適当。
またサーフェスのメモリ位置を座標(x, y)でアクセスする場合には、
メモリ上の位置を x + y * desc.lPitch とする点に注意。
// バックサーフェス上の点(0, 0)にパレット 0 の色を塗る。
unsigned char *p;
DDSURFACEDESC desc;
ZeroMemory(&desc, sizeof(DDSURFACEDESC));
desc.dwSize = sizeof(DDSURFACEDESC);
lpBack->Lock(NULL, &desc, DDLOCK_WAIT | DDLOCK_SURFACEMEMORYPTR, NULL);
p = (unsigned char *)desc.lpSurface;
p[x + y * desc.lPitch] = 0;
lpBack->Unlock(desc.lpSurface);
11、FlipとBlt
Flip はフルスクリーンでしか使用できず、必ず垂直同期を待ちます。
主な動作はプライマリバッファとバックバッファの入れ替えです。
Blt はフルスクリーンでもウィンドウでも使用できます。
こちらは垂直同期を待ちません。
FastBlt はウィンドウでは使用できません。
12、メインループ
メインループを以下のように書いてみました。
画面を1秒間に約60回書きかえるやり方です。
アルゴリズムとしては、
1、マルチメディアタイマーで現時点の時刻を記録
2、キー状態を取得、画像転送処理をし、垂直同期を待って Flip。
3、メッセージがあれば処理が無くなるまでループする。
4、メッセージが無ければ 1000/60 ナノ秒が経ったか調べる。
5、経った場合は1に戻る、経っていない場合は3に戻る。
となる。
「画像転送処理」にどれだけ時間を要するか?が非常に気になるところではある。
//メインループ
DWORD wait_time;
MSG msg;
// ウィンドウの登録と表示
// DirectDraw の初期化
while(1){
wait_time = timeGetTime();
// キー状態取得
// 描画判定処理
// バックサーフェスへの描画
lpFront->Flip(NULL, DDFLIP_WAIT);
do{
while(PeekMessage(&msg, NULL, 0, 0, PM_REMOVE)){
if(msg.message == WM_QUIT){
goto fin;
}
TranslateMessage(&msg);
DispatchMessage(&msg);
}
}while(timeGetTime() < wait_time + 1000 / 60);
}
fin: