014.インスタンス描画(Dx12版)

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

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

 

図0014a

 


 動画は以下になります。

 

 

【サンプルのポイント】

 今項のサンプルはインスタンス描画です。インスタンス描画というのは1回の描画命令にワールド行列を複数渡すことで、複数のインスタンスの描画を実装します。通常の描画より劇的に速くなります。

【共通解説】

 Dx12、Dx11両方に共通なのはシェーダーです。DxSharedプロジェクト内にシェーダファイルというフィルタがあり、そこに記述されてます。
 今回使用するシェーダは頂点シェーダとピクセルシェーダです。VertexPositionNormalTexture型の頂点を持つものです。コンスタントバッファもあります。

 更新処理は動きは同じですが、Dx12版の更新処理で説明します。OnUpdate()関数には、更新する方法が記述されています。
 今回描画するオブジェクトは階層になってます。1つ1つの立方体は構造体でCubeObject構造体です。そして、そのインスタンスを管理するのがCubeObjectGroupクラスです。

【Dx12版解説】

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

■初期化■

 初期化は、まず頂点の配列、インデックスの配列を初期化しメッシュを作成します。インスタンス構築時のパラメータで法線の作成方法を変えます。CubeObjectGroup::CreateBuffers()関数でその処理をしています。
 メッシュ作成は前項までの例と変わりません。
 ただ今回はワールド行列の配列を作成します。CubeObjectGroup::CreateBuffers()関数の一番下に以下の記述があります。
void CubeObjectGroup::CreateBuffers() {

    //中略

    //インスタンス行列バッファの作成
    //Max値で作成する
    vector<Mat4x4> matrices(m_MaxInstance);
    for (auto& m : matrices) {
        m = Mat4x4();
    }
    //インスタンス描画用の行列のメッシュ(内容変更できる)
    m_InstanceMatrixMesh = MeshResource::CreateMeshResource(matrices, true);
}
 このようにm_InstanceMatrixMesh行列の配列を使ってメッシュを作成します。配列のサイズはm_MaxInstanceで設定されますが、サンプルでは2000になっています。
 このCubeObjectGroup::CreateBuffers() 関数CubeObjectGroup::OnCreate()関数から呼ばれますが、この関数ではメッシュを作成後、今回使用する複数の立方体を、実際に作成します。
void CubeObjectGroup::OnCreate() {
    CreateBuffers();
    //テクスチャの作成
    m_TextureResource = TextureResource::CreateTextureResource(m_TextureFileName, L"WIC");
    //インスタンス配列の作成
    for (UINT count = 0; count < 500; count++) {
        CubeObject Data;
        Data.Refresh();
        m_CubeObjectVec.push_back(Data);
    }

    //中略

}
 このようにMax値は2000ですが、実装するのは500です。
 このようにメッシュと、ワールド行列用配列の準備ができたらDX12リソースの初期化を行います。

■Dx12リソース初期化■

 DX12リソースは前回と変わりありません。

■更新処理■

 CubeObjectGroup::OnUpdate()関数です。複数の立方体(500個)のそれぞれの更新処理を行ってます
void CubeObjectGroup::OnUpdate() {
    if (m_CubeObjectVec.size() >= m_MaxInstance) {
        throw BaseException(
            L"インスタンス上限を超えてます",
            L"if(m_CubeObjectVec.size() >= m_MaxInstance)",
            L"CubeObjectGroup::OnUpdate()"
        );

    }
    float ElapsedTime = App::GetApp()->GetElapsedTime();
    for (auto& v : m_CubeObjectVec) {
        v.m_Posision += v.m_Velocity * ElapsedTime;
        Quat QtSpan(v.m_QuaternionRot, v.m_QuaternionVelocity * ElapsedTime);
        v.m_Quaternion *= QtSpan;
        v.m_Quaternion.normalize();
        if (v.m_Posision.length() >= 2.0f) {
            v.Refresh();
        }
    }
    vector<Mat4x4> MatVec;
    for (size_t i = 0; i < m_CubeObjectVec.size(); i++) {
        Mat4x4 World;
        //ワールド行列の決定
        World.affineTransformation(
            m_CubeObjectVec[i].m_Scale,         //スケーリング
            Vec3(0, 0, 0),      //回転の中心(重心)
            m_CubeObjectVec[i].m_Quaternion,        //回転角度
            m_CubeObjectVec[i].m_Posision       //位置
        );
        //転置する
        World.transpose();
        MatVec.push_back(World);
    }
    //メッシュの頂点の変更
    m_InstanceMatrixMesh->UpdateVirtex(MatVec);
}
 このように、それぞれの立方体の更新を行い、それぞれのワールド行列が決定したら、最後に、赤くなってる部分のように、m_InstanceMatrixMeshの更新を行います。

■描画処理■

 CubeObjectGroup::DrawObject()関数です。
 インスタンス描画を行いますので、これまでの描画とは少し違います。
void CubeObjectGroup::DrawObject() {
    CommandList::Reset(m_PipelineState, m_CommandList);

    m_CubeMesh->UpdateResources<VertexPositionNormalTexture>(m_CommandList);
    m_InstanceMatrixMesh->UpdateResources<Matrix4X4>(m_CommandList);
    m_TextureResource->UpdateResources(m_CommandList);

    //描画
    m_CommandList->SetGraphicsRootSignature(m_RootSignature.Get());
    ID3D12DescriptorHeap* ppHeaps[] = { m_CbvSrvUavDescriptorHeap.Get(), 
        m_SamplerDescriptorHeap.Get() };
    m_CommandList->SetDescriptorHeaps(_countof(ppHeaps), ppHeaps);

    for (size_t i = 0; i < m_GPUDescriptorHandleVec.size(); i++) {
        m_CommandList->SetGraphicsRootDescriptorTable(i, m_GPUDescriptorHandleVec[i]);
    }
    auto Dev = App::GetApp()->GetDeviceResources();
    m_CommandList->RSSetViewports(1, &Dev->GetViewport());
    m_CommandList->RSSetScissorRects(1, &Dev->GetScissorRect());

    CD3DX12_CPU_DESCRIPTOR_HANDLE rtvHandle(
        Dev->GetRtvHeap()->GetCPUDescriptorHandleForHeapStart(),
        Dev->GetFrameIndex(),
        Dev->GetRtvDescriptorSize());
    CD3DX12_CPU_DESCRIPTOR_HANDLE dsvHandle(
        Dev->GetDsvHeap()->GetCPUDescriptorHandleForHeapStart()
    );
    m_CommandList->OMSetRenderTargets(1, &rtvHandle, FALSE, &dsvHandle);

    m_CommandList->IASetPrimitiveTopology(D3D_PRIMITIVE_TOPOLOGY_TRIANGLELIST);
    m_CommandList->IASetIndexBuffer(&m_CubeMesh->GetIndexBufferView());
    //インスタンス描画は、頂点バッファを複数登録する
    const D3D12_VERTEX_BUFFER_VIEW Buf[2] = { 
        m_CubeMesh->GetVertexBufferView(),
        m_InstanceMatrixMesh->GetVertexBufferView() 
    };
    m_CommandList->IASetVertexBuffers(0, 2, Buf);

    m_CommandList->DrawIndexedInstanced(m_CubeMesh->GetNumIndicis(), 
        m_CubeObjectVec.size(), 0, 0, 0);

    //コマンドリストのクローズ
    CommandList::Close(m_CommandList);
    //デバイスにコマンドリストを送る
    Dev->InsertDrawCommandLists(m_CommandList.Get());
}
 赤くなっているところがインスタンス描画特有の箇所です。

 以上、Dx12側の説明は終わりです。

【まとめ】

 今回のインスタンス描画というのは、実は、昔からあるテクニックでDx11やDx12ではじめて対応したものではありません。シェーダもそんなに変更する必要がなく(頂点シェーダのみの変更で良い)、気軽に高速化を図れるので、知っておくと何かと便利と思います。