001.三角形の描画(Dx12版)

 これから、シンプルバージョンのサンプルの説明を始めます。このサンプルはSimplSample001というディレクトリに含まれます。
 BaseCrossDx12.slnというソリューションを開くとDx12版が起動します。

 実行結果は以下のような画面が出ます。単純な3角形の描画です。

 

図0001a

 


Dx12版解説

 BaseCrossDx12.slnを開くと、BaseCrossDx12というメインプロジェクトがあります。この中の「Character.h/cpp」が主な記述個所になります。
 表示されている三角形はTriangleObjectクラスです。ObjectInterfaceおよびShapeInterfaceの多重継承オブジェクトです。
 親クラスのObjectInterfaceOnCreate()仮想関数を持つインターフェイスクラスです。子クラスはこの仮想関数を実装しなければなりません。ObjectInterfaceは、自分自身のshared_ptrを作成できるクラスです。(つまりthisポインタのshared_ptr版)。このポインタはオブジェクト配置の時に、簡単に包含関係を作れますので、便利と思われます。
 一方、ShapeInterfaceクラスOnUpdate、OnDraw仮想関数を持ちます。こちらは、ObjectInterfaceほど重要度は高くありません。OnUpdate、OnDraw仮想関数を作成せずに実装することも可能です。
 さて、TriangleObjectクラスOnCreate()関数は以下のようになります。
void TriangleObject::OnCreate() {
    //頂点を作成するための配列
    vector<VertexPositionColor> vertices = {
        { Vec3(0.0f, 0.5f , 0.0f),Col4(1.0f, 0.0f, 0.0f, 1.0f) },
        { Vec3(0.5f, -0.5f, 0.5f),Col4(0.0f, 1.0f, 0.0f, 1.0f) },
        { Vec3(-0.5f, -0.5f, 0.5f),Col4(0.0f, 0.0f, 1.0f, 1.0f) }
    };
    m_TriangleMesh = MeshResource::CreateMeshResource(vertices,false);
    CreateRootSignature();
    CreatePipelineState();
    CreateCommandList();
}
 ここでポイントなるのは、CreateRootSignature、CreatePipelineState、CreateCommandListというDx12用リソース構築の関数です。  これらの関数を説明する前に、Dx12で描画するのには何が必要かを述べます。
【ルートシグネチャ】シェーダに入力する内容の設定
【パイプラインステート】どのシェーダを使うか、ラスタライザステートやデプスステンシルステートなどの
  各レンダリングステートを設定する。
【コマンドリスト】描画命令や、頂点の型の設定など実際に描画するときに実行する「命令」をコマンドリストという、
  命令の手順を定義したものを作成して、それを「実行する」という形で、描画する。
 これらの3つのリソースの記述は、最低限行わなければなりません。  そしてCreateRootSignature、CreatePipelineState、CreateCommandListの3つの関数は、それぞれの初期化を行う関数です。
 簡単にそれらの関数の役割を述べます。

CreateRootSignature関数

 Dx12では、シェーダーへのアクセス方法の定義をカプセル化したものが必要です。これをルートシグネチャといいます。
 例えば、コンスタントバッファ、サンプラー、テクスチャの3つのデータをシェーダに渡す場合、それらを渡すための定義が必要になります。  このサンプルでは、三角形を描画するわけですが、描画方法は頂点データのみとなります。ですから、以下のような記述になります。
///ルートシグネチャ作成
void TriangleObject::CreateRootSignature() {
    m_RootSignature = RootSignature::CreateSimple();
}
 RootSignature::CreateSimple関数を読んでいるわけですが、これの実体は以下です。
//一番シンプルなルートシグネチャ
static inline ComPtr<ID3D12RootSignature> CreateSimple() {
    auto Dev = App::GetApp()->GetDeviceResources();
    ComPtr<ID3D12RootSignature> Ret = Dev->GetRootSignature(L"Simple");
    if (Ret != nullptr) {
        return Ret;
    }

    CD3DX12_ROOT_SIGNATURE_DESC rootSignatureDesc;
    rootSignatureDesc.Init(0, nullptr, 0, nullptr, 
        D3D12_ROOT_SIGNATURE_FLAG_ALLOW_INPUT_ASSEMBLER_INPUT_LAYOUT);

    Ret = CreateDirect(rootSignatureDesc);
    Dev->SetRootSignature(L"Simple", Ret);
    return Ret;
}
 ここでは、テクスチャ(シェーダーリソース)もサンプラーも、コンスタントバッファも指定しません。シェーダーに入れるのは頂点データのみなのでこのようなルートシグネチャになります。

CreatePipelineState関数

 ここではパイプラインステートを作成します。
///パイプラインステート作成
void TriangleObject::CreatePipelineState() {
    //パイプラインステートの定義
    D3D12_GRAPHICS_PIPELINE_STATE_DESC PineLineDesc;
    m_PipelineState 
    = PipelineState::CreateDefault2D<VertexPositionColor, 
        VSPCDirect, PSPCDirect>(m_RootSignature, PineLineDesc);
}
 ここで呼び出しているPipelineState::CreateDefault2Dはテンプレート関数になっていて、基本的な2Dのパイプラインの設定を行います。使用するシェーダと頂点形式をテンプレート引数に渡します。  また、パイプラインステートを作成するのにルートシグネチャが必要になります。

CreateCommandList関数

 この関数でコマンドリストを作成します。中身は単純です。
///コマンドリスト作成
void TriangleObject::CreateCommandList() {
    m_CommandList = CommandList::CreateDefault(m_PipelineState);
    CommandList::Close(m_CommandList);
}
 このように、Dx12描画にはこれらの3つのリソースが必ず必要になります。このサンプルは、非常に単純なのでそれぞれの初期化関数も単純になってますが、この後のサンプルでは、徐々に複雑になっていきます。
 まずは、Dx12描画の基本だけをおさえてもらえればと思います。

描画処理

 上記3つの関数は初期化処理です。実際の描画はTriangleObject::DrawObject関数に記述します。
///描画処理
void TriangleObject::DrawObject() {
    auto Dev = App::GetApp()->GetDeviceResources();
    //コマンドリストのリセット
    CommandList::Reset(m_PipelineState, m_CommandList);
    //メッシュが更新されていればリソース更新
    m_TriangleMesh->UpdateResources<VertexPositionColor>(m_CommandList);

    m_CommandList->SetGraphicsRootSignature(m_RootSignature.Get());

    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());
}
 ここでは、初期化処理で作成したコマンドリストによる描画命令を記述します。
 内容的にはDx11のデバイスコンテキストの処理に似ています。違いは、最後にDev->InsertDrawCommandLists関数を呼んでいます。この関数はDx12の関数ではなく、フレームワークの関数です。  どういう関数かというとコマンドリストを貯める(一時的に保持する)関数です。
 フレームワークではこのように、いくつものコマンドリストを貯めておいて、描画ターンの最後にコマンドリストの実行を行うことで描画処理をします。


 さて、このような三角形クラスをゲーム上に配置するのは、シーンの役割です。Scene.h/cppに記述があります。
 まず、Scene.hに宣言があります。
//--------------------------------------------------------------------------------------
/// ゲームシーン
//--------------------------------------------------------------------------------------
class Scene : public SceneInterface {
    shared_ptr<TriangleObject> m_TriangleObject;
public:
    //中略
};
 ここでメンバ変数になってるm_TriangleObjectが三角形オブジェクトのshared_ptrです。
 これを、シーンのOnCreateで初期化します。
void Scene::OnCreate() {
    //三角形の作成
    m_TriangleObject = ObjectFactory::Create<TriangleObject>();
}
 そして、シーンのOnDraw()で描画します。
void Scene::OnDraw() {
    //描画デバイスの取得
    auto Dev = App::GetApp()->GetDeviceResources();
    Dev->ClearDefaultViews(Col4(0, 0, 0, 1.0));
    //デフォルト描画の開始
    Dev->StartDefaultDraw();
    //三角形の描画
    m_TriangleObject->OnDraw();
    //デフォルト描画の終了
    Dev->EndDefaultDraw();
}
 ここで、三角形のOnDraw()を呼び出しているため、TriangleObject::OnDraw()が実行されます。
 TriangleObject::OnDraw()TriangleObject::DrawObject()を呼び出します。
 上記(このページの上のほう)で、ShapeInterfaceクラスは重要ではない、といったのは実は、この、シーンのOnDraw()で、呼び出す三角形の描画関数名は何でもいいのです。
 つまり、三角形オブジェクトがOnRender()関数を持ち、シーンからその関数を呼び出せば、結果同じになります。
 このあたりがシンプルバージョンの融通性でもあります。これがフルバージョンでは、OnDraw()やOnUpdate()は必ずこの仮想関数でなければなりません。
 ということは逆に考えれば仮想関数がお嫌いなら使わなくてもよいということになります。描画を呼び出すのはシーンですし、シーンはゲーム側のオブジェクトですから、そのあたりの設計は、ライブラリ(つまりBaseCross側)は関係ないのです。
 以上、Dx12側の説明は終わりです。

 以降、こんな感じでサンプルの解説を行っていきたいと思います。