落書きノート

ふと自分が気になった事を書いてます

VC++ DirectX9 基本的な設定と三次元シーンの構築と描画

まずはDirectXGraphicsの初期化から入る。

// レンダラーオブジェクト
Renderer renderer;  

// レンダラーの初期化
HRESULT hr;
hr = renderer.Initialize(hWnd, isFullScreen, CLIENT_WIDTH, CLIENT_HEIGHT);
    
if(FAILED(hr)) {
    return 0;    // 初期化失敗
}

Rendererクラスを使っているが、もちろん別にRenderer.cppを作成してクラスを定義して使っている。ここでは、RendererクラスのInitializeメソッドを使って初期化している。タイミングはウィンドウの登録が完了した直後に実行している。

// Direct3D9オブジェクトの作成
if((m_pD3D = ::Direct3DCreate9(D3D_SDK_VERSION)) == 0){
        return E_FAIL;    // 取得失敗
}

上記は、DirectXGraphic自体の初期化。この処理が成功すると、m_pD3DにDirect3D9オブジェクトへのポインタが保存される。

// デバイスのプレゼンテーションパラメータを初期化
ZeroMemory(&m_D3DPP, sizeof(D3DPRESENT_PARAMETERS));
m_D3DPP.BackBufferCount = 1;
if(isFullScreen) { // フルスクリーンの場合
    m_D3DPP.Windowed = FALSE;           // フルスクリーン表示の指定
    m_D3DPP.BackBufferWidth = clientWidth;      // フルスクリーン時の横幅
    m_D3DPP.BackBufferHeight = clientHeight;        // フルスクリーン時の縦幅
}
else {
    m_D3DPP.Windowed = TRUE;                // ウインドウ内表示の指定
}
m_D3DPP.BackBufferFormat = d3ddm.Format;            // カラーモードの指定
m_D3DPP.SwapEffect = D3DSWAPEFFECT_DISCARD; // 
m_D3DPP.EnableAutoDepthStencil = TRUE;                      // エラー対策
m_D3DPP.AutoDepthStencilFormat = D3DFMT_D16;                // エラー対策

上記は、プレゼンテーションパラメータの設定を行っている。プレゼンテーションパラメータとは、フルスクリーンモードを使うかウィンドウモードを使うか、あるいは画面の解像度や色数といった設定のこと。ZeroMemory関数を使っているが、これはD3DPRESENT_PARAMETERS構造体のすべてのメンバが0でクリアされる。なお、D3DPRESENT_PARAMETERS構造体のほとんどのメンバのデフォルト値は0なので、実用上は問題ない。

// ディスプレイアダプタを表すためのデバイスを作成
// 描画と頂点処理をハードウェアで行なう
if(FAILED(m_pD3D->CreateDevice(D3DADAPTER_DEFAULT, 
            D3DDEVTYPE_HAL, 
            hWnd, 
            D3DCREATE_HARDWARE_VERTEXPROCESSING, 
            &m_D3DPP, &m_pD3DDevice))) {
    // 上記の設定が失敗したら
    // 描画をハードウェアで行い、頂点処理はCPUで行なう
    if(FAILED(m_pD3D->CreateDevice(D3DADAPTER_DEFAULT, 
                    D3DDEVTYPE_HAL, 
                    hWnd, 
                    D3DCREATE_SOFTWARE_VERTEXPROCESSING, 
                    &m_D3DPP, &m_pD3DDevice))) {
        // 上記の設定が失敗したら
        // 描画と商店処理をCPUで行なう
        if(FAILED(m_pD3D->CreateDevice(D3DADAPTER_DEFAULT, 
                        D3DDEVTYPE_REF, hWnd, 
                        D3DCREATE_SOFTWARE_VERTEXPROCESSING, 
                        &m_D3DPP, &m_pD3DDevice))) {
                    // 初期化失敗
                return E_FAIL;
            }
        }
    }

上記について、詳細は以下の通り。一回目の処理は以下の様な設定。

  • D3DTYPE_HAL:ハードウェアアクセラレーション機能を使う
  • D3DCREATE_HARDWARE_VERTEXPROCESSING:頂点処理をハードウェアで行う

二回目の処理は以下の様な設定。

  • D3DTYPE_HAL:ハードウェアアクセラレーション機能を使う
  • D3DCREATE_SOFTWARE_VERTEXPROCESSING:頂点処理をソフトウェアで行う

三回目の処理は以下の様な設定。

  • 3DTYPE_REF:ハードウェアアクセラレーション機能を使わない
  • D3DCREATE_SOFTWARE_VERTEXPROCESSING:頂点処理をソフトウェアで行う

このように分ける理由は、PCによって使える機能が違う。ビデオカードが頂点処理ハードウェアを搭載していれば、一回目の処理で問題ないし、パフォーマンスも良い。ノートPCなどは頂点処理に対応していないビデオチップを搭載していることが多く、そのような場合はCreateDeviceメソッドが失敗する。そこで二回目の処理を行う。多くの環境ではこの処理でデバイスを作成出来る。しかし何らかの理由で二回目までの処理によるデバイス生成が失敗したときは、すべての処理をソフトウェアのみで行うデバイスを作成しないと行けない。それが三回目の処理。

// シーンオブジェクト
Scene    scene;

// シーンの生成
if(FAILED(scene.Create(renderer.GetDevice()))) {
    return 0;
}

SceneクラスのCreateメソッドを呼び出して、シーンの構築を行っている。

//-------------------------------------------------------------
// シーンを生成
// 引数
//     pD3DDevice : IDirect3DDevice9 インターフェイスへのポインタ
// 戻り値
//     成功したらS_OK
//-------------------------------------------------------------
HRESULT Scene::Create(LPDIRECT3DDEVICE9 pD3DDevice)
{
    Destroy();

    if(pD3DDevice == 0) {
        return E_FAIL;
    }

    // オブジェクトのVertexBufferを生成
    if( FAILED( pD3DDevice->CreateVertexBuffer( 3*sizeof(MY_VERTEX),
                        D3DUSAGE_WRITEONLY,
                        MY_VERTEX_FVF,
                        D3DPOOL_MANAGED, &m_pVB, NULL ))){
        return E_FAIL;
    }

    //VertexBufferの中身を埋める
    MY_VERTEX* v;
    m_pVB->Lock( 0, 0, (void**)&v, 0 );
    // 各頂点の位置
    v[0].p = D3DXVECTOR3(-1.0f,  1.0f, 0.0f );
    v[1].p = D3DXVECTOR3( 1.0f, -1.0f, 0.0f );
    v[2].p = D3DXVECTOR3(-1.0f, -1.0f, 0.0f );
    // 各頂点の色
    v[0].color = D3DXCOLOR( 1.0f, 0.0f,  0.0f,  1.0f);
    v[1].color = D3DXCOLOR( 0.0f, 1.0f,  0.0f,  1.0f);
    v[2].color = D3DXCOLOR( 0.0f, 0.0f,  1.0f,  1.0f);
    m_pVB->Unlock();

    // レンダリングステートパラメータの設定
    pD3DDevice->SetRenderState( D3DRS_CULLMODE, D3DCULL_NONE);
    pD3DDevice->SetRenderState( D3DRS_ZENABLE, TRUE);
    pD3DDevice->SetRenderState( D3DRS_LIGHTING, FALSE);

    // ビューイング行列の初期化
    D3DXMatrixIdentity( &m_view);
    D3DXMatrixLookAtLH( &m_view, &D3DXVECTOR3(0.0f, 0.0f, -5.0f), 
                                 &D3DXVECTOR3(0.0f, 0.0f, 0.0f), 
                                 &D3DXVECTOR3(0.0f, 1.0f, 0.0f));

    return S_OK;
}

VertexBufferはシーンに配置される3Dモデルを表すデータの集まり、レンダリングステート…。この詳細は次の記事以降で載せる予定。

// ビューポートと深度バッファのクリアとステンシルバッファの削除
if(FAILED(m_pD3DDevice->Clear(0,NULL,        // クリアする領域は全面
            D3DCLEAR_TARGET |           // バックバッファを指定
            D3DCLEAR_ZBUFFER,           // 深度バッファ(Zバッファ)を指定
            D3DCOLOR_XRGB(0,0,0),        // クリアする色
            1.0f,                      // クリアする深度バッファ(Zバッファ)の値
            0))) {                     // クリアするステンシルバッファの値
    return;
}

// 描画開始宣言
if(SUCCEEDED(m_pD3DDevice->BeginScene())) {

    // モデルなどの描画
    pScene->Draw(m_pD3DDevice);

    // 描画終了宣言
    m_pD3DDevice->EndScene();
}

上記はバッファのクリアをしている。それが終わったら実際に描画する。ただし、描画処理の前後には、それぞれ描画宣言、描画終了宣言を行わなければならない。この辺りはWin32APIを使ったことのある人ならすぐに分かる。

//-------------------------------------------------------------
// オブジェクト等の描画
// 引数
//     pD3DDevice : IDirect3DDevice9 インターフェイスへのポインタ
//-------------------------------------------------------------
void Scene::Draw(LPDIRECT3DDEVICE9 pD3DDevice)
{
    if(pD3DDevice == 0) {
        return;
    }

    if(m_pVB == 0) {
        return;
    }

    // 座標変換(カメラの設定など)
    Transform(pD3DDevice);

    // 三角形の描画処理
    // 頂点バッファの設定
    pD3DDevice->SetStreamSource( 0, m_pVB, 0, sizeof(MY_VERTEX) );
    // 頂点バッファのフォーマットの設定
    pD3DDevice->SetFVF( MY_VERTEX_FVF );
    // 頂点バッファの描画
    pD3DDevice->DrawPrimitive( D3DPT_TRIANGLESTRIP, 0, 1 );

}

描画開始宣言が成功したら、Scene::Draw関数を呼び出して実際にシーン描画していく。先ほど構築したVertexBufferつまり頂点情報をデバイスにセットして、DrawPrimitiveメソッドにより描画する。この時点でシーンはバックバッファに書き込まれている。しかし、バックバッファは裏画面なのでまだ目に見えるようにはなっていない。バックバッファの内容を目に見えるように表示するには、IDirect3DDevice9::Presentメソッドを用いて、ディスプレイに転送しなければならない。

// 描画結果の転送
if(FAILED(m_pD3DDevice->Present( 0, 0, 0, 0 ))) {
    // デバイス消失から復帰
    m_pD3DDevice->Reset(&m_D3DPP);
}

以上で描画処理は終了。これらの処理はメッセージループがまわるごとに呼びだされ、そのたびに画面が更新される。このおかげで、アニメーションを行うことが出来る。

// メッセージ処理および描画ループ
while(TRUE) {
  if(PeekMessage(&msg, NULL, 0, 0, PM_REMOVE)) {
    if(msg.message == WM_QUIT) {  // PostQuitMessage()が呼ばれた
        break;    //ループの終了
    }
    else {
    // メッセージの翻訳とディスパッチ
        TranslateMessage(&msg);
        DispatchMessage(&msg);
    }
} else {  // 処理するメッセージが無いときは描画を行う

    // ウィンドウが見えている時だけ描画するための処理
    WINDOWPLACEMENT wndpl;
    GetWindowPlacement(hWnd, &wndpl);   // ウインドウの状態を取得
    if((wndpl.showCmd != SW_HIDE) && 
        (wndpl.showCmd != SW_MINIMIZE) &&
        (wndpl.showCmd != SW_SHOWMINIMIZED) &&
        (wndpl.showCmd != SW_SHOWMINNOACTIVE)) {

        // 描画処理の実行
        renderer.RenderScene(&scene);
        }
    }

        // OSに制御を明け渡すためのスリープ(コラム参照)
        Sleep(1);
    }

最後に終了処理。

//-------------------------------------------------------------
// シーンの破棄
//-------------------------------------------------------------
void Scene::Destroy()
{
    // 頂点バッファの解放
    SAFE_RELEASE(m_pVB);
}

Sceneクラスのデストラクタでは、SceneクラスのDestroyメソッドを呼び出して、上記のようなコードを実行している。ここでは、デバイスから生成した頂点バッファオブジェクトを開放している。もし、ほかのオブジェクトも生成している場合は、ここですべて開放しなければならない。ここで利用しているSAFE_RELEASEマクロは、次のように定義している。

// 参照カウンタのデクリメント
#define SAFE_RELEASE(p) { if(p) { (p)->Release(); (p)=NULL; } }

一方、Rendererクラスのデストラクタでは、RendererクラスのFinalizeメソッドを呼び出して、次のようなコードを実行している。

//-------------------------------------------------------------
// 終了処理
//-------------------------------------------------------------
void Renderer::Finalize()
{
    // デバイスオブジェクトの解放
    SAFE_RELEASE(m_pD3DDevice);

    // DirectXGraphicsの解放
    SAFE_RELEASE(m_pD3D);
}

見ての通り、デバイスオブジェクト、DirectXGraphicsの順で開放している。これは定型的な処理で、変更されることはほとんどない。

話はそれるが、エラーの対処に便利なマクロの定義方法をメモしておく。

// エラーの報告とアプリケーションの終了
#define ERROR_EXIT() { int line = __LINE__; const char *file = __FILE__;\
   char msg[_MAX_FNAME + _MAX_EXT + 256];\
   char drive[_MAX_DRIVE];\
   char dir[_MAX_DIR];\
   char fname[_MAX_FNAME];\
   char ext[_MAX_EXT];\
   _splitpath(file, drive, dir, fname, ext);\
   sprintf(msg, "何らかのエラーが発生したためアプリケーションを終了します\r\n"\
       "ファイル : %s%s\r\n"\
       "行番号 : %d", fname, ext, line);\
   MessageBox(NULL, msg, "Error", MB_OK | MB_ICONEXCLAMATION);\
   PostQuitMessage(1);\
}

これを以下のように使うことが出来る。

// Direct3D9オブジェクトの作成
if((m_pD3D = ::Direct3DCreate9(D3D_SDK_VERSION)) == 0){
    ERROR_EXIT();
    return E_FAIL;    // 取得失敗
}