DIRECTX1 DirectDraw 1 初期化、描画

DirectXの中核をなす、DirectDrawについて説明します。

DirectDrawは、グラフィックカードの能力を引き出すためにMSが作ったものです。プログラマはハードウェアの違いをあまり意識しないでプログラムを組むことができます。といってもやはり違いは存在していて意識しないわけにはいかないのですが。
後々ウィンドウズで3Dを扱う場合にもDirectDrawは必須となるので、やっておきましょう。

ここでは基本的な部分のテストを行います。実際に使うにはいろいろ不足しているの部分があるのでそれは後から修正知ていくことにします。

1 準備

  必要なものはDirectXのSDKです。これはMSのホームページからダウンロードすることもできるし、雑誌や本に付録でついていることもあります。適当なソースから入手してください。通常のアプリケーションをインストールするのと同じようにDirectX SDKをインストールします。

バージョンの問題がありますが、現在の最新バージョンは7です。しかし、5以上であれば大丈夫です。
インストールが無事に完了したら、新規プロジェクトを作成します。名前はDddにしましょう。DDRAWや、DirextXなどにすると、あとあと面倒なことになります。予約語は避けなければいけません。

「プロジェクト」 >> 「設定」 >> 「リンク」 の 「オブジェクト/ライブラリモジュール」に 「ddraw.lib」「dxguid.lib」の二つをスペースで区切って書きます。準備は以上で完了です。

プロジェクトはSDIにしてViewクラスをメインにします。

まずはフルスクリーンにするためにメニューとシステムメニューをはずします。
ステップ4/6で、全てのチェックをはずします。さらに詳細設定で全てのチェックをはずします。実にシンプルなウィンドウが出来ますね。

次にキー入力のハンドルを作っておきます。これはデバッグを起動すると終了ができなくなってしまうことがあるので、その対策です。エスケープキーでぬけられるようにしておきます。具体的にはOnKeyDownのハンドルを作ってその中に以下のコードを書き込んでおきます。

 

 if(nChar==VK_ESCAPE){
  GetParent()->PostMessage(WM_CLOSE,0,0);
  return;
 }
 

ここまでで一度ビルドしてみてエラーの無いことを確かめましょう。

2 クラスの作成

  いままでは動作をテストしながらうまく行ってからクラス化していましたが、今回からいきなりクラスとして作成してみようかと思います。DirectDrawのクラスは、CDxdrawにしましょう。間違ってもDDrawにしてはいけません。ライブラリファイルがDDRAWと同じ名前になるので必要なライブラリが読み込まれなくなり、100個以上のエラーが出てきます。CDxDrawが出来たらクラス定義の中に以下のメンバを追加します。
 

 //----- DirectDraw オブジェクトは一つだけ存在する
 LPDIRECTDRAW lpDD; // DirectDraw オブジェクト
 int InitDxDraw(HWND hWnd);
 

いずれもPublicなメンバとします。

LPDIRECTDRAW lpDD; は、DirectDrawのオブジェクトです。アスタリスクがついていませんが、これはポインタです。DirectDrawはそうやって動かすととりあえずは理解しておいてください。詳しい話はあとで出来たらやりましょう。で、下の

int InitDxDraw(HWND hWnd); は、このDirectDrawの初期化をして使えるようにするための関数です。このメンバ関数の中は以下の通り。
 

// 書き漏らしや間違いがあると、エラーになってしまうので注意

 int CDxDraw::InitDxDraw(HWND hWnd)
 {
 HRESULT ret;
 
 ret=DirectDrawCreate(NULL, &lpDD, NULL);
 if (ret != DD_OK)
 {
  AfxMessageBox("Create error");
  return -1;
 }
 
 ret = lpDD->SetCooperativeLevel(hWnd,
 DDSCL_ALLOWREBOOT |
 DDSCL_EXCLUSIVE |-------------------------------(2)
 DDSCL_FULLSCREEN |
 DDSCL_ALLOWMODEX);
 
 if (ret != DD_OK)
 {
  AfxMessageBox("SetCooperativLevel errorあはは");
  return -1;
 }
 
 // lpDD->EnumDisplayModes(DDEDM_REFRESHRATES ,NULL,---------(3)
 // lpDD, EnumDisplayModesCallback);
 
 //解像度が使用できない場合は「空のドキュメントの作成に失敗」エラーが出る
 ret = lpDD->SetDisplayMode(640, 480, 32;--------------------(4)
 if (ret != DD_OK)
 {
  AfxMessageBox("DisplayMode error");
  return -1;
 }
 
  return DD_OK;
 }
 

クラスのヘッダファイルの上に、#include "ddraw.h" を追加してビルドします。

ついでにCMainFrameのPreCreateWindowに以下のコードを追加しておいてください。

 cs.dwExStyle=WS_EX_TOPMOST;

これが無いと終了したときにデスクトップが壊れたままになってしまいます。→現時点ではこれを書いてもデスクトップがなおりません

ビルドがうまく行ったらViewクラスのCreateから呼び出してみます。
ViewクラスのWM_OnCreateハンドルの作り方は覚えていますね。
CDddViewのヘッダにはDxDrawクラスのオブジェクトを作ります。

CDxDrasw ddx; としておきましょう。

おっと、Include ファイルも忘れずに。DddView.h DddView.cpp 両方の上の方に ->ヘッダーだけで大丈夫

#include "dxdraw.h" が必要です。

WM_OnCreateの中の呼び出し部分は、以下の通り。ハンドルを作って渡します。
 

 HWND m_hWnd=NULL;
 ret = ddx.InitDxDraw( GetParent()->m_hWnd);
 if (ret != DD_OK){
  AfxMessageBox("InitDxDraw in Oncreate error");
  return -1;
 }
 

さあ、ビルドして実行します。

指定の解像度が使用できない場合は「空のドキュメントの作成に失敗」エラーが出ます。その場合は(4)の部分を
ret = lpDD->SetDisplayMode(640, 480, 24);

と変えてください。32を24にするわけですね。どうでしょうか。
これは色数の設定ですね。
つい数年前まではここを「8」にするのが普通でしたが、今では余計なことを考えずに24または32で問題ありません。余計なことを考えるすきが無いほど、グラフィックボードが進化してしまいました。今後256色パレットに戻ることはないでしょうから、ここから先は1600万色か、フルカラーで話を進めましょう。

エラーの場合はダイアログが出ます。出ない場合は(2)の行をコメントアウトしてみると、出ます。わざわざ出すこともありませんが。

解像度はグラフィックボードによって変わるので、どの解像度が使えるかあらかじめ調べなければなりません。

そのために関数が用意されていてそれが、(3)のEnumDisplayModes 関数です。
これを使うためにはコールバック関数を指定する必要があります。この関数が調べた結果を受け取るための関数で、EnumDisplayModes は、解像度が見つかるたびにその関数の中からコールバック関数を呼び出します。

コールバック関数は以下のようにします。
 

 int dkainum = 0;
 struct dkai{
 int w;
 int h;
 int bits;
 };
 
 dkai kaiz[40];
 
 HRESULT
 CALLBACK CDxDraw::EnumDisplayModesCallback(LPDDSURFACEDESC pddsd, LPVOID lpContext)
 {
 
 /*
 * Very large resolutions cause problems on some hardware. They are also
 * not very useful for real-time rendering. We have chosen to disable
 * them by not reporting them as available.
 */
 /*
 * Save this mode at the end of the mode array and increment mode count
 */
 if(dkainum >= 20)
  return DDENUMRET_OK;
 
  kaiz[dkainum].h = pddsd->dwHeight;
  kaiz[dkainum].w = pddsd->dwWidth;
  kaiz[dkainum].bits = pddsd->ddpfPixelFormat.dwRGBBitCount;
 
  dkainum ++;
  return DDENUMRET_OK;
 }
 

KAIZ の中に結構な数のモードが格納されます。その中に指定の解像度があれば、その解像度+色数で初期化をします。
無い場合はそれに近い解像度で初期化します。とくに色数は kaiz[].bits に入っているのでこれに24が存在しなければ32にしなければいけません。

これは関数にして解像度が使えるかどうか調べるのが良いでしょう。下はそのサンプルコード。できれば自分で考えてみてください。
 

 BOOL CDxDraw::TestColor(int mWidth, int mHeight, int ColorBits) →int ColorBitsをint mColorBitsに訂正(関数内で mColorBitsになっている)
 {
 for(int i=0 ; i<40; i++)
 {
 if(kaiz[i].w == mWidth)
 if(kaiz[i].h == mHeight)
 if(kaiz[i].bits == mColorBits)
  return TRUE; →for文閉じる } が抜けている
  return FALSE;
 }
 

これで初期化は終了しました。そこでテストとして、実行中に解像度を変えてみます。

まず、メニューに解像度を変更するためのハンドルを作ってください。新しいメニューで「解像度」をつくり、その中に1024というTAGを作ります。その1024のハンドルを作ってその中に以下のコードを書きます。
 

 void CDddView::OnMenuitem1024()
 {
 // TODO: この位置にコマンド ハンドラ用のコードを追加してください
 
 HRESULT ret;
 
 ret = ddx.lpDD->SetDisplayMode(1024, 768, 32); //もし動かなかったら(解像度変更エラー)32のところを24にしてください
 if(ret != DD_OK)
  AfxMessageBox("解像度変更エラー");
 
 }
 

実行してみて解像度が変わったらOKです。

次に、ビットマップを表示してみましょう。

DirectDrawは、サーフェスというものを使います。これは、Imageと似ています。サーフェスを作成してそこにビットマップを転送します。その後、それを見える場所に転送する、という手順を踏みます。
サーフェスには、プライマリサーフェスと、バックバッファと呼ばれるものを最初に作ります。プライマリサーフェスというのは、一番上にあってユーザが見る部分にあたります。バックバッファというのは、書き込みに使用します。バックバッファに書き込んだあと、それをプライマリサーフェスに転送します。その転送する方法をフリップと呼びます。

そのほか、パーツを持つためのサーフェスなども必要に応じて作成します。

最初につくるのはプライマリサーフェスです。これはCDxDrawのメンバ関数です。
 

 //----- プライマリサーフェスを作成する
 int CDxDraw::MakePrimarySurface()
 {
 
 // 書き漏らしや間違いがあると、エラーになってしまうので
 // 注意
 
 HRESULT ret;
 
 lpDDSFrontBuffer = 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;
 
 ret = lpDD->CreateSurface(&ddsd, &lpDDSFrontBuffer, NULL);
 if (ret != DD_OK){
  AfxMessageBox("CreateSurface errorだよ");
  return -1;
 }
  return DD_OK;
 }
 

このプライマリサーフェスを作る関数はViewの「Oncreate」でDirectDrawのインターフェースを作った後に呼び出します。

次にビットマップを読み込むオフスクリーンバッファを作ります。
注意するのはここで制作するオフスクリーンバッファは、読み込んだビットマップより大きく作らなければならないということです。実際は制作順序が逆で、先に読み込むビットマップの大きさを調べてからこの大きさを決めなければなりません。でもまあ、いまはこれでOK。
 

 //------ オフスクリーンバッファを作成する
 int CDxDraw::MakeOff1()
 {
 HRESULT ret;
 
 ZeroMemory(&ddsd1, sizeof(ddsd1));
 ddsd1.dwSize = sizeof(ddsd1);
 ddsd1.dwFlags = DDSD_CAPS | DDSD_HEIGHT | DDSD_WIDTH; // 必要
 ddsd1.ddsCaps.dwCaps =
 DDSCAPS_OFFSCREENPLAIN;
 
 ddsd1.dwWidth = 450;
 ddsd1.dwHeight = 600;
 
 ret = lpDD->CreateSurface(&ddsd1, &lpOS1, NULL);
 
 if (ret != DD_OK){
  AfxMessageBox("CreateSurface errorだよ");
 }
  return ret;
 }
 

おなじくOnCreateにて呼び出しましょう。

あとはビットマップを読み込んでそれをオフスクリーンバッファにコピーします。
オフスクリーンバッファは、ビットマップを同じように、デバイスコンテキストを取得できるのでやり方はビットマップのコピーと同じです。ここでは、リソースからビットマップを読み込んでオフスクリーンバッファに転送してみます。もちろん、リソースで適当な大きさの(300*300くらい)ビットマップをインポートしておいてください。

読み込みと表示はキーダウンでやりましょう。リターンキーでビットマップを読み込んで、スペースキーで表示します。
 

 void CDddView::OnKeyDown(UINT nChar, UINT nRepCnt, UINT nFlags)
 {
 // TODO: この位置にメッセージ ハンドラ用のコードを追加するかまたはデフォルトの処理を呼び出してください
 
 if(nChar==VK_ESCAPE){
  GetParent()->PostMessage(WM_CLOSE,0,0);
 }
 
 if(nChar==VK_RETURN){
 
  ddx.DxLoadBitmap();
 }
 
 if(nChar==VK_SPACE)
 {
 
  RECT rcDest, rcSrc;
 
  SetRect(&rcDest, 0,0,300,300);
  SetRect(&rcSrc, 0,0,300,300);
 
  ddx.lpDDSFrontBuffer->Blt(&rcDest, ddx.lpOS1,&rcSrc, DDBLT_WAIT, NULL);
 
 }
 }
 

うまく表示できましたか?

ヘッダーに以下のことを書き込んでくださいあとddx.DxLoadBitmap();の定義をしてないのでその定義もしてください

ヘッダの中
 

 //----- プライマリサーフェス+バックバッファ用のサーフェス
 LPDIRECTDRAWSURFACE lpDDSFrontBuffer; // フロントバッファ
 DDSURFACEDESC ddsd;
 
 LPDIRECTDRAWSURFACE lpBackBuffer; // フロントバッファ
 DDSCAPS ddscaps;
 
 //----- オフスクリーン用サーフェス1
 LPDIRECTDRAWSURFACE lpOS1;
 DDSURFACEDESC ddsd1;
 
 LPDIRECTDRAWSURFACE lpOS2;
 DDSURFACEDESC ddsd2;
 
 LPDIRECTDRAWSURFACE lpOS3;
 DDSURFACEDESC ddsd3;
 
 ヘッダの中は以上です
 
 つぎにDxLoadBitmap();は
 
 void CDxDraw::DxLoadBitmap();
 {
 //----- ビットマップの読み込み
 CBitmap Bitmap;
 
 CDC DC;
 DC.CreateCompatibleDC(0);
 Bitmap.LoadBitmap(IDB_BITMAP1);
 DC.SelectObject(&Bitmap);
 
 //----- オフスクリーンサーフェスの作成
 HRESULT ret;
 
 ZeroMemory(&ddsd1, sizeof(ddsd1));
 ddsd1.dwSize = sizeof(ddsd1);
 ddsd1.dwFlags = DDSD_CAPS | DDSD_HEIGHT | DDSD_WIDTH; // 必要
 ddsd1.ddsCaps.dwCaps =
 DDSCAPS_OFFSCREENPLAIN;
 
 ddsd1.dwWidth = 450;
 ddsd1.dwHeight = 600;
 
 ret = lpDD->CreateSurface(&ddsd1, &lpOS1, NULL);
 
 //----- オフスクリーンサーフェスへ転送
 HDC hdcSurf;
 
 if(lpOS1->GetDC(&hdcSurf)== DD_OK){
  BitBlt(hdcSurf, 0,0,450, 600,DC,0,0,SRCCOPY);
 
  lpOS1->ReleaseDC(hdcSurf);
 } 
 
 }
 

これでうまくいくはずです

ではこの章の最後にDirectDrawの転送スピードを実感できる実験を。
OnMouseMoveに以下のコードを打ち込んでみましょう。
 

 RECT rcDest, rcSrc;
 
 SetRect(&rcDest, point.x,point.y,300+point.x,300+point.y);
 SetRect(&rcSrc, 0,0,300,300);
 
 ddx.lpDDSFrontBuffer->Blt(&rcDest, ddx.lpOS1,&rcSrc, DDBLT_WAIT, NULL);
 

一度ビットマップを読み込んでおくとマウスにあわせて転送を行います。
なかなか早いでしょ?