【第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();
    }
 
コマンドリストを集める
 最初に呼ばれる
 は
コマンドリストを集める処理です。以下の様な内容です。
    // 描画のためのコマンドリストを集める
    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のものと同じです。
 これで
更新、および描画処理の終了です。
 次項は、終了時の処理を説明します。