【3Dゲーム開発#02】Direct3D11のセットアップ

前回までで、Windows上でウィンドウを表示、任意で閉じることができるようになりました。

今回は、ウィンドウ内に描画する準備をしていきます。具体的にはDirectX11を使って描画していきます。

前回の記事:【3Dゲーム開発#01】Windowの表示

3Dゲーム開発記事のまとめ:「3Dゲーム開発」

DirectX11を使うことについて

こちらではグラフィクスAPIとしては、DirectX11を利用します。

DirectXはゲームグラフィクスでは定番の描画ライブラリですが、現在の最新バージョンはDirectX12です。

ただ、DirectXは進化とともに、様々な描画機能を差し込むことが可能となっており、それらすべてを使うための知識は複雑で難易度が上がっています。

DirectX11は、プログラマブルシェーダを使った現代のゲームグラフィクスの基礎としては十分な機能が備わっており、DirectX12を使う場合もDirectX11の知識が基礎となります。

こういった現状を踏まえ、グラフィクスが本質の目的でない本記事では、DirectX11を使っていきます。

DirectX12に関しては、現在の日本語の唯一の解説書でありながら、詳細な解説がされている名著[]()があるので、気になる方は読んでみてください。

Direct3D11のセットアップ

Rendererクラスの作成

まずは描画を担うRendererクラスを作成します。

クラス構成は以下です。以降で実装の中身を詳しく見ていきましょう。

#pragma once

#include <d3d11.h>
#pragma comment(lib, "d3d11.lib")
#define DX_SAFE_RELEASE(x)    { if(x) { (x)->Release(); (x)=nullptr; } }

class Renderer
{
public:
    Renderer();
    ~Renderer();

    bool Initialize(HWND hWindow);

    void Terminate();

    void Draw();

    void Swap();

    ID3D11Device* GetDevice() { return pD3DDevice_; }
    ID3D11DeviceContext* GetDeviceContext() { return pImmediateContext_; }

private:
    bool initDeviceAndSwapChain(HWND hWindow);
    bool initBackBuffer();

private:
    //! 機能レベルの配列
    static const UINT   FEATURE_LEVELS_NUM = 4;
    D3D_FEATURE_LEVEL pFeatureLevels_[FEATURE_LEVELS_NUM] = {};
    //! デバイス作成時に返される機能レベル
    D3D_FEATURE_LEVEL featureLevelsSupported_;

    //! デバイス
    ID3D11Device*           pD3DDevice_ = nullptr;
    //! デバイスコンテキスト
    ID3D11DeviceContext*    pImmediateContext_ = nullptr;
    //! スワップ・チェイン
    IDXGISwapChain*         pSwapChain_ = nullptr;

    //! 描画ターゲット・ビュー
    ID3D11RenderTargetView* pRenderTargetView_ = nullptr;
    //! ビューポート
    D3D11_VIEWPORT          viewPort_[1];

    UINT    backBufferNum_ = 3;
    UINT    screenWidth_ = 0;
    UINT    screenHeight_ = 0;

};

初期化(Initialize)

初期化処理が以下となります。

前回までで実装した、Windowのハンドルを渡して、Window情報からスクリーンサイズを初期化します。

そして、グラフィクス処理セットアップの大きな要素

  • デバイスとスワップチェインの作成
  • バックバッファーの初期化

を行います。それぞれ詳しくは後述していきます。

bool Renderer::Initialize(HWND hWindow)
{
    // Windowに合わせてスクリーンサイズ初期化
    RECT rc;
    GetClientRect(hWindow, &rc);
    screenWidth_ = rc.right - rc.left;
    screenHeight_ = rc.bottom - rc.top;

    initDeviceAndSwapChain(hWindow);

    initBackBuffer();

    return true;
}

デバイスとスワップチェインの作成

以下がデバイスとスワップチェインの作成処理です。

初めてではなかなか聞き慣れない用語ですが、ざっくりいってしまえば

  • デバイス:使用するディスプレイアダプター(GPU)
  • スワップチェイン:画面の更新(画面の描画バッファー(フロントバッファー)とバックバッファーの切替)

パラメータは多いですが、基本的にやってる事自体はD3D11CreateDeviceAndSwapChain()の呼び出し、だけです。

bool Renderer::initDeviceAndSwapChain(HWND hWindow)
{
    // デバイスとスワップ・チェイン作成
    DXGI_SWAP_CHAIN_DESC sd;
    ZeroMemory(&sd, sizeof(sd));
    sd.BufferCount = backBufferNum_;        // バックバッファの数
    sd.BufferDesc.Width = screenWidth_;    // バックバッファの幅
    sd.BufferDesc.Height = screenHeight_;    // バックバッファの高さ
    sd.BufferDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM;    // フォーマット
    sd.BufferDesc.RefreshRate.Numerator = 60;    // リフレッシュレート(分母)
    sd.BufferDesc.RefreshRate.Denominator = 1;    // リフレッシュレート(分子)
    sd.BufferDesc.ScanlineOrdering = DXGI_MODE_SCANLINE_ORDER_PROGRESSIVE;
    sd.BufferDesc.Scaling = DXGI_MODE_SCALING_CENTERED;
    sd.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT;    // バックバッファの使用法
    sd.OutputWindow = hWindow;        // 関連付けるウィンドウ
    sd.SampleDesc.Count = 1;            // マルチサンプル(アンチエイリアス)の数
    sd.SampleDesc.Quality = 0;            // マルチサンプル(アンチエイリアス)のクオリティ
    sd.Windowed = TRUE;        // ウィンドウモード(TRUEがウィンドウモード)
    sd.Flags = DXGI_SWAP_CHAIN_FLAG_ALLOW_MODE_SWITCH;        // モード自動切り替え

#if defined(DEBUG) || defined(_DEBUG)
    UINT createDeviceFlags = D3D11_CREATE_DEVICE_DEBUG;
#else
    UINT createDeviceFlags = 0;
#endif

    const D3D_DRIVER_TYPE DriverTypes[] = {
        D3D_DRIVER_TYPE_HARDWARE,
        D3D_DRIVER_TYPE_WARP,
        D3D_DRIVER_TYPE_REFERENCE,
    };

    HRESULT hr;
    for (auto type : DriverTypes) {
        // ハードウェアデバイスを作成
        hr = D3D11CreateDeviceAndSwapChain(
            nullptr, type, nullptr, createDeviceFlags,
            pFeatureLevels_, FEATURE_LEVELS_NUM, D3D11_SDK_VERSION, &sd,
            &pSwapChain_, &pD3DDevice_, &featureLevelsSupported_, &pImmediateContext_);
        if (SUCCEEDED(hr)) {
            break;
        }
    }
    if (FAILED(hr)) return false;

    return true;
}

この処理で設定パラメータに基づいてメンバに持っている

    //! デバイス
    ID3D11Device*           pD3DDevice_ = nullptr;
    //! デバイスコンテキスト
    ID3D11DeviceContext*    pImmediateContext_ = nullptr;
    //! スワップ・チェイン
    IDXGISwapChain*         pSwapChain_ = nullptr;

が取得されます。

なかなか聞き慣れない用語かと思いますが、それぞれ以下のようなものです。

  • デバイス:GPUなどの描画デバイス(ハードウェア)へのインターフェース
  • デバイスコンテキスト:描画デバイス解釈をパラメータに応じてラップ(=コンテキスト)した描画命令インターフェース。基本的にこれを介して描画操作を行う
  • スワップチェイン:画面への更新反映を行う。フロントバッファ(画面)とバックバッファ(裏の描画先)を入れ替える(スワップ)

ただ、この説明でも聞き慣れない用語が多いかもしれません。その場合、描画について詳しく取り扱う書籍や他サイトを参照ください。

本記事では主目的が3Dゲーム作成のため、これ以上の深堀りはしません。

バックバッファの初期化

続いて、ゲーム処理上の描画先となるバックバッファをセットアップします。

バックバッファは、上述した通りスワップチェインの裏描画先のことです。

ここに描画していき、画面に表示する準備が整った良きタイミングでフロントバッファと入れ替え(スワップ)して画面を更新します。

そのバックバッファのセットアップ手順は

  • バックバッファに描画ターゲットビュー(RenderTargetView)を作成・設定
  • そのRenderTargetViewに対して、ViewPortを設定

です。

以下のコードとなります。

bool Renderer::initBackBuffer()
{
    HRESULT hr;

    // スワップ・チェインから最初のバック・バッファを取得する
    ID3D11Texture2D *pBackBuffer;  // バッファのアクセスに使うインターフェイス
    hr = pSwapChain_->GetBuffer(
        0,                         // バック・バッファの番号
        __uuidof(ID3D11Texture2D), // バッファにアクセスするインターフェイス
        (LPVOID*)&pBackBuffer);    // バッファを受け取る変数
    if (FAILED(hr)) {
        //TRACE("InitBackBuffer g_pSwapChain->GetBuffer(%0x08x)n", hr);  // 失敗
        return false;
    }

    // バック・バッファの情報
    D3D11_TEXTURE2D_DESC descBackBuffer;
    pBackBuffer->GetDesc(&descBackBuffer);

    // バック・バッファの描画ターゲット・ビューを作る
    hr = pD3DDevice_->CreateRenderTargetView(
        pBackBuffer,           // ビューでアクセスするリソース
        nullptr,               // 描画ターゲット・ビューの定義
        &pRenderTargetView_); // 描画ターゲット・ビューを受け取る変数
    DX_SAFE_RELEASE(pBackBuffer);  // 以降、バック・バッファは直接使わないので解放
    if (FAILED(hr)) {
        //TRACE("InitBackBuffer g_pD3DDevice->CreateRenderTargetView(%0x08x)n", hr);  // 失敗
        return false;
    }

    // ビューポートの設定
    viewPort_[0].TopLeftX = 0.0f;    // ビューポート領域の左上X座標。
    viewPort_[0].TopLeftY = 0.0f;    // ビューポート領域の左上Y座標。
    viewPort_[0].Width    = static_cast<float>(screenWidth_);  // ビューポート領域の幅
    viewPort_[0].Height   = static_cast<float>(screenHeight_);  // ビューポート領域の高さ
    viewPort_[0].MinDepth = 0.0f; // ビューポート領域の深度値の最小値
    viewPort_[0].MaxDepth = 1.0f; // ビューポート領域の深度値の最大値
    pImmediateContext_->RSSetViewports(1, &viewPort_[0]);

    return true;
}

描画処理

これで最低限の、バックバッファに書き込んで、スワップチェインで画面へ更新、する準備が整ったので、描画処理実装に入りましょう。

上述の通り

  • バックバッファひも付きのRenderTargetViewへの描画(Draw())
  • バックバッファをフロントバッファ(画面)へ表示(Swap())

という処理を実装します。

void Renderer::Draw()
{
    if (!pImmediateContext_ || !pRenderTargetView_) return;

    // 青でクリア
    float color[] = { 0.f, 0.f, 1.f, 0.f };
    pImmediateContext_->ClearRenderTargetView(pRenderTargetView_, color);
}

void Renderer::Swap()
{
    // バックバッファの表示(画面をすぐに更新)
    HRESULT hr = pSwapChain_->Present(0, 0);
    if (FAILED(hr)) {
        //TRACE("Swap failed(%0x08x)n", hr);
        return;
    }
}

描画は、上述もしましたが、基本的にデバイスコンテキストを介して命令します(いろいろなデバイスがあってもいい感じにラップしてくれる)。

今回の描画は、任意の色(青)でクリアするだけです。

あとは、SwapChain->Presentで画面に反映されます。

終了処理

ここまで、Direct3D、DXGIに関する各種インターフェース・リソースを取得しました。

こういったセットアップしたリソースは、使う準備を整えた状態、となっているため、そのままではずっと使える状態を保持したままになります。

そこで使い終わったら、解放してあげることで、もう使わないということを伝え、使うための準備リソース(メモリなどの資源)を解放します。

以下が解放処理です。

アプリの終了時に呼ばれ、取得したすべてのリソースを解放してから、アプリケーションを終了します。

void Renderer::Terminate()
{
    // デバイス・ステートのクリア
    if (pImmediateContext_) pImmediateContext_->ClearState();

    // スワップ チェインをウインドウ モードにする
    if (pSwapChain_) pSwapChain_->SetFullscreenState(FALSE, nullptr);

    // 取得したインターフェイスの開放
    DX_SAFE_RELEASE(pRenderTargetView_);
    DX_SAFE_RELEASE(pSwapChain_);

    DX_SAFE_RELEASE(pImmediateContext_);
    DX_SAFE_RELEASE(pD3DDevice_);
}

ちなみに、アプリを落とすと、プロセスに関連するメモリも解放されるので、このように最後に全部解放する場合、実際は呼ばずとも成立はしてしまいます。

ただそれでは、システムが「解放されずに残っているリソースがあったから解放したよ」、という警告を出したり、今後の拡張で、途中でリソースを解放したい、といったことに対応できません。

正規処理として、また拡張性も見据えて、しっかりと解放処理は実装しておきましょう。

アプリに組み込み

あとは前回までで作ったウィンドウを表示するアプリに組み込んで、ウィンドウに描画をしていきましょう。

以下のように、Applicationの初期化、ゲームループ、終了処理、に上で実装した各処理の呼び出しを入れるだけです。

しっかり、役割ごとにクラス・メソッドをわけているので、各レイヤーの処理はすっきりですね。

void Application::Initialize(HINSTANCE hInst)
{
    window_.Initialize(hInst);
    renderer_.Initialize(window_.GetWindowHandle());
}

void Application::Terminate()
{
    renderer_.Terminate();
    window_.Terminate();
}

bool Application::gameLoop()
{
    renderer_.Draw();

    renderer_.Swap();

    return true;
}

結果

以上を実装して実行すると、青で画面をクリアしたので、以下のように前回の白から変わって、青一色のウィンドウが表示されます。

setup_graphics

次はいよいよ3D描画のすべての礎となる、ポリゴンの描画、を行っていきます。

次回の記事:【3Dゲーム開発 – 12】ポリゴンの表示

3Dゲーム開発記事のまとめ:「3Dゲーム開発」

コメント

タイトルとURLをコピーしました