009.立方体の描画(Dx12版)

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

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

 

図0009a

 


 動画は以下になります。

 

 

【サンプルのポイント】

 今項から3Dの描画になります。手始めにVertexPositionColor型の頂点を持つ立方体の描画です。
 まず頂点バッファを自作する処理からやってみます。立法や球体など、基本的な形状はライブラリを使って作成することができます。ですが、今項では、自作する方法の説明から始めます。

【共通解説】

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

 更新処理は動きは同じですが、Dx12版の更新処理で説明します。

【Dx12版解説】

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

■初期化■

 初期化しなければならないDx12リソースはこれまでのスプライトと同じです。ですが初期化の仕方(呼び出す関数など)が若干違います。
 今項は初めての3Dなので、少し詳しく説明します。

■頂点作成■

 頂点バッファとインデックスバッファCubeObject::CreateBuffers()関数で初期化します。
void CubeObject::CreateBuffers() {
    float HelfSize = 0.5f;
    vector<VertexPositionColor> vertices = {
        { VertexPositionColor(Vec3(-HelfSize, HelfSize, -HelfSize), Col4(1.0f, 0.0f, 0.0f, 1.0f)) },
        { VertexPositionColor(Vec3(HelfSize, HelfSize, -HelfSize), Col4(0.0f, 1.0f, 0.0f, 1.0f)) },
        { VertexPositionColor(Vec3(-HelfSize, -HelfSize, -HelfSize), Col4(0.0f, 0.0f, 1.0f, 1.0f)) },
        { VertexPositionColor(Vec3(HelfSize, -HelfSize, -HelfSize), Col4(1.0f, 0.0f, 1.0f, 1.0f)) },
        { VertexPositionColor(Vec3(HelfSize, HelfSize, HelfSize), Col4(1.0f, 0.0f, 0.0f, 1.0f)) },
        { VertexPositionColor(Vec3(-HelfSize, HelfSize, HelfSize), Col4(0.0f, 1.0f, 0.0f, 1.0f)) },
        { VertexPositionColor(Vec3(HelfSize, -HelfSize, HelfSize), Col4(0.0f, 0.0f, 1.0f, 1.0f)) },
        { VertexPositionColor(Vec3(-HelfSize, -HelfSize, HelfSize), Col4(1.0f, 0.0f, 1.0f, 1.0f)) }
    };
    vector<uint16_t> indices = {
        0, 1, 2,
        1, 3, 2,
        1, 4, 3,
        4, 6, 3,
        4, 5, 6,
        5, 7, 6,
        5, 0, 7,
        0, 2, 7,
        5, 4, 0,
        4, 1, 0,
        6, 7, 3,
        7, 2, 3
    };
    //メッシュの作成(変更できない)
    m_CubeMesh = MeshResource::CreateMeshResource(vertices, indices, false);
}
 ここではまず、VertexPositionColor型の配列であるverticesを作成します。立方体ですので、頂点数は8つです。頂点にカラー要素をもち、グラデーションするようにします。
 位置情報は、立方体の中心が原点に来るように作成します。これを、描画時にはワールド、ビュー、射影行列を反映させて、画面上に表示します。また作成時のサイズですが、1辺が1.0fになるように作成します。
 頂点の配列を作成したら、インデックスの配列を作成します。uint16_t型(符号なし16ビット整数)の番号の配列です。この番号は、各頂点を使って時計回りの三角形が作れるように、頂点配列の各頂点のインデックス(添え字)を配列にします。たとえば、インデックス配列の最初に0, 1, 2と記述がありますが、これは頂点配列の0番目, 1番目, 2番目の頂点で三角形を作る、という意味になります。このようにして、頂点配列の各頂点を使って、12個の三角形を作成します。立方体は面が6つですから、各面を作成するのに三角形2つ必要なので、12個の三角形ということです。
 ただこの方法で作成できるのはライティングを行わないからで、ライティングを行うためには法線という要素も頂点に必要になるので、もう少し複雑になります。

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

 ルートシグネチャCubeObject::CreateRootSignature()関数で初期化します。コンスタントバッファのみのルートシグネチャで問題ありません。

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

 CubeObject::CreateDescriptorHeap()関数で初期化します。こちらもコンスタントバッファのみで問題ありません。

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

 CubeObject::CreateConstantBuffer()関数で初期化します。2次元スプライトと違うところは、コンスタントバッファに渡す行列です。今回使用するコンスタントバッファのC++側構造体は以下です。
struct StaticConstantBuffer
{
    Mat4x4 World;
    Mat4x4 View;
    Mat4x4 Projection;
    Col4 Emissive;
    StaticConstantBuffer() {
        memset(this, 0, sizeof(StaticConstantBuffer));
    };
};
 ここではWorld、View、Projectionと、3つの行列があります。これは、ワールド行列、ビュー行列、射影行列ですが、この3つで、3Dオブジェクトをデバイス座標に座標変換することができます。
 その座標変換はシェーダで行っているわけですが、VSPCStatic.hlslという、今回使用する頂点シェーダを見てみると
PSPCInput main(VSPCInput input)
{
    PSPCInput result;
    //頂点の位置を変換
    float4 pos = float4(input.position.xyz, 1.0f);
    //ワールド変換
    pos = mul(pos, World);
    //ビュー変換
    pos = mul(pos, View);
    //射影変換
    pos = mul(pos, Projection);
    //ピクセルシェーダに渡す変数に設定
    result.position = pos;
    result.color = input.color;
    return result;
}
 上記、赤くなっているところのように、シェーダに渡された位置情報を、ワールド行列、ビュー行列、射影行列を順番に掛け算し、ピクセルシェーダに渡しているのがわかります。

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

 CubeObject::CreatePipelineState()関数で行ってます。ここでは
void CubeObject::CreatePipelineState() {
    D3D12_GRAPHICS_PIPELINE_STATE_DESC PineLineDesc;
    m_PipelineState = PipelineState::CreateDefault3D<VertexPositionColor, 
        VSPCStatic, PSPCStatic>(m_RootSignature, PineLineDesc);
}
 ここで呼び出しているPipelineState::CreateDefault3D()テンプレート関数は、以下のような内容です。
template<typename Vertex, typename VS, typename PS>
static inline ComPtr<ID3D12PipelineState> 
CreateDefault3D(const ComPtr<ID3D12RootSignature>& rootSignature, 
    D3D12_GRAPHICS_PIPELINE_STATE_DESC& RetDesc) {

    CD3DX12_RASTERIZER_DESC rasterizerStateDesc(D3D12_DEFAULT);
    //裏面カリング
    rasterizerStateDesc.CullMode = D3D12_CULL_MODE_NONE;

    ZeroMemory(&RetDesc, sizeof(RetDesc));
    RetDesc.InputLayout = { Vertex::GetVertexElement(), Vertex::GetNumElements() };
    RetDesc.pRootSignature = rootSignature.Get();
    RetDesc.VS =
    {
        reinterpret_cast<UINT8*>(VS::GetPtr()->GetShaderComPtr()->GetBufferPointer()),
        VS::GetPtr()->GetShaderComPtr()->GetBufferSize()
    };
    RetDesc.PS =
    {
        reinterpret_cast<UINT8*>(PS::GetPtr()->GetShaderComPtr()->GetBufferPointer()),
        PS::GetPtr()->GetShaderComPtr()->GetBufferSize()
    };
    RetDesc.RasterizerState = rasterizerStateDesc;
    RetDesc.BlendState = CD3DX12_BLEND_DESC(D3D12_DEFAULT);
    RetDesc.DepthStencilState = CD3DX12_DEPTH_STENCIL_DESC(D3D12_DEFAULT);
    RetDesc.SampleMask = UINT_MAX;
    RetDesc.PrimitiveTopologyType = D3D12_PRIMITIVE_TOPOLOGY_TYPE_TRIANGLE;
    RetDesc.NumRenderTargets = 1;
    RetDesc.RTVFormats[0] = DXGI_FORMAT_R8G8B8A8_UNORM;
    RetDesc.DSVFormat = DXGI_FORMAT_D32_FLOAT;
    RetDesc.SampleDesc.Count = 1;
    return CreateDirect(RetDesc);
}
 2D用のパイプラインステートとの一番の違いは、赤くなっているデプスステンシルステートの設定でしょう。CD3DX12_DEPTH_STENCIL_DESC(D3D12_DEFAULT)を追いかけていくと、以下の用な設定ですd3dx12.hに記述があります。このファイルはマイクロソフト社のDirectX-Graphics-Samplesからの引用です。
explicit CD3DX12_DEPTH_STENCIL_DESC( CD3DX12_DEFAULT )
{
    DepthEnable = TRUE;
    DepthWriteMask = D3D12_DEPTH_WRITE_MASK_ALL;
    DepthFunc = D3D12_COMPARISON_FUNC_LESS;
    StencilEnable = FALSE;
    StencilReadMask = D3D12_DEFAULT_STENCIL_READ_MASK;
    StencilWriteMask = D3D12_DEFAULT_STENCIL_WRITE_MASK;
    const D3D12_DEPTH_STENCILOP_DESC defaultStencilOp =
    { D3D12_STENCIL_OP_KEEP, D3D12_STENCIL_OP_KEEP, D3D12_STENCIL_OP_KEEP, 
        D3D12_COMPARISON_FUNC_ALWAYS };
    FrontFace = defaultStencilOp;
    BackFace = defaultStencilOp;
}
 このように、3Dなので深度バッファを有効にしてます。

■コマンドリスト作成■

 デフォルトのコマンドリストです。CubeObject::CreateCommandList()関数に記述があります。

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

 CubeObject::UpdateConstantBuffer()関数です。
void CubeObject::UpdateConstantBuffer() {
    //行列の定義
    Mat4x4 World, View, Proj;
    //ワールド行列の決定
    World.affineTransformation(
        m_Scale,            //スケーリング
        Vec3(0, 0, 0),      //回転の中心(重心)
        m_Qt,               //回転角度
        m_Pos               //位置
    );
    //転置する
    World.transpose();
    //ビュー行列の決定
    View = XMMatrixLookAtLH(Vec3(0, 2.0, -5.0f), Vec3(0, 0, 0), Vec3(0, 1.0f, 0));
    //転置する
    View.transpose();
    //射影行列の決定
    float w = static_cast<float>(App::GetApp()->GetGameWidth());
    float h = static_cast<float>(App::GetApp()->GetGameHeight());
    Proj = XMMatrixPerspectiveFovLH(XM_PIDIV4, w / h, 1.0f, 100.0f);
    //転置する
    Proj.transpose();

    m_StaticConstantBuffer.World = World;
    m_StaticConstantBuffer.View = View;
    m_StaticConstantBuffer.Projection = Proj;
    m_StaticConstantBuffer.Emissive = Col4(0, 0, 0, 0);
    //更新
    memcpy(m_pConstantBuffer, reinterpret_cast<void**>(&m_StaticConstantBuffer),
        sizeof(m_StaticConstantBuffer));
}
 ここでは、CubeObjectが保持しているスケール、ポジションなどのメンバ変数をもとにワールド行列を作成し、カメラ位置からビュー行列を作成し、ゲームの幅と高さから射影行列を作成しています。
 気を付けたいのは赤くなっている転置するの部分です。Matrix4X4クラス行優先(row major)の行列です。それに対してシェーダは列優先(col major)がデフォルトです。2Dのスプレイとのシェーダの場合、ビュー行列(2Dの場合は通常必要ない)、射影行列の反映はC++側で行っていました。そのため受け取るシェーダーでは、シェーダ側で行優先(row major)としていましたが、3Dの場合、C++側で列優先(col major)に直したうえでシェーダに入力してます。

■更新処理■

 CubeObject::OnUpdate()関数です。ここではオブジェクトを回転させています。この処理はDx11版も同様です。

■描画処理■

 CubeObject::DrawObject()関数です。この関数を呼ぶ前にコンスタントバッファ更新を行います。  描画処理自体はこれまでのサンプルと大きく変わるものではありません。初期化処理で作成したルートシグネチャデスクプリタヒープなどを適切にセットして最後に描画処理します。描画した後、コマンドリストはコマンドリストのプールに送ります。

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

【まとめ】

 今項はテクスチャや法線を含みませんので、比較的単純な3Dの描画を行ってます。次項ではもう少し複雑になります。