002.三角形の移動(Dx12版)

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

 実行結果は以下のような画面が出ます。単純な3角形が左右に移動します。

 

図0002a

 


 動画は以下になります。

 

 

【共通解説】

 Dx12、Dx11両方に共通なのはシェーダーです。DxSharedプロジェクト内にシェーダファイルというフィルタがあり、そこに記述されてます。
 今回使用するシェーダは頂点シェーダとピクセルシェーダです。VertexPositionColor型の頂点を持ち、コンスタントいバッファからの入力で、位置を変更させています。
 コンスタントバッファとは、シェーダに渡すパラメータと考えていいと思います。行列やベクトル、あるいは単なるfloat型変数なども渡すことができます。
 ですので、コンスタントバッファを作成するにはどのような変数が渡されるのかを知る必要があります。
 このサンプルの頂点シェーダは以下のようになります。
#include "INCStructs.hlsli"

cbuffer ConstantBuffer : register(b0)
{
    row_major float4x4 MatrixTransform : packoffset(c0);
    float4 Emissive : packoffset(c4);
};


PSPCInput main(VSPCInput input)
{
    PSPCInput result;

    result.position = mul(input.position, MatrixTransform);
    result.color = input.color;

    return result;
}
 ここでインクルードされているINCStructs.hlsliは頂点の型が宣言されたものです。コンスタントバッファcbuffer ConstantBufferというのが、シェーダ側のバッファです。
 ここでは、MatrixTransformという行列と、Emissiveというfloat4型の変数があります。行列のrow_majorという修飾子は、行優先という意味です。DirectXはデフォルトで行優先で操作します。ところがシェーダはデフォルトで列優先(col-major)で実行されます。そのため、通常、3D描画時などは、行列を転置させてからシェーダに入力します。
 コンスタントバッファへCPU側から渡す処理はDx12、Dx11で違いがありますので、それぞれの説明で行います。
 頂点シェーダでの処理は、頂点のインプットに行列をかけてピクセルシェーダに渡します。頂点色はそのまま渡します。
 ピクセルシェーダは以下になります。
#include "INCStructs.hlsli"

cbuffer ConstantBuffer : register(b0)
{
    row_major float4x4 MatrixTransform : packoffset(c0);
    float4 Emissive : packoffset(c4);
};


float4 main(PSPCInput input) : SV_TARGET
{
    return saturate(Emissive + input.color);
}
 ここでの処理は頂点シェーダから渡されたカラー情報に、コンスタントバッファEmissiveを足してリターンします。ピクセルシェーダのリターンはそのままバックバッファへの書き込みとなります。
 また「更新処理」も共通になります。「TriangleSprite::OnUpdate関数」には、三角形が左右に移動する記述がされています。

【Dx12版解説】

 BaseCrossDx12.slnを開くと、BaseCrossDx12というメインプロジェクトがあります。この中のCharacter.h/cppが主な記述個所になります。
 表示されている三角形はTriangleObjectクラスです。ObjectInterfaceおよびShapeInterfaceの多重継承オブジェクトです。
 今回は、SimplSample001を左右に移動させています。  さて、TriangleObjectクラスOnCreate()関数は以下のようになります。
void TriangleSprite::OnCreate() {
    //頂点を作成するための配列
    vector<VertexPositionColor> vertices = {
        { VertexPositionColor(Vec3(0.0f, 0.5f, 0.0f), Col4(1.0f,0.0f,0.0f,1.0f)) },
        { VertexPositionColor(Vec3(0.5f, -0.5f, 0.0f), Col4(0.0f, 1.0f, 0.0f, 1.0f)) },
        { VertexPositionColor(Vec3(-0.5f, -0.5f, 0.0f), Col4(0.0f, 0.0f, 1.0f, 1.0f)) },
    };
    m_TriangleMesh = MeshResource::CreateMeshResource(vertices, false);

    ///ルートシグネチャ作成
    CreateRootSignature();
    ///デスクプリタヒープ作成
    CreateDescriptorHeap();
    ///コンスタントバッファ作成
    CreateConstantBuffer();
    ///パイプラインステート作成
    CreatePipelineState();
    ///コマンドリスト作成
    CreateCommandList();
    //コンスタントバッファの更新
    UpdateConstantBuffer();
}
 ここではSimplSample001のサンプルの三角形を移動させるのにコンスタントバッファを使用します。そのため、コンスタントバッファ作成、コンスタントバッファの更新という2つの関数が追加されています。

■ルートシグネチャ作成■

 今回のサンプルではルートシグネチャコンスタントバッファつきのルートシグネチャを設定します。その記述は以下です。CreateRootSignature関数です。
    ///ルートシグネチャ作成
    void TriangleSprite::CreateRootSignature() {
        //コンスタントバッファ付ルートシグネチャ
        m_RootSignature = RootSignature::CreateCbv();
    }
 RootSignature::CreateCbv関数を追いかけていくと、以下の記述になります。
//コンスタントバッファのみ
static inline ComPtr<ID3D12RootSignature> CreateCbv() {
    auto Dev = App::GetApp()->GetDeviceResources();
    ComPtr<ID3D12RootSignature> Ret = Dev->GetRootSignature(L"Cbv");
    if (Ret != nullptr) {
        return Ret;
    }

    CD3DX12_DESCRIPTOR_RANGE ranges[1];
    ranges[0].Init(D3D12_DESCRIPTOR_RANGE_TYPE_CBV, 1, 0);
    CD3DX12_ROOT_PARAMETER rootParameters[1];
    rootParameters[0].InitAsDescriptorTable(1, &ranges[0], D3D12_SHADER_VISIBILITY_ALL);

    D3D12_ROOT_SIGNATURE_FLAGS rootSignatureFlags =
        D3D12_ROOT_SIGNATURE_FLAG_ALLOW_INPUT_ASSEMBLER_INPUT_LAYOUT;

    CD3DX12_ROOT_SIGNATURE_DESC rootSignatureDesc;
    rootSignatureDesc.Init(_countof(rootParameters), rootParameters, 0, nullptr, 
        rootSignatureFlags);

    Ret = CreateDirect(rootSignatureDesc);
    Dev->SetRootSignature(L"Cbv", Ret);
    return Ret;
}
 ここではL"Cbv"という名前のルートシグネチャがあればそれを使用します。ルートシグネチャは、オブジェクトごとに作成しても意味がないので、使いまわしできるようになっています。
 このルートシグネチャで使用するCD3DX12_DESCRIPTOR_RANGEは一つです。すなわち、シェーダにはコンスタントバッファのみインプットすればいいので1つということになります。D3D12_DESCRIPTOR_RANGE_TYPE_CBVというのはコンスタントバッファに使うレンジということです。
 シェーダーには頂点シェーダ、ピクセルシェーダ両方にコンスタントバッファをインプットするので、D3D12_SHADER_VISIBILITY_ALLという設定で、CD3DX12_ROOT_PARAMETERを作成します。

■デスクプリタヒープ作成■

 デスクプリタヒープの作成は以下のようになります。ここでもコンスタントバッファのみインプットする形で定義されています。
///デスクプリタヒープ作成
void TriangleSprite::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);
}
 ここで注目したいのは、ここでメンバ変数m_CbvSrvDescriptorHandleIncrementSizeに、デスクプリタハンドルのインクリメントサイズというのを設定しているところです。Dx12は、GPUむき出しのところがあるので、GPU内のデスクプリタヒープハンドルの値をゲームプログラマ側が意識的に設定する必要があります。このサンプルではコンスタントバッファのみインプットするので、このメンバ変数は使用しませんが、覚えておきましょう。
 続いてデスクプリタヒープを作成します。DescriptorHeap::CreateCbvSrvUavHeap(1)というのはコンスタントバッファ及びシェーダリソースに使用するデスクリタヒープを1つ作成するということです。
 その内容は以下になります。
static inline ComPtr<ID3D12DescriptorHeap> CreateCbvSrvUavHeap(UINT NumDescriptorHeap) {
    //CbvSrvデスクプリタヒープ
    D3D12_DESCRIPTOR_HEAP_DESC CbvSrvHeapDesc = {};
    CbvSrvHeapDesc.NumDescriptors = NumDescriptorHeap;
    CbvSrvHeapDesc.Type = D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV;
    CbvSrvHeapDesc.Flags = D3D12_DESCRIPTOR_HEAP_FLAG_SHADER_VISIBLE;
    return CreateDirect(CbvSrvHeapDesc);
}
 ここでは、D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAVというタイプでデスクプリタヒープを作成しています。
 TriangleSprite::CreateDescriptorHeap関数では、デスクプリタヒープを作成した後、GPU側デスクプリタヒープのハンドルの配列の作成を行います。
 これは、デスクプリタヒープはGPU側ハンドルとCPU側ハンドルをペアにして、CPU側のインプットデータをGPU側に伝える仕組みになっています。CPU側のハンドルは、コンスタントバッファの作成で設定します。
 m_GPUDescriptorHandleVecというのは、GPU側デスクプリタのハンドルの配列です。ここで設定したハンドルを保存しておきます。  GPU側デスクプリタのハンドルを作成するにはCD3DX12_GPU_DESCRIPTOR_HANDLE構造体のコンストラクタを使用します。上記の設定で、先頭位置のハンドルを作成できます。作成したハンドルは、中身は単なる小さな構造体です。ですので、m_GPUDescriptorHandleVecに保存しておくことが可能です。

■コンスタントバッファ作成■

 シェーダ側のコンスタントバッファは【共通解説】のところで説明しました。ここではDx12側のコンスタントバッファです。
 BaseCrossでは、CPU側コンスタントバッファは、オブジェクトごとに持つように設計されています。これはDx11版とは違います。ですので、TriangleSpriteクラスでも、CPU側構造体の実体を持っています。以下はTriangleSpriteクラスの宣言の一部です。このようにコンスタントバッファ構造体とそのメンバ変数が実装されています。
class TriangleSprite : public ObjectInterface, public ShapeInterface {
    //中略

    ///コンスタントバッファ
    struct SpriteConstantBuffer
    {
        Mat4x4 World;
        Col4 Emissive;
        SpriteConstantBuffer() {
            memset(this, 0, sizeof(SpriteConstantBuffer));
        };
    };
    ///コンスタントバッファデータ
    SpriteConstantBuffer m_SpriteConstantBuffer;
    ///コンスタントバッファアップロードヒープ
    ComPtr<ID3D12Resource> m_ConstantBufferUploadHeap;
    ///コンスタントバッファのGPU側変数
    void* m_pConstantBuffer{ nullptr };

    //中略
};
 このようにコンスタントバッファ関連の変数が3つあります。初期化時はそれらを初期化します。
 コンスタントバッファの初期化は、TriangleSprite::CreateConstantBuffer関数で行ってます。以下がその内容です。
///コンスタントバッファ作成
void TriangleSprite::CreateConstantBuffer() {
    auto Dev = App::GetApp()->GetDeviceResources();
    ThrowIfFailed(Dev->GetDevice()->CreateCommittedResource(
        &CD3DX12_HEAP_PROPERTIES(D3D12_HEAP_TYPE_UPLOAD),
        D3D12_HEAP_FLAG_NONE,
        &CD3DX12_RESOURCE_DESC::Buffer((sizeof(SpriteConstantBuffer) + 255) & ~255),
        D3D12_RESOURCE_STATE_GENERIC_READ,
        nullptr,
        IID_PPV_ARGS(&m_ConstantBufferUploadHeap)),
        L"コンスタントバッファ用のアップロードヒープ作成に失敗しました",
        L"Dev->GetDevice()->CreateCommittedResource()",
        L"TriangleSprite::CreateConstantBuffer()"
    );
    //コンスタントバッファのビューを作成
    D3D12_CONSTANT_BUFFER_VIEW_DESC cbvDesc = {};
    cbvDesc.BufferLocation = m_ConstantBufferUploadHeap->GetGPUVirtualAddress();
    //コンスタントバッファは256バイトにアラインメント
    cbvDesc.SizeInBytes = (sizeof(SpriteConstantBuffer) + 255) & ~255;
    //コンスタントバッファビューを作成すべきデスクプリタヒープ上のハンドルを取得
    //シェーダリソースがある場合コンスタントバッファはシェーダリソースビューのあとに設置する
    CD3DX12_CPU_DESCRIPTOR_HANDLE cbvSrvHandle(
        m_CbvSrvUavDescriptorHeap->GetCPUDescriptorHandleForHeapStart(),
        0,
        0
    );
    Dev->GetDevice()->CreateConstantBufferView(&cbvDesc, cbvSrvHandle);
    //コンスタントバッファのアップロードヒープのマップ
    CD3DX12_RANGE readRange(0, 0);
    ThrowIfFailed(m_ConstantBufferUploadHeap->Map(0, 
        &readRange, reinterpret_cast<void**>(&m_pConstantBuffer)),
        L"コンスタントバッファのマップに失敗しました",
        L"pImpl->m_ConstantBufferUploadHeap->Map()",
        L"TriangleSprite::CreateConstantBuffer()"
    );
}
 コンスタントバッファを作成するためには、まず、デバイスのCreateCommittedResource関数を使って、アップロードのためのバッファ(アップロードヒープ)を確保します。その際、確保するサイズはsizeof(SpriteConstantBuffer) + 255) & ~255のように256バイトにアラインメントしなければなりません。
 続いてコンスタントバッファのビューを作成します。
 その設定でも、256バイトにアラインメントしたサイズを設定します。また、この時にCPU側デスクプリタハンドルを作成し、コンスタントバッファのビューの作成に使用します。コンスタントバッファのビューは、アップロードヒープとデスクプリタヒープを結びつけるもの(ビュー)と考えられます。
 最後に、コンスタントバッファのアップロードヒープにメンバ変数m_pConstantBufferをマップします。
 いろいろリソースが多くてわかりにくいかもしれませんが、それぞれのかかわりは以下のような感じです
1、コンスタントバッファのためにDx12によって確保される領域はアップロードヒープである。
2、アップロードヒープとデスクプリタヒープのCPU側ハンドルを使って、コンスタントバッファビューを作成する。
3、アップロードヒープにデータをアップするために、メンバ変数のポインタをマップさせる
 このように初期設定しておくことで、メンバ変数m_pConstantBufferを介して、シェーダに渡すコンスタントバッファの内容を変更することができます。
 実際にその内容変更は、UpdateConstantBuffer関数で、以下がその内容です。
void TriangleSprite::UpdateConstantBuffer() {
    //コンスタントバッファの準備
    m_SpriteConstantBuffer.Emissive = Col4(0.0f, 0.0f, 0, 1.0f);
    Matrix4X4 mat;
    mat.TranslationFromVector(m_Pos);
    m_SpriteConstantBuffer.World = mat;
    //更新
    memcpy(m_pConstantBuffer, reinterpret_cast<void**>(&m_SpriteConstantBuffer),
        sizeof(m_SpriteConstantBuffer));

}
 このようにメンバ変数m_SpriteConstantBufferの内容を設定し、memcpy関数によって、アップロードヒープにコピーされます。コピーされたデータは、コンスタントバッファビューによってデスクプリタヒープのCPU側ハンドルと関連付けられているので、それはそのまま、デスクプリタヒープのGPU側ハンドルとペアになっているので、シェーダに入力されることになります。
 結構まどろっこしいのですが、それぞれのリソースの役割をじっくり考えると、コンスタントバッファのデータがどのようにシェーダに渡されるかが理解できると思います。
 UpdateConstantBuffer()関数は、初期設定が終わったときと、各ターンでの描画直前に呼び出されます。

■パイプラインステート作成■

 パイプラインステートは、TriangleSprite::CreatePipelineState関数で行います。
void TriangleSprite::CreatePipelineState() {
    D3D12_GRAPHICS_PIPELINE_STATE_DESC PineLineDesc;
    m_PipelineState 
    = PipelineState::CreateDefault2D<VertexPositionColor, 
    VSPCSprite, PSPCSprite>(m_RootSignature, PineLineDesc);
}
 PipelineState::CreateDefault2Dテンプレート関数により、デフォルトの2Dパイプライステートに初期化します。パラメータに、頂点の型、頂点シェーダ、ピクセルシェーダを渡します。

■シェーダクラスについて■

 ここでシェーダを管理するC++側のクラスについて説明します。VSPCSpriteVSPCSprite.hlsl頂点シェーダを管理するクラスです。シェーダクラスの作成にはマクロを使用します。メインプロジェクトのProjectShader.h/cppに記述があります。以下はProjectShader.hVSPCSpriteクラスの宣言ですが
    DECLARE_DX12SHADER(VSPCSprite)
 と記述があります。これはマクロになっていて
#define DECLARE_DX12SHADER(ShaderName) class ShaderName : \
    public Dx12Shader<ShaderName>{ \
    public: \
        ShaderName(); \
    };
 というマクロ定義に展開されます。つまり、VSPCSpriteクラスをマクロによって作成しているわけです。
 実体部についてはProjectShader.cppに記述がありますが
    IMPLEMENT_DX12SHADER(VSPCSprite, App::GetApp()->m_wstrRelativeShadersPath + L"VSPCSprite.cso")
 と記述があります。これもマクロになっていて
#define IMPLEMENT_DX12SHADER(ShaderName,CsoFilename) unique_ptr<ShaderName, ShaderName::Deleter> ShaderName::m_Ptr; \
    ShaderName::ShaderName() : \
    Dx12Shader(CsoFilename){}
 となります。ヘッダとcppの組み合わせでVSPCSpriteクラスを作成してるのがわかります。
 このシェーダクラスは、親クラスにDx12Shaderクラスを持ち、追いかけていくとコンストラクタによって渡されたシェーダファイルをコンパイルし、リソースを作成しているのがわかります。
 つまり、シェーダクラス作成には、頂点シェーダピクセルシェーダ両方とも(ほかのシェーダも)、上記マクロを通せばクラス化できます。

■コマンドリスト作成■

 コマンドリストTriangleSprite::CreateCommandList関数によって作成されます。以下が実体です。
    void TriangleSprite::CreateCommandList() {
        m_CommandList = CommandList::CreateDefault(m_PipelineState);
        CommandList::Close(m_CommandList);
    }
 このように先ほど作成したパイプライステートにより初期化されます。初期化後はクローズしておきます。

■描画処理■

 描画処理はまずTriangleSprite::OnDraw関数が呼び出されます。その中で、コンスタントバッファ更新描画関数が呼ばれます。
 描画関数はTriangleSprite::DrawObject関数です。以下が実体です。
void TriangleSprite::DrawObject() {
    auto Dev = App::GetApp()->GetDeviceResources();
    //コマンドリストのリセット
    CommandList::Reset(m_PipelineState, m_CommandList);
    //メッシュが更新されていればリソース更新
    m_TriangleMesh->UpdateResources<VertexPositionColor>(m_CommandList);
    //ルートシグネチャのセット
    m_CommandList->SetGraphicsRootSignature(m_RootSignature.Get());
    //デスクプリタヒープのセット
    ID3D12DescriptorHeap* ppHeaps[] = { m_CbvSrvUavDescriptorHeap.Get() };
    m_CommandList->SetDescriptorHeaps(_countof(ppHeaps), ppHeaps);
    //GPUデスクプリタヒープハンドルのセット
    for (size_t i = 0; i < m_GPUDescriptorHandleVec.size(); i++) {
        m_CommandList->SetGraphicsRootDescriptorTable(i, m_GPUDescriptorHandleVec[i]);
    }

    m_CommandList->RSSetViewports(1, &Dev->GetViewport());
    m_CommandList->RSSetScissorRects(1, &Dev->GetScissorRect());

    //レンダーターゲットビューのハンドルを取得
    CD3DX12_CPU_DESCRIPTOR_HANDLE rtvHandle = Dev->GetRtvHandle();
    //デプスステンシルビューのハンドルを取得
    CD3DX12_CPU_DESCRIPTOR_HANDLE dsvHandle = Dev->GetDsvHandle();
    //取得したハンドルをセット
    m_CommandList->OMSetRenderTargets(1, &rtvHandle, FALSE, &dsvHandle);

    m_CommandList->IASetPrimitiveTopology(D3D_PRIMITIVE_TOPOLOGY_TRIANGLELIST);
    m_CommandList->IASetVertexBuffers(0, 1, &m_TriangleMesh->GetVertexBufferView());
    m_CommandList->DrawInstanced(m_TriangleMesh->GetNumVertices(), 1, 0, 0);

    //コマンドリストのクローズ
    CommandList::Close(m_CommandList);
    //デバイスにコマンドリストを送る
    Dev->InsertDrawCommandLists(m_CommandList.Get());
}
 コメントに記述してある通りですが、注意したいのはデスクプリタヒープのセット関連です。デスクプリタヒープによって、シェーダにコンスタントバッファが入力されます。ですので、初期化時に設定しておいたGPUデスクプリタヒープハンドルをパイプラインに設定しなければなりません。
 あとはおおむね前項のサンプルと同じかと思います。

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


【まとめ】

 このように「Dx12版」は、明らかにゲーム制作側で記述しなければならない箇所が多くあるのがわかります。
 しかしながら、慣れてくると、あるいは、描画処理をクラス化するなどで共有できる箇所が増えていくと、「Dx12版」のほうが自由度が高く、GPUの仕組みを理解するのに役に立つと思います。