008.頂点色とテクスチャの合成(Dx12版)

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

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

 

図0008a

 


 動画は以下になります。

 

 

【サンプルのポイント】

 今項のサンプルのポイントは複数のシェーダによる、複数の描画のサンプルです。今項のサンプルには、シェーダは2種類含まれます。1つは前項までと同じVertexPositionTexture型を持ったオブジェクトの描画です。中心の流れるテクスチャですが、これは前項とかわりません。そしてあたらしく背景の壁にVertexPositionColorTexture型の頂点を持ったオブジェクトを描画します。
 これらを実装するのにDx12版Dx11版で実装方法を変えています。Dx12版については、描画クラスを階層的に作成し、それぞれの描画をそれらのクラスに任せます。Dx11版については、それぞれのオブジェクトで、べたに描画します。
 当然Dx12版のような描画方法のほうがオブジェクト指向なのですが、Dx11版でもDx12版にならって描画クラスを作成してみると、勉強になると思います。

【共通解説】

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

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

【Dx12版解説】

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

■初期化■

 Dx12版の初期化は描画クラスで行います。今項のサンプルでは描画クラスは階層化されています。すなわちSpriteDrawクラスが親クラス。PCTSpriteDrawクラスPTSpriteDrawクラスが子クラスの関係で、描画クラスを構成します。
 しかしながら、ほとんどのDx12リソースの初期化は親クラス側で行います。今回のオブジェクトの描画処理で違いがあるのはコンスタントバッファパイプラインステートです。違いがあるといってもそんなに大きな違いがあるわけではありません。前者はコンスタントバッファのサイズが違うだけで、後者はシェーダと頂点データの型が違うだけです。ですので、コンスタントバッファについてはサイズをパラメータ化して初期化という方法が取れますし、後者はテンプレートで型を渡す方法で親クラス側に初期化関数を持たせることができます。それらはSpriteDraw::CreateConstantBufferBase()関数、SpriteDraw::CreatePipelineStateBase()テンプレート関数という形で、親クラス側に実装されています。

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

 ルートシグネチャSpriteDraw::CreateRootSignature()関数で行います。シェーダリソースビュー、サンプラー、コンスタントバッファを持つルートシグネチャです。

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

 デスクプリタヒープSpriteDraw::CreateDescriptorHeap()関数で行います。前項と同じです。

■サンプラー作成■

 SpriteDraw::CreateSampler()関数で行います。ラッピングサンプラーです。

■シェーダーリソースビュー作成■

 SpriteDraw::CreateShaderResourceView()関数で行います。前項と同じです。

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

 コンスタントバッファは親クラス側ではSpriteDraw::CreateConstantBufferBase()関数で行います。このクラスは引数にコンスタントバッファのサイズを渡します。子クラス側でサイズを指定して、親クラス側の関数を呼び出します。

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

 パイプラインステートは親クラス側ではSpriteDraw::CreatePipelineStateBase()テンプレート関数が用意されています。子クラス側で頂点型、シェーダ型を渡してこのテンプレート関数を呼び出します。

■コマンドリスト作成■

 デフォルト処理です。SpriteDraw::CreateCommandList()関数で行います。

■コンスタントバッファの更新■

 子クラス側のUpdateConstantBuffer()関数で行います。この関数は引数にコンスタントバッファの構造体を渡します。これは子クラスごとに違います。

■初期化のメカニズム■

 これらのDX12リソースの初期化を実装するのは、SpriteDraw::OnCreate()関数です。
void SpriteDraw::OnCreate() {
    //テクスチャの作成
    m_TextureResource = TextureResource::CreateTextureResource(m_TextureFileName, L"WIC");
    ///各初期化関数呼び出し
    ///ルートシグネチャ作成
    CreateRootSignature();
    ///デスクプリタヒープ作成
    CreateDescriptorHeap();
    ///サンプラー作成
    CreateSampler();
    ///シェーダーリソースビュー作成
    CreateShaderResourceView();
    ///コンスタントバッファ作成
    CreateConstantBuffer();
    ///パイプラインステート作成
    CreatePipelineState();
    ///コマンドリスト作成
    CreateCommandList();
}
 ここで呼び出している関数のうち、CreateConstantBuffer()とCreatePipelineState()は純粋仮想関数になってます。ということは子クラス側の関数を呼びだすわけですが、子クラスPCTSpriteDrawCreateConstantBuffer()は以下のようになってます。
void PCTSpriteDraw::CreateConstantBuffer() {
    CreateConstantBufferBase(sizeof(SpriteConstantBuffer));
}
 子クラスPTSpriteDrawCreateConstantBuffer()は以下のようになってます。
void PTSpriteDraw::CreateConstantBuffer() {
    CreateConstantBufferBase(sizeof(DiffuseSpriteConstantBuffer));
}
 つまり、コンスタントバッファのサイズが違うだけなので、引数を変えて親クラスの関数を呼び出すわけです。
 CreatePipelineState()の子クラス側の関数は、
void PCTSpriteDraw::CreatePipelineState() {
    CreatePipelineStateBase<VertexPositionColorTexture, VSPCTSprite, PSPCTSprite>();
}
 と
void PTSpriteDraw::CreatePipelineState() {
    CreatePipelineStateBase<VertexPositionTexture, VSPTSprite, PSPTSprite>();
}
 という形です。どちらもそれぞれの頂点型とシェーダクラスを渡しています。このように親クラスから純粋仮想関数を呼び出し、子クラスでは親クラスの関数を呼び出すというテクニックは、子クラスごとの実装の違いを分けるのに便利です。

■更新処理■

 初期化はもっぱらDx12リソースの初期化ですが、更新は、各配置されるオブジェクトの更新です。今項では、中央の流れるテクスチャのクラスSquareSprite頂点UV値の変更が更新処理です。SquareSprite::UpdateVertex()関数です。この処理は前項と変わりません。

■描画処理■

 描画は描画クラスDrawObject()関数で行います。描画する前にコンスタントバッファを更新します。

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

【まとめ】

 今回はシェーダの違うオブジェクトを同居させる場合の、ヒントになるようなサンプルになってます。表現は前項とほぼ同じなので、比べてみると違いが判ると思います。
 また、ここで知ってほしいのはプログラム実装は一通りではないということです。描画処理1つとっても、クラスの設計や関数の書き方で、効率の良しあしが決まります。せっかくC++という自由度が高い言語を扱っているので、テンプレート、仮想関数など。C++の機能をふんだんに使って実装を試みましょう。