004.複数の四角形の描画(Dx12版)

 このサンプルはSimplSample004というディレクトリに含まれます。
 BaseCrossDx12.slnというソリューションを開くとDx12版が起動します。

 実行結果は以下のような画面が出ます。

 

図0004a

 


 動画は以下になります。

 

 

【共通解説】

 Dx12、Dx11両方に共通なのはシェーダーです。DxSharedプロジェクト内にシェーダファイルというフィルタがあり、そこに記述されてます。
 今回使用するシェーダは頂点シェーダとピクセルシェーダです。VertexPositionColor型の頂点を持ち、コンスタントバッファからの入力で、位置を変更させています。
 SimpleSample002と同じシェーダです。

 更新処理は動きは同じですが、Dx12版の更新処理で説明します。SquareSpriteGroup::OnUpdate関数には、それぞれの四角形をグループ化して個別に更新する方法が記述されています。

【Dx12版解説】

 BaseCrossDx12.slnを開くと、BaseCrossDx12というメインプロジェクトがあります。この中のCharacter.h/cppが主な記述個所になります。

■初期化■

 複数の四角形を描画するにあたって、個別に別々のルートシグネチャやパイプライステートを作成するのは効率が悪く、GPUにも負担がかかります。ですので、個別の最適化が必要でしょう。
 今回のサンプルでは、Dx12版においてはコンスタントバッファをどう設計するかがポイントになります。
 個別の四角形はSquareSprite構造体です。クラスで作成してもいいのですが、単なるデータのかたまりを強調するために構造体にしてあります。とはいえ、メンバとしてデスクプリタヒープとコンスタントバッファ関連を持っています。
 それに対して、それぞれの四角形をまとめるクラスとしてSquareSpriteGroupクラスがあります。こちらはゲームに配置されるオブジェクトです。そしてSquareSprite構造体の配列を持ちます。
 ルートシグネチャ、パイプラインステート、コマンドリスト、メッシュリソースSquareSpriteGroupクラスに持ちます。
 初期化は、SquareSprite構造体の配列及びSquareSpriteGroupクラスを初期化します。それはまずSquareSpriteGroup::OnCreate()関数から始まります。
void SquareSpriteGroup::OnCreate() {
    float HelfSize = 0.5f;
    //頂点配列
    vector<VertexPositionColor> vertices = {
        { VertexPositionColor(Vec3(-HelfSize, HelfSize, 0), Col4(1.0f, 0.0f, 0.0f, 1.0f)) },
        { VertexPositionColor(Vec3(HelfSize, HelfSize, 0), Col4(0.0f, 1.0f, 0.0f, 1.0f)) },
        { VertexPositionColor(Vec3(-HelfSize, -HelfSize, 0), Col4(0.0f, 0.0f, 1.0f, 1.0f)) },
        { VertexPositionColor(Vec3(HelfSize, -HelfSize, 0), Col4(1.0f, 0.0f, 1.0f, 1.0f)) },
    };
    //インデックス配列
    vector<uint16_t> indices = { 0, 1, 2, 1, 3, 2 };
    //メッシュの作成
    //頂点変更できない
    m_SquareSpriteMesh = MeshResource::CreateMeshResource(vertices, indices, false);
    //グループの配列の作成
    m_SquareSpriteVec.assign(100, SquareSprite());
    for (auto& v : m_SquareSpriteVec) {
        v.m_LocalRot = Util::RandZeroToOne(true);
        v.m_LocalRotVelocity = Util::RandZeroToOne(true) * 20.0f - 10.0f;
        v.m_LocalPos = Vec2(0, 0);
        v.m_LocalVelocity = Vec2(Util::RandZeroToOne(true) * 200.0f - 100.0f,
             100 + Util::RandZeroToOne(true) * 100.0f);
        v.m_LocalGravityVelocity = Vec2(0, 0);
    }
    ///ルートシグネチャ作成
    CreateRootSignature();
    ///デスクプリタヒープ作成
    CreateDescriptorHeap();
    ///コンスタントバッファ作成
    CreateConstantBuffer();
    ///パイプラインステート作成
    CreatePipelineState();
    ///コマンドリスト作成
    CreateCommandList();

}
 初期化処理は、まず、メッシュリソースの初期化から始まります。これは、前のサンプルと変わりません。
 その後、グループの配列の作成を行います(赤くなっています)。
 このm_SquareSpriteVec.assign(100, SquareSprite())で、100個のSquareSprite構造体が初期化されます。vectorのassign関数最初期化を意味し、2番目の引数でコンストラクタを呼び出してくれます(実際にはインスタンスの参照を渡すのですが、コンストラクタ呼び出しで構築したインスタンスで問題ありません)。
 SquareSprite()コンストラクタ呼び出しによって実行されるのは、SquareSprite::SquareSprite()関数です。ここでは、m_LocalScale、m_LocalPosなどのそれぞれの四角形が管理する情報をデフォルト初期化します。
 実際にそれらの値を意味のある値に代入するのは、次の
for (auto& v : m_SquareSpriteVec) {
    v.m_LocalRot = Util::RandZeroToOne(true);
    v.m_LocalRotVelocity = Util::RandZeroToOne(true) * 20.0f - 10.0f;
    v.m_LocalPos = Vec2(0, 0);
    v.m_LocalVelocity = Vec2(Util::RandZeroToOne(true) * 200.0f - 100.0f,
         100 + Util::RandZeroToOne(true) * 100.0f);
    v.m_LocalGravityVelocity = Vec2(0, 0);
}
 という記述です。この処理により、各四角形(100個)の初期値が代入されます。

■Dx12リソースの初期化■

 さて、Dx12リソースの初期化ですが、例によってルートシグネチャ、デスクプリタヒープ、コンスタントバッファ、パイプラインステート、コマンドリストの順で初期化するわけですが、今回のサンプルでは各四角形に持たせるリソースがあるため内容が違います。
 前サンプルと内容が違うのはデスクプリタヒープとコンスタントバッファです。
 まずデスクプリタヒープですが、SquareSpriteGroup側では以下の記述になってます。
///デスクプリタヒープ作成
void SquareSpriteGroup::CreateDescriptorHeap() {
    for (auto& v : m_SquareSpriteVec) {
        v.CreateDescriptorHeap();
    }
}
 このように、m_SquareSpriteVec拡張for文でループし各四角形のCreateDescriptorHeap()関数を呼び出しています。各四角形におけるCreateDescriptorHeap()関数は以下です。
void SquareSprite::CreateDescriptorHeap() {
    auto Dev = App::GetApp()->GetDeviceResources();
    m_CbvSrvDescriptorHandleIncrementSize
    = Dev->GetDevice()->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV);
    //CbvSrvデスクプリタヒープ(コンスタントバッファのみ)
    m_CbvSrvUavDescriptorHeap = DescriptorHeap::CreateCbvSrvUavHeap(1);
    //GPU側デスクプリタヒープのハンドルの配列の作成
    m_GPUDescriptorHandleVec.clear();
    CD3DX12_GPU_DESCRIPTOR_HANDLE CbvHandle(
        m_CbvSrvUavDescriptorHeap->GetGPUDescriptorHandleForHeapStart(),
        0,
        0
    );
    m_GPUDescriptorHandleVec.push_back(CbvHandle);
}
 デスクプリタヒープを各四角形に持たせるわけはコンスタントバッファアップロードヒープデスクプリタヒープに結び付けなければならないためです。
 この関数は内容的には前サンプルの四角形と同じです。
 つづいてコンスタントバッファですが、こちらもSquareSpriteGroup側では以下の記述になってます。
    ///コンスタントバッファ作成
    void SquareSpriteGroup::CreateConstantBuffer() {
        for (auto& v : m_SquareSpriteVec) {
            v.CreateConstantBuffer();
        }
    }
 これもCreateDescriptorHeap()関数同様、各四角形のCreateConstantBuffer()関数を呼び出しています。呼び出した先は、前サンプルのコンスタントバッファを作成するのと同じなので省略します。

 デスクプリタヒープとコンスタントバッファ以外は、SquareSpriteGroup側に持ちます。

■更新処理■

 更新処理SquareSpriteGroup::OnUpdate()関数になります。
    void SquareSpriteGroup::OnUpdate() {
        float ElapsedTime = App::GetApp()->GetElapsedTime();
        UpdateObjects(ElapsedTime);
    }
 ここでは前回のターンからの経過時間を取得してUpdateObjects()関数を呼び出しています。以下がUpdateObjects()関数です。
//各オブジェクトの位置等の変更
void SquareSpriteGroup::UpdateObjects(float ElapsedTime) {
    float h = static_cast<float>(App::GetApp()->GetGameHeight());
    h /= 2.0f;
    for (auto& v : m_SquareSpriteVec) {
        if (v.m_LocalPos.y < -h) {
            v.m_LocalRot = Util::RandZeroToOne(true);
            v.m_LocalRotVelocity = Util::RandZeroToOne(true) * 20.0f - 10.0f;
            v.m_LocalPos = Vec2(0, 0);
            v.m_LocalVelocity = Vec2(Util::RandZeroToOne(true) * 200.0f - 100.0f,
                 100 + Util::RandZeroToOne(true) * 100.0f);
            v.m_LocalGravityVelocity = Vec2(0, 0);
        }
        else {
            v.m_LocalRot += v.m_LocalRotVelocity * ElapsedTime;
            v.m_LocalPos += v.m_LocalVelocity * ElapsedTime;
            v.m_LocalGravityVelocity += Vec2(0, -98.0f) * ElapsedTime;
            v.m_LocalPos += v.m_LocalGravityVelocity * ElapsedTime;
        }
    }
}
 ここでの処理は、2パターンあります。ある一定Y位置より上にある場合そうでない場合です。
 ある一定Y位置より上にある場合各四角形の速度と落下加速度を加味して、位置を決定します。回転もかけます。そうでない場合は、乱数などを利用して初期化と同じ処理を行います。この処理によって、オブジェクトが繰り返し噴水のように上に飛ばされるような動きになります。(このアルゴリズムはDx11版も同じです)

■描画処理■

 描画処理は、コマンドリスト、ルートシグネチャ、パイプライステートの描画準備が済んだところで、実際の描画は各四角形からデータを取り出して描画します。以下の赤くなっているところが、各四角形の処理をするところです。
void SquareSpriteGroup::DrawObject() {

    //中略

    //各スプライトごとの処理
    for (auto& v : m_SquareSpriteVec) {
        //デスクプリタヒープのセット
        ID3D12DescriptorHeap* ppHeaps[] = { v.m_CbvSrvUavDescriptorHeap.Get() };
        m_CommandList->SetDescriptorHeaps(_countof(ppHeaps), ppHeaps);
        //GPUデスクプリタヒープハンドルのセット
        for (size_t i = 0; i < v.m_GPUDescriptorHandleVec.size(); i++) {
            m_CommandList->SetGraphicsRootDescriptorTable(i, v.m_GPUDescriptorHandleVec[i]);
        }
        //インデックス描画
        m_CommandList->DrawIndexedInstanced(m_SquareSpriteMesh->GetNumIndicis(), 1, 0, 0, 0);
    }

    //中略
}
 以上、Dx12側の説明は終わりです。


【まとめ】

 今回は複数のインスタンスの描画を行いました。ただ今回のように、ワールド行列のみ違うオブジェクトの複数描画であればSimplSample014で紹介するインスタンス描画のほうが効率が良いかもしれません。
 いろんなケースに応じて今回のサンプルのような方法をとるか、インスタンス描画をとるかを検討してみるといいと思います。