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

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

1107.シンプルな三角形の描画(7)

 前項に引き続きGameDevice::LoadAssets()関数です。

コマンドリスト

 Dx12では、GPUに対する命令群(メッシュの作成なども含む)をコマンドリストという、コマンド(命令)の集合としてためておき、一気に実行するという処理方法を行います。
 すでに作成したコマンドアロケータは、このコマンドリストを作成するためのものです。
 GameDevice::LoadAssets()関数ではルートシグネチャの作成に続き、リソース構築用のコマンドリストを作成します。コードは以下です。
    m_commandList = CommandList::CreateSimple(m_commandAllocators[m_frameIndex]);
 コマンドアロケータは、フレームごとに作成したので、コマンドアロケータ配列の現在のインデックスのオブジェクトを使用します。
 このコードは以下のように展開されます。赤くなっている部分です。
    namespace CommandList {
        static inline  ComPtr<ID3D12GraphicsCommandList> 
        CreateSimple(const ComPtr<ID3D12CommandAllocator>& allocator) {
            //デバイスの取得
            auto device = App::GetID3D12Device();
            ComPtr<ID3D12GraphicsCommandList> ret;
            ThrowIfFailed(device->CreateCommandList(
                0,
                D3D12_COMMAND_LIST_TYPE_DIRECT,
                allocator.Get(),
                nullptr,
                IID_PPV_ARGS(&ret)),
                L"コマンドリストの作成に失敗しました",
                L"device->GetDevice()->CreateCommandList()",
                L"CommandList::CreateSimple()"
            );
            return ret;
        }
//中略
    }
 このように作成されたコマンドリストは、リソース構築用のものです。描画用のは、また後程作成します。

リソースの構築

 今回のサンプルで作成するのは、単純な三角形です。3つの頂点を持ちます。位置情報のほかに、色情報も持ちます。
 BaseDxではシーンという概念を持っています。デバイスシーンを分けることで、仕事の分担を明確にしています。
 ここまでの処理はデバイスの役割です。ここから先はシーンに制御を渡します。
        App::GetSceneBase().OnInitAssets();
 App::GetSceneBase()で、シーンを所得します(厳密にはSceneクラスの親クラス、SceneBaseクラスのインスタンス)。
 このクラスにはOnInitAssets()という仮想関数があり、この呼び出してScene::OnInitAssets()関数が呼び出されます。

Scene::OnInitAssets()関数

 さて、ここからはScene.cppにある、Scene::OnInitAssets()関数の処理になります。以下が全体像です。
    void Scene::OnInitAssets() {
        //ここでシーン上のオブジェクトを構築
        //デバイスの取得
        auto baseDevice = App::GetBaseDevice();
        // 2Dの基本的なパイプライン
        {
            D3D12_GRAPHICS_PIPELINE_STATE_DESC PipeLineDesc;
            m_pipelineState
            = PipelineState::CreateDefault2D<VertexPositionColor, VSPCSprite, PSPCSprite>(
                baseDevice->GetRootSignature(), PipeLineDesc
            );
        }
        // メッシュ
        {
            auto aspectRatio = baseDevice->GetAspectRatio();
            vector<VertexPositionColor> vertex =
            {
                { Float3(0.0f, 0.25f * aspectRatio, 0.0f), Float4(1.0f, 0.0f, 0.0f, 1.0f) },
                { Float3(0.25f, -0.25f * aspectRatio, 0.0f), Float4(0.0f, 1.0f, 0.0f, 1.0f) },
                { Float3(-0.25f, -0.25f * aspectRatio, 0.0f), Float4(0.0f, 0.0f, 1.0f, 1.0f) }
            };
            //メッシュ作成
            m_baseMesh = BaseMesh::CreateBaseMesh<VertexPositionColor>(vertex);
        }
    }
 ここで参照されているm_pipelineStateおよびm_baseMeshは、Scene.hに宣言されているものです。以下はScene.hSceneクラスです。
    class Scene :public SceneBase {
        ComPtr<ID3D12PipelineState> m_pipelineState;
        shared_ptr<BaseMesh> m_baseMesh;
    public:
        Scene() :SceneBase() {}
        virtual ~Scene() {}
        virtual void OnInit()override;
        virtual void OnInitAssets()override;
        virtual void OnUpdate()override;
        virtual void OnDraw()override;
        virtual void OnDestroy()override;
    };
 このようにm_pipelineStateID3D12PipelineStateインターフェイスですが、m_baseMeshBaseMeshクラスshared_ptrになっています。
 BaseMeshクラスについては、後ほど解説します。
 Scene::OnInitAssets()関数に戻りますが、まず
        auto baseDevice = App::GetBaseDevice();
 により、デバイスを取得します。

パイプラインステート

 続いて
        // 2Dの基本的なパイプライン
        {
            D3D12_GRAPHICS_PIPELINE_STATE_DESC PipeLineDesc;
            m_pipelineState
            = PipelineState::CreateDefault2D<VertexPositionColor, VSPCSprite, PSPCSprite>(
                baseDevice->GetRootSignature(), PipeLineDesc
            );
        }
 により、2D用のパイプライステートを作成します。CreateDefault2D()関数は、テンプレート関数で、以下のように展開されます。赤くなっている部分です。
namespace PipelineState {
    static inline ComPtr<ID3D12PipelineState> CreateDirect(const D3D12_GRAPHICS_PIPELINE_STATE_DESC& desc) {
        //デバイスの取得
        auto device = App::GetID3D12Device();
        ComPtr<ID3D12PipelineState> ret;
        ThrowIfFailed(device->CreateGraphicsPipelineState(&desc, IID_PPV_ARGS(&ret)),
            L"パイプラインステートの作成に失敗しました",
            L"device->GetDevice()->CreateGraphicsPipelineState()",
            L"PipelineState::CreateDirect()"
        );
        return ret;
    }
    template<typename Vertex, typename VS, typename PS>
    static inline ComPtr<ID3D12PipelineState> 
    CreateDefault2D(
        const ComPtr<ID3D12RootSignature>& rootSignature, 
        D3D12_GRAPHICS_PIPELINE_STATE_DESC& retDesc) {
        ZeroMemory(&retDesc, sizeof(retDesc));
        retDesc.InputLayout = { Vertex::GetVertexElement(), Vertex::GetNumElements() };
        retDesc.pRootSignature = rootSignature.Get();
        retDesc.VS = CD3DX12_SHADER_BYTECODE(
            VS::GetPtr()->GetShaderComPtr()->GetBufferPointer(),
            VS::GetPtr()->GetShaderComPtr()->GetBufferSize()
        );
        retDesc.PS = CD3DX12_SHADER_BYTECODE(
            PS::GetPtr()->GetShaderComPtr()->GetBufferPointer(), 
            PS::GetPtr()->GetShaderComPtr()->GetBufferSize()
        );
        retDesc.RasterizerState = CD3DX12_RASTERIZER_DESC(D3D12_DEFAULT);
        retDesc.BlendState = CD3DX12_BLEND_DESC(D3D12_DEFAULT);
        retDesc.DepthStencilState.DepthEnable = FALSE;
        retDesc.DepthStencilState.StencilEnable = FALSE;
        retDesc.SampleMask = UINT_MAX;
        retDesc.PrimitiveTopologyType = D3D12_PRIMITIVE_TOPOLOGY_TYPE_TRIANGLE;
        retDesc.NumRenderTargets = 1;
        retDesc.RTVFormats[0] = DXGI_FORMAT_R8G8B8A8_UNORM;
        retDesc.SampleDesc.Count = 1;
        return CreateDirect(retDesc);
    }
//中略
}
 パイプラインステートは、そのオブジェクトの描画に使うシェーダー、頂点の型、ラスタライザ、ブレンド、サンプラー各ステートなどを定義するものです。
 この設定はかなり細かく設定することが可能です。ここでは、関数の引数に
D3D12_GRAPHICS_PIPELINE_STATE_DESC& retDesc
 というパラメータを渡しています。これは、作成したパイプライステートの定義構造体を再利用できるためのものです。上記のようにして作成したパイプライステートの定義構造体の一部を修正して、別のパイプラインステートを作成することが可能になります。この場合、再利用して作成した定義構造体はPipelineState::CreateDirect()関数によって、作成することが可能になります。
 CreateDefault2D()テンプレート関数テンプレート引数は、
template<typename Vertex, typename VS, typename PS>
 です。Vertex頂点型VS頂点シェーダクラス型PSピクセルシェーダクラス型です。
 VertexはここではVertexPositionColor型です。位置情報と色情報を保持する型です。
 VSVSPCSprite型で、これはBaseDx12独特のものです。

BaseDx12のシェーダの扱い

 ここでBaseDx12のシェーダの扱いについて説明します。
 BaseDx12では、固有のシェーダクラスを持ちます。
 シェーダは.hlslファイルをそのまま読み込む方法、と.csoファイルを読み込む方法があるかと思います(ほかにもありますが、まあ、代表的なのはこの2つ)。
 .hlslファイルは、シェーダのソースコードなので、中身的には普通のテキストファイルなので、あんまり安全ではありません。ゲームを遊ぶ人が、直接書き換えることができます。
 .csoファイルは、VisualStudioによってコンパイルされたバイナリファイルで、だからと言って、決して万全ではありませんが.hlslファイルよりはましです。ですのでBaseDx12では.csoファイルを読み込む形になってます。
 BaseDx12のシェーダクラスは、ヘッダ部と実装部で作成します。
 クラスを作り出すマクロが用意されています。
 Scene.hを見てください。上方に
    DECLARE_DX12SHADER(VSPCSprite)
    DECLARE_DX12SHADER(PSPCSprite)
 という記述があります。これはVSPCSpriteクラスあるいはPSPCSpriteクラスのヘッダ部を作り出すマクロです。
 例えばVSPCSpriteクラスはどのように展開されるかというと
class VSPCSprite : public Dx12Shader<VSPCSprite>{
    public:
        VSPCSprite();
};
 というクラス宣言になります。Dx12Shaderテンプレートクラスを継承したクラスです。
 実体部はというとScene.cppに記載があり
    IMPLEMENT_DX12SHADER(VSPCSprite, App::GetShadersPath() + L"vshader.cso")
    IMPLEMENT_DX12SHADER(PSPCSprite, App::GetShadersPath() + L"pshader.cso")
 という形になります。VSPCSpriteの展開は
unique_ptr<VSPCSprite, VSPCSprite::Deleter> VSPCSprite::m_Ptr;

VSPCSprite::VSPCSprite() : 
        Dx12Shader(CsoFilename){}
 という形になります。
 親クラスであるDx12Shaderテンプレートクラスを調べればわかりますが、各シェーダクラスはシングルトンとして作成され、シェーダクラスは、一つのインスタンスを再利用する形となっています。また、使わないシェーダはインスタンスも作成しません。
 親クラスでは、渡された.csoファイルを展開し、パイプラインステートで使える準備をします。

パイプラインステートにシェーダを渡す

 このようにしてクラス化されたシェーダは以下のようにパイプラインステートに渡します。
        retDesc.VS = CD3DX12_SHADER_BYTECODE(
            VS::GetPtr()->GetShaderComPtr()->GetBufferPointer(),
            VS::GetPtr()->GetShaderComPtr()->GetBufferSize()
        );
        retDesc.PS = CD3DX12_SHADER_BYTECODE(
            PS::GetPtr()->GetShaderComPtr()->GetBufferPointer(),
            PS::GetPtr()->GetShaderComPtr()->GetBufferSize()
        );

シェーダーの設定

 さてそのようにパイプラインステートに渡されるシェーダですが、中身はどうなっているのでしょうか。
 ソリューションエクスプローラ、でシェーダーーファイルのフィルタを選択すると、以下のようになってます。

 

図1107a

 

 このなかの、例えばvshader.hlslを右クリックしプロパティを表示すると、以下の設定になっています。全般タブを開いてみましょう。

 

図1107b

 

 この設定の意味するところは、エントリポイントはmainであり、シェーダーのバージョン5.0であり、シェーダーの種類頂点シェーダである設定になっています。
 さらに出力ファイルタブを選択すると、以下のように出力先が、
$(SolutionDir)media\Shaders\%(Filename).cso
 となっています。

 

図1107c

 

 これはVisualStdioの設定なのですが、$(SolutionDir)というのはソリューションファイルがあるディレクトリを指し、その中のmedia\Shadersディレクトリに%(Filename).csoという名前で、コンパイルしなさい、という設定になっています。
 つまり、vshader.hlslはファイルそのものは
...\BaseDx12\SimpleSample101\BaseDx12\Shaders\vshader.hlsl
 にありますが、これを
...\BaseDx12\SimpleSample101\media\Shaders\vshader.cso
 にコンパイルしなさい、という設定になります。
 同様にピクセルシェーダであるpshader.hlslのプロパティを見ますと、全般タブは

 

図1107d

 

 となっており、出力ファイルタブは

 

図1107e

 

 となっています。
 このように設定することで、Scene.cppにおけるシェーダのマクロは
    IMPLEMENT_DX12SHADER(VSPCSprite, App::GetShadersPath() + L"vshader.cso")
    IMPLEMENT_DX12SHADER(PSPCSprite, App::GetShadersPath() + L"pshader.cso")
 と記述できるわけです。

頂点シェーダーの記述

 それでは頂点シェーダであるvshader.hlslを見てみましょう。
struct PSInput
{
    float4 position : SV_POSITION;
    float4 color : COLOR;
};

PSInput main(float4 position : POSITION, float4 color : COLOR)
{
    PSInput result;

    result.position = position;
    result.color = color;

    return result;
}
 エントリポイントであるmain関数は、PSInput構造体をリターンします。
 入力は
...float4 position : POSITION, float4 color : COLOR
 となっています。つまり位置情報と色情報です。これを、PSInput型のresult連数にそのまま代入してreturnします。
 今回のサンプルでは頂点シェーダのリターンはそのままピクセルシェーダに渡されます。

ピクセルシェーダーの記述

 ピクセルシェーダでは以下の処理になります。
struct PSInput
{
    float4 position : SV_POSITION;
    float4 color : COLOR;
};


float4 main(PSInput input) : SV_TARGET
{
    return input.color;
}
 このように、頂点シェーダと同じPSInputが定義され、入力のinputcolorメンバをそのままリターンします。
 ピクセルシェーダのリターンはそのまま、該当ピクセルのになります。グラデーション処理は、パイプラインの中で自動的に行われます。

 さて、この項ではシーン側での初期化作業として、コマンドラインそしてパイプラインステートシェーダの設定を行いました。
 次項ではメッシュを作成します。