BaseDx12(Dx12研究とフレームワーク)

【第1部】シンプルバージョン編

1109.シンプルな三角形の描画(9)

更新描画処理

 一般的なゲームのフレームワークでは、各オブジェクトの位置や回転、大きさといった変化を実装するUpdate処理と、各オブジェクトを描画するDraw処理(もしくはRender処理)を分けて考えます。
 ゲーム画面には、大量のオブジェクトが配置される可能性があります。それら一つ一つのUpdate処理Draw処理を同時い行うと、何かと不便です。つまりタスク分けのような関係になります。
 BaseDx12における更新描画処理は、まず、WinMain.cppの以下の部分から始まります。
int MainLoop(HINSTANCE hInstance, HWND hWnd, int nCmdShow, int iClientWidth, int iClientHeight) {
    //終了コード
    int retCode = 0;
    //ウインドウ情報。メッセージボックス表示チェックに使用
    WINDOWINFO winInfo;
    ZeroMemory(&winInfo, sizeof(winInfo));
    //例外処理開始
    try {
        //COMの初期化
        //サウンドなどで使用する
        if (FAILED(::CoInitialize(nullptr))) {
            // 初期化失敗
            throw exception("Com初期化に失敗しました。");
        }
        basedx12::Scene scene;
        basedx12::App::Init(hWnd, &scene, hInstance, nCmdShow, iClientWidth, iClientHeight);
        //メッセージループ
        MSG msg = { 0 };
        while (WM_QUIT != msg.message) {
            //キー状態が何もなければウインドウメッセージを得る
            if (PeekMessage(&msg, nullptr, 0, 0, PM_REMOVE)) {
                TranslateMessage(&msg);
                DispatchMessage(&msg);
            }
            //更新描画処理
            basedx12::App::UpdateDraw();
        }
        //msg.wParamには終了コードが入っている
        basedx12::App::Destroy();
        retCode = (int)msg.wParam;
    }
    catch (exception & e) {
        //STLエラー
        //マルチバイトバージョンのメッセージボックスを呼ぶ
        if (GetWindowInfo(hWnd, &winInfo)) {
            MessageBoxA(hWnd, e.what(), "エラー", MB_OK);
        }
        else {
            MessageBoxA(nullptr, e.what(), "エラー", MB_OK);
        }
        retCode = 1;
    }
    catch (...) {
        //原因不明失敗した
        if (GetWindowInfo(hWnd, &winInfo)) {
            MessageBox(hWnd, L"原因不明のエラーです", L"エラー", MB_OK);
        }
        else {
            MessageBox(nullptr, L"原因不明のエラーです", L"エラー", MB_OK);
        }
        retCode = 1;
    }
    //例外処理終了
    //COMのリリース
    ::CoUninitialize();
    return retCode;
}
 赤くなっている部分ですが、アイドリングループごとにbasedx12::App::UpdateDraw()関数が呼ばれます。
 その内容は以下です。
    void  App::UpdateDraw() {
        m_timer.Tick();
        m_controler.ResetControlerState();
        if (m_pSceneBase && m_baseDevice)
        {
            m_baseDevice->OnUpdate();
            m_baseDevice->OnDraw();
        }
    }
 ここではタイマーの更新およびコントローラを初期化して、デバイスのOnUpdate()関数そしてOnDraw()関数を呼び出します。

更新処理

            m_baseDevice->OnUpdate();
 は仮想関数ですので、GameDevice::OnUpdate()関数が呼ばれます。以下がその内容です。
    void GameDevice::OnUpdate()
    {
        App::GetSceneBase().OnUpdate();
    }
 このようにシーンOnUpdate()関数を呼び出します。この関数もSceneBaseクラスを親に持つ仮想関数ですので、結果的にScene::OnUpdate()関数が呼ばれます。内容は以下です。
    void Scene::OnUpdate() {
    }
 このように何もしていません。このサンプルは、できるだけ単純にするために、オブジェクトの移動、などの変化はしない内容となっています。次のサンプルからは、ここに記述が入ります。

描画処理

 更新処理は、何もしなくていいのですが、描画処理は処理が必要です。
 m_device->OnUpdate()に続いてm_device->OnDraw()が呼ばれます。
 これは以下の様な処理になります。
    void GameDevice::OnDraw()
    {
        // 描画のためのコマンドリストを集める
        PopulateCommandList();
        // 描画用コマンドリスト実行
        ID3D12CommandList* ppCommandLists[] = { m_commandList.Get() };
        m_commandQueue->ExecuteCommandLists(_countof(ppCommandLists), ppCommandLists);
        // フロントバッファに表示
        ThrowIfFailed(GetIDXGISwapChain3()->Present(1, 0));
        //次のフレームに移動
        MoveToNextFrame();
    }

コマンドリストを集める

 最初に呼ばれる
        PopulateCommandList();
 はコマンドリストを集める処理です。以下の様な内容です。
    // 描画のためのコマンドリストを集める
    void GameDevice::PopulateCommandList()
    {
        ThrowIfFailed(m_commandAllocators[m_frameIndex]->Reset());
        //コマンドリストのリセット(パイプライン指定なし)
        CommandList::Reset(m_commandAllocators[m_frameIndex], m_commandList);
        // Set necessary state.
        m_commandList->SetGraphicsRootSignature(m_rootSignature.Get());
        m_commandList->RSSetViewports(1, &m_viewport);
        m_commandList->RSSetScissorRects(1, &m_scissorRect);
        // Indicate that the back buffer will be used as a render target.
        m_commandList->ResourceBarrier(
            1,
            &CD3DX12_RESOURCE_BARRIER::Transition(
                m_renderTargets[m_frameIndex].Get(), 
                D3D12_RESOURCE_STATE_PRESENT, 
                D3D12_RESOURCE_STATE_RENDER_TARGET
            )
        );
        CD3DX12_CPU_DESCRIPTOR_HANDLE rtvHandle(
            m_rtvHeap->GetCPUDescriptorHandleForHeapStart(), 
            m_frameIndex, 
            m_rtvDescriptorHandleIncrementSize
        );
        m_commandList->OMSetRenderTargets(1, &rtvHandle, FALSE, nullptr);

        // Record commands.
        const float clearColor[] = { 0.0f, 0.2f, 0.4f, 1.0f };
        m_commandList->ClearRenderTargetView(rtvHandle, clearColor, 0, nullptr);
        // シーンに個別描画を任せる
        App::GetSceneBase().OnDraw();
        // Indicate that the back buffer will now be used to present.
        m_commandList->ResourceBarrier(
            1, 
            &CD3DX12_RESOURCE_BARRIER::Transition(
                m_renderTargets[m_frameIndex].Get(), 
                D3D12_RESOURCE_STATE_RENDER_TARGET, 
                D3D12_RESOURCE_STATE_PRESENT
            )
        );
        ThrowIfFailed(m_commandList->Close());
    }
 この関数は、基本的にDirectX-Graphics-Samplesを参考にしてますが、微妙に違います。
 DirectX-Graphics-SamplesのほうはGameDeviceとかSceneとかの概念はないので、この関数内で、コマンドリストに、描画処理を集めてますが、BaseDx12では、個別の描画をシーンに任せます。

コマンドアロケータとコマンドリストのリセット

 ここではまず
        ThrowIfFailed(m_commandAllocators[m_frameIndex]->Reset());
        //コマンドリストのリセット(パイプライン指定なし)
        CommandList::Reset(m_commandAllocators[m_frameIndex], m_commandList);
 で、コマンドアロケータとコマンドリストのリセットを行います。
 コマンドリストのリセットは、パイプラインを指定することもできるのですが、個別描画の中で、オブジェクト単位でパイプラインステートを設定することもあるので、この指定はシーン側に任せます。

ルートシグネチャと描画領域

 このあと
        m_commandList->SetGraphicsRootSignature(m_rootSignature.Get());
        m_commandList->RSSetViewports(1, &m_viewport);
        m_commandList->RSSetScissorRects(1, &m_scissorRect);
 でルートシグネチャの設定を行いビューポートシザー矩形を設定します。
 ビューポートはこれまでのDirectXにあった概念で、GPUのデバイス座標ウインドウのクライアント座標を結びつけるものです。
 GameDeviceの親クラスであるBaseDeviceクラスのコンストラクタですでに初期化されています。
 デフォルトは、WinMain.cppによって作成された、ウインドウの幅、高さが設定されています。奥行(depth)は0.0から1.0に設定されています。
 GameDeviceの初期化時に再設定することもできます。
 シザー矩形は、ビューポートのうち、どの領域を描画するかを指定します。画面の中を部分的に描画することもできるように、なっています。デフォルトはビューポート同様BaseDeviceクラスのコンストラクタで初期化されており、ビューポート全体を描画するようになっています。変更する場合はこちらも、GameDeviceの初期化時などに行います。

バリアを張る

 Dx12では、バックバッファに書き込むタイミングで、他からの書き込みを制限するバリアの実装もプログラマが行います。
 その記述は以下です。
        m_commandList->ResourceBarrier(
            1, 
            &CD3DX12_RESOURCE_BARRIER::Transition(
                m_renderTargets[m_frameIndex].Get(),
                D3D12_RESOURCE_STATE_PRESENT, 
                D3D12_RESOURCE_STATE_RENDER_TARGET
            )
        );
 このバリアは、バックバッファがレンダーターゲットとして使用される、推移時のバリアです。

レンダーターゲットビューの作成と設定

 この後、レンダーターゲットビューを、ハンドルという形で作成し、コマンドリストに設定します。
        CD3DX12_CPU_DESCRIPTOR_HANDLE rtvHandle(
            m_rtvHeap->GetCPUDescriptorHandleForHeapStart(), 
            m_frameIndex, 
            m_rtvDescriptorHandleIncrementSize
        );
        m_commandList->OMSetRenderTargets(1, &rtvHandle, FALSE, nullptr);

背景色の設定

 背景色背景色でクリアするという形で設定します。ここで設定するのは濃い青の背景色です。
 ここでもコマンドリストで行います。先ほど作成したrtvHandleも再利用します。
        const float clearColor[] = { 0.0f, 0.2f, 0.4f, 1.0f };
        m_commandList->ClearRenderTargetView(rtvHandle, clearColor, 0, nullptr);

シーンに個別描画を任せる

 ここから先は、シーンに個別描画を任せます。
        App::GetSceneBase().OnDraw();
 この呼び出しで、Scene::OnDraw()関数が呼ばれます。
    void Scene::OnDraw() {
        auto baseDevice = App::GetBaseDevice();
        auto commandList = baseDevice->GetCommandList();
        commandList->SetPipelineState(m_pipelineState.Get());
        commandList->IASetPrimitiveTopology(D3D_PRIMITIVE_TOPOLOGY_TRIANGLELIST);
        commandList->IASetVertexBuffers(0, 1, &m_baseMesh->GetVertexBufferView());
        commandList->DrawInstanced(3, 1, 0, 0);
    }
 ここでは、まず
        auto baseDevice = App::GetBaseDevice();
        auto commandList = baseDevice->GetCommandList();
 で、デバイスクラスと、コマンドリストを取り出します。
        commandList->SetPipelineState(m_pipelineState.Get());
 で、パイプラインステートを設定します。
        commandList->IASetPrimitiveTopology(D3D_PRIMITIVE_TOPOLOGY_TRIANGLELIST);
 ここで3角形として描画するという設定を行います。
        commandList->IASetVertexBuffers(0, 1, &m_baseMesh->GetVertexBufferView());
 で頂点バッファを設定します。m_baseMesh->GetVertexBufferView()D3D12_VERTEX_BUFFER_VIEWの参照が帰りますので、そのアドレスを渡します。
 そして
        commandList->DrawInstanced(3, 1, 0, 0);
 で、描画します。パラメータは3つの頂点を1つだけの意味です。
 Scene::OnDraw()の処理が終わるとGameDevice::PopulateCommandList()関数に制御が戻ります。

GameDevice::PopulateCommandList()に戻る

 戻ったら、再びバリアを張ります。
        m_commandList->ResourceBarrier(
            1, 
            &CD3DX12_RESOURCE_BARRIER::Transition(
                m_renderTargets[m_frameIndex].Get(), 
                D3D12_RESOURCE_STATE_RENDER_TARGET, 
                D3D12_RESOURCE_STATE_PRESENT
            )
        );
 これは、バックバッファを表示用に切り替える間のバリアです。
 最後に
        ThrowIfFailed(m_commandList->Close());
 で、コマンドリストをクローズします。

GameDevice::PopulateCommandList()の終了

 PopulateCommandList()が終わるとGameDevice::OnDraw()に制御が戻ります。

コマンドリストの実行

 GameDevice::OnDraw()ではコマンドリストの実行を行います。
        // 描画用コマンドリスト実行
        ID3D12CommandList* ppCommandLists[] = { m_commandList.Get() };
        m_commandQueue->ExecuteCommandLists(_countof(ppCommandLists), ppCommandLists);
 このようにコマンドリストのポインタの配列を作成します。ここでは1つですね。
 作成したらコマンドキューでそのコマンドリストを実行します。
 ここまででバックバッファへの書き込みは終了です。
        // フロントバッファに表示
        ThrowIfFailed(GetIDXGISwapChain3()->Present(1, 0));
 で、そのバックバッファを表示用に切り替えます。
 これでこのターンでの描画処理は終了です。

次のフレームの準備

 その後
        //次のフレームに移動
        MoveToNextFrame();
 で、次のフレームに移動します。
 具体的には以下のようになっています。
    void BaseDevice::MoveToNextFrame()
    {
        // Schedule a Signal command in the queue.
        const UINT64 currentFenceValue = m_fenceValues[m_frameIndex];
        ThrowIfFailed(m_commandQueue->Signal(m_fence.Get(), currentFenceValue));

        // Update the frame index.
        m_frameIndex = GetIDXGISwapChain3()->GetCurrentBackBufferIndex();

        // If the next frame is not ready to be rendered yet, wait until it is ready.
        if (m_fence->GetCompletedValue() < m_fenceValues[m_frameIndex])
        {
            ThrowIfFailed(m_fence->SetEventOnCompletion(m_fenceValues[m_frameIndex], m_fenceEvent));
            WaitForSingleObjectEx(m_fenceEvent, INFINITE, FALSE);
        }

        // Set the fence value for the next frame.
        m_fenceValues[m_frameIndex] = currentFenceValue + 1;
    }
 ここはDirectX-Graphics-Samplesのものと同じです。

 これで更新、および描画処理の終了です。
 次項は、終了時の処理を説明します。