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

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

1201.複数のオブジェクトの描画(1)

 今項から、1201.複数のオブジェクトの描画の解説をしたいと思います。
 ソリューションはSimpleSample102内にあります、BaseDx12.slnです。
 このファイルをVS2019で開き、ビルド-ソリューションのリビルドでビルドしましょう。

まず実行してみる

 ビルドが終わったら、デバッグ-デバッグなしで開始を選択します。すると、以下の画面が現れます。

 

図1201a

 

 3つのオブジェクト(2つの三角形と1つの四角形)が表示され、1つの三角形と四角形は左か右に移動します。
 右のウインドウ枠の外に出ると左からまた出てきます。

オブジェクトごとのパラメータ

 ゲーム画面上には、いろんなオブジェクトが配置されます。プレイヤーや敵キャラ、あるいは、障害物や、ライフなどをあらわすインターフェイスなど。
 それらを1画面に同時に出現させるには、それぞれの大きさや位置情報などを別々に管理しなければなりません。
 BaseDx12シンプルバージョンでは、それらを管理する仕組みは、直接は提供しませんが、効率よく書くための環境は用意されています。
 サンプルSimpleSample001でも紹介したようなシーンの仕組みです。
 Scene.h/cppあるいは、同じようなセットでPlayer.h/cppなどのファイルを用意し、キャラクター別のクラスを記述することで、複数のオブジェクトを画面上に表示させることができます。
 この項からは、それらをDx12で実現するための方法としてコンスタントバッファを記述し、それをDx12に結び付ける方法を紹介したいと思います。

デバイスの準備

 SimpleSample001と同じようにデバイスの初期化からはじめます。
 エントリポイントのWinMainが含まれるWinMain.cppの記述は変わりません。
 まずソリューションエクスプローラを開いてGameDevice.cppを開きましょう。
 その中のGameDevice::OnInit()関数を見てください。
 1102.シンプルな三角形の描画(2)で説明したように、
    void GameDevice::OnInit()
    {
        LoadPipeline();
        LoadAssets();
    }
 という記述になっています。LoadPipeline()関数は共通のオブジェクトの初期化、LoadAssets()関数は、個別のオブジェクトの初期化、の意味合いです。

LoadPipeline()関数

 LoadPipeline()関数から見ていきます。
 SimpleSample001と同じ処理の場合は、説明は省略します。場合によってはそれぞれの解説ページへのリンクも用意しますので、そちらを参考にしてください。
 LoadPipeline()関数は、以下の様な内容です。赤くなっている部分が、今回のサンプルで新たに付け足された部分です。
    void GameDevice::LoadPipeline()
    {
        //ファクトリ
        ComPtr<IDXGIFactory4> factory = Dx12Factory::CreateDirect();
        //デバイス
        m_device = D3D12Device::CreateDefault(factory, m_useWarpDevice);
        //コマンドキュー
        m_commandQueue = CommandQueue::CreateDefault();
        //スワップチェーン
        m_swapChain = SwapChain::CreateDefault(factory, m_commandQueue, m_frameCount);
        m_frameIndex = m_swapChain->GetCurrentBackBufferIndex();
        // デスクプリタヒープ
        {
            // レンダリングターゲットビュー
            m_rtvHeap = DescriptorHeap::CreateRtvHeap(m_frameCount);
            m_rtvDescriptorHandleIncrementSize 
                = m_device->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_RTV);
            //CbvSrvUavデスクプリタヒープ
            //(コンスタントバッファとシェーダリソースと順序不定のアクセスビュー)
            //CbvSrvUavデスクプリタヒープの数はGetCbvSrvUavMax()により取得する
            m_cbvSrvUavDescriptorHeap = DescriptorHeap::CreateCbvSrvUavHeap(GetCbvSrvUavMax());
            m_cbvSrvUavDescriptorHandleIncrementSize
            = m_device->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV);
            //サンプラーデスクリプタヒープ
            m_samplerDescriptorHeap = DescriptorHeap::CreateSamplerHeap(1);
        }
        // RTVとコマンドアロケータ
        CD3DX12_CPU_DESCRIPTOR_HANDLE rtvHandle(m_rtvHeap->GetCPUDescriptorHandleForHeapStart());
        for (UINT n = 0; n < m_frameCount; n++)
        {
            ThrowIfFailed(m_swapChain->GetBuffer(n, IID_PPV_ARGS(&m_renderTargets[n])));
            m_device->CreateRenderTargetView(m_renderTargets[n].Get(), nullptr, rtvHandle);
            rtvHandle.Offset(1, m_rtvDescriptorHandleIncrementSize);
            //コマンドアロケータ
            m_commandAllocators[n] = CommandAllocator::CreateDefault();
        }
    }
 ファクトリ、デバイス、コマンドキュー、スワップチェーンまでは、前サンプルと変わりません。
 デスクプリタヒープは今回のサンプルではCbvSrvUavデスクプリタヒープサンプラーデスクプリタヒープという新たなデスクプリタヒープが加わります。
 このサンプルは、その中でもCbvSrvUavデスクプリタヒープが大きなテーマです。

CbvSrvUavデスクプリタヒープ

 さて、以下の表を見てください。1105.シンプルな三角形の描画(5)で説明した、デスクプリタヒープの概念図です。

 

図1105a(再)

 

 デスクプリタヒープは、GPUとやり取りするビューを引き出しにまとめたようなもの、と考えられます。
 RTV(レンダーターゲットビュー)は、フレーム数と同じ数だけ作成しました。デプスステンシル用は、今回は出てきませんが、通常1つです。Sampler(サンプラー)は、影を使ったりする場合は2つ以上、今回は1つ定義します。
 そこまではわかりやすいのですが、CbvSrvUavデスクプリタヒープはいったいいくつ必要なのでしょうか?
 Dx12はGPUに対する処理を、柔軟に設定できるよう設計されていますが、BaseDx12では、CbvSrvUavデスクプリタヒープできるだけ多く作成します。デフォルトは1024個です。
 シンプルバージョンの場合はシーンという単位が1つの基準となるので。ゲーム全体で1024個CbvSrvUavデスクプリタヒープが用意されています。
 もしこの値を変更したければ、GameDeviceのコンストラクタ
    GameDevice::GameDevice(UINT frameCount) :
        Dx12Device(frameCount,2048)
    {
    }
 のように記述します。GameDeviceの親クラスのDx12Deviceクラスは、2つめの引数にCbvSrvUavの数を渡します(デフォルトが1024ということです)。この数はすなわちCbvSrvUavデスクプリタヒープの上限数になります。

コンスタントバッファビュー

 CbvSrvUavCbvコンスタントバッファビューの意味です。コンスタントバッファ定数バッファとも訳されますが定数というよりはシェーダーに渡す変数という意味合いが強いと思います。
 シェーダに渡す情報は、基本的に頂点のデータですからローカル座標になります。それをワールド座標に変換してさらにはカメラの視点に変換するのが頂点シェーダの役割です。
 そのワールド座標に変換するためのワールド行列、そしてカメラ視点に変換するためのビュー行列射影行列をシェーダに渡す領域がコンスタントバッファです。
 ですからコンスタントバッファビューオブジェクト単位あるいはメッシュ単位で違う内容になると考えて差し支えありません。
 そうした場合、ゲーム上に配置されるオブジェクトは、オブジェクトごとにコンスタントバッファビューが必要なのがわかります。
 Dx11の場合は、それぞれのオブジェクトの描画で一回完結するので、コンスタントバッファは使いまわしができます。極端な話1個のコンスタントバッファを使って、1つのオブジェクトを描画し、そして次のオブジェクトの描画でそのコンスタントバッファを使うことができます。
 しかしDx12の場合はコマンドリストという形で、複数の描画命令をため込んで一気に描画するという手法を取ります。そのため、コンスタントバッファビューは複数(描画単位で)必要になります。(BaseDx12はそういう設計になっています)

シェーダーリソースビュー

 CbvSrvUavSrvシェーダーリソースビューです。これはテクスチャと思っていいと思います。CbvSrvUavデスクプリタヒープコンスタントバッファのほかにテクスチャも同じグループで管理します。
 そういうわけで、前述したCbvSrvUavの数テクスチャの数も加味して決める必要があります。
 モデルによっては複数のテクスチャを持つ場合もあるので、コンスタントバッファほどではありませんが、ある程度の数は過去穂しなければなりません。

アンオーダードアクセスビュー

 CbvSrvUavUavアンオーダードアクセスビュー(順序不定のアクセスビュー)です。
 この領域はシェーダとのデータのやり取りに使用できるエリアで、コンスタントバッファやシェーダリソースより、自由度が高いです。
 このこのサンプルでは使用しません。

CbvSrvUavデスクプリタヒープの作成

 このような背景があるので、GameDevice::LoadPipeline()関数ではまず
    m_cbvSrvUavDescriptorHeap = DescriptorHeap::CreateCbvSrvUavHeap(GetCbvSrvUavMax());
 という形でCbvSrvUavデスクプリタヒープを作成します。
 この呼び出しは、以下のように展開されます
namespace DescriptorHeap {
    static inline ComPtr<ID3D12DescriptorHeap> CreateDirect(const D3D12_DESCRIPTOR_HEAP_DESC& desc) {
        auto device = App::GetID3D12Device();
        ComPtr<ID3D12DescriptorHeap> ret;
        ThrowIfFailed(device->CreateDescriptorHeap(&desc, IID_PPV_ARGS(&ret)),
            L"デスクプリタヒープの作成に失敗しました",
            L"device->CreateDescriptorHeap()",
            L"DescriptorHeap::CreateDirect()"
        );
        return ret;
    }
//中略
    static inline ComPtr<ID3D12DescriptorHeap> CreateCbvSrvUavHeap(UINT numDescriptorHeap) {
        //CbvSrvUavデスクプリタヒープ
        D3D12_DESCRIPTOR_HEAP_DESC cbvSrvUavHeapDesc = {};
        cbvSrvUavHeapDesc.NumDescriptors = numDescriptorHeap;
        cbvSrvUavHeapDesc.Type = D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV;
        cbvSrvUavHeapDesc.Flags = D3D12_DESCRIPTOR_HEAP_FLAG_SHADER_VISIBLE;
        return CreateDirect(cbvSrvUavHeapDesc);
    }
//中略
}
 ここでnumDescriptorHeapはデフォルトで1024ということがわかります。
 続いて、1つのCbvSrvUavデスクプリタヒープのサイズ、をもとめます。上記概念図によりますと、1つの引き出しの大きさ、のようなものです。
    m_cbvSrvUavDescriptorHandleIncrementSize
    = m_device->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV);

サンプラーデスクプリタヒープの作成

 サンプラーは、シェーダリソースをサンプリングするのに必要なリソースです。今回は1つ用意します(四角形のテクスチャのサンプリングに使用します)。
    //サンプラーデスクリプタヒープ
    m_samplerDescriptorHeap = DescriptorHeap::CreateSamplerHeap(1);

RTVとコマンドアロケータ

  RTVとコマンドアロケータは、SimpleSample101と変わりません。
 ただしScene::OnInit()関数
    void Scene::OnInit() {
        //フレーム数は3
        ResetActiveBaseDevice<GameDevice>(3);
    }
 と記述していますので、今回のフレーム数(RTVとコマンドアロケータの数)はそれぞれ3個になります。
 ここまでで、LoadPipeline();の処理は終了です。