21.コンスタントバッファの作成

前回途中だった、「配置オブジェクトの初期化処理」の続きです。
今回は「コンスタントバッファ」についてです。「コマンドリスト」はもう少し後になります。

Dx12に関する情報は、マイクロソフト社の「DirectX-Graphics-Samples」が主な情報源となります。

マイクロソフト社の「DirectX-Graphics-Samples」のGitHubサイトは以下になります。

https://github.com/Microsoft/DirectX-Graphics-Samples

ぜひ、興味ある方はダウンロードしてみてDx12研究を始めるといいと思います。

この記事は、
コミット「Dx11、Dx12両頂点定義とプリミティブメッシュ作成の追加」
から、
コミット「weak_ptrからshared_ptr取得方法の修正」
の間の作業です。
GitHubサイト

https://github.com/WiZFramework/BaseCross

を参照して下さい。
その間にいくつかのコミットがありますが、後ろのコミットを開いていただいてそれと合わせ読んでいただくとわかりやすいと思います。

オブジェクトの初期化処理
配置されるオブジェクトは、「BaseCrossDx12」プロジェクトの「Character.h/cpp」に記述があります。「NormalTextureBoxクラス」です。
このオブジェクトの初期化処理は、「NormalTextureBox::OnCreate関数」で、以下がその実体です。

void NormalTextureBox::OnCreate() {
    m_CubeMesh = MeshResource::CreateCube(1.0f);
    wstring DataDir;
    App::GetApp()->GetDataDirectory(DataDir);
    wstring strTexture = DataDir + L"wall.jpg";
    m_WallTex = TextureResource::CreateTextureResource(strTexture);
    m_DrawContext = ObjectFactory::Create<VSPSDrawContext>(VSPSDrawContext::CreateParam::CreateSrvSmpCbv);
    m_DrawContext->CreateConstantBuffer(sizeof(m_ConstantBufferData));
    m_DrawContext->CreateDefault3DPipelineCmdList<VertexPositionNormalTexture, VSPNTStatic, PSPNTStatic>();
    ZeroMemory(&m_ConstantBufferData, sizeof(m_ConstantBufferData));
    //各行列をIdentityに初期化
    m_ConstantBufferData.World = Matrix4X4EX::Identity();
    m_ConstantBufferData.View = Matrix4X4EX::Identity();
    m_ConstantBufferData.Projection = Matrix4X4EX::Identity();
    //初期値更新
    m_DrawContext->UpdateConstantBuffer(reinterpret_cast<void**>(&m_ConstantBufferData), sizeof(m_ConstantBufferData));
    //テクスチャ設定
    m_DrawContext->SetTextureResource(m_WallTex);
}

前回はこの中の

    m_DrawContext = ObjectFactory::Create<VSPSDrawContext>(VSPSDrawContext::CreateParam::CreateSrvSmpCbv);

について詳しく説明しました。主に「デスクプリタヒープの作成」について述べました。
今回は、続く

    m_DrawContext->CreateConstantBuffer(sizeof(m_ConstantBufferData));

についての処理です。上の構文は「コンスタントバッファの作成」になります。
「コンスタントバッファ」は、cpp側からシェーダに渡す情報を構造体にしたものです。主に「ワールド行列」「ビュー行列」「射影行列」などを含みます。影の処理をする場合は、ライトの方向などを渡す場合があります。また、デフィーズ、エミッシブなどの色情報を追加で含む場合もあります。
「コンスタントバッファ」の構造体を決定するためには、描画に使用するシェーダを考える必要があります。
今回使用する「頂点シェーダ」は以下のようになってます。「DxShared」プロジェクトの「VSPNTStatic.hlsl」が頂点シェーダです。

struct VSPNInput
{
    float4 position : SV_POSITION;
    float3 norm : NORMAL;
};

struct PSPNInput
{
    float4 position : SV_POSITION;
    float3 norm : NORMAL;
};

cbuffer SimpleConstantBuffer : register(b0)
{
    float4x4 World  : packoffset(c0);
    float4x4 View   : packoffset(c4);
    float4x4 Projection : packoffset(c8);
    float4 LightDir : packoffset(c12);
    float4 Emissive : packoffset(c13);
    float4 Diffuse  : packoffset(c14);
};


PSPNTInput main(VSPNTInput input)
{
    PSPNTInput 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.norm = mul(input.norm, (float3x3)World);
    result.norm = normalize(result.norm);
    //テクスチャUV
    result.tex = input.tex;
    return result;
}

このソース中、「VSPNInput」と「PSPNTInput」は「INCStructs.hlsli」をインクルードすることで実装しています。わかりにくいのでここに記述しています。
このなかで「cbuffer SimpleConstantBuffer」という構造体のようなデータ構造が「シェーダ側のコンスタントバッファ」です。ここでは、いくつかの行列やライト方向などがcppから渡される形になっています。
「float4x4」というのは「4X4行列のシェーダ側の変数型」です。同様シェーダの型として「float4」「float3」などがあります。
「packoffset(c0)」のような記述は、パックオフセットといってGPU側の定数レジスタを表しています。名称は仮想的なものです。ひとつのレジスタのサイズは4バイト×4で16バイトの境界で区切られます。
ですから、「packoffset(c0)」から「packoffset(c1)」の間は4バイト間隔に割り当てられると考えられます。
しかし、

    float4x4 World  : packoffset(c0);
    float4x4 View   : packoffset(c4);

のように、float4x4型は16バイト×4で、64バイト必要とします。ですから「パックオフセット」も「packoffset(c0)」から「packoffset(c4)」に飛ぶ形になります。(c1,c2,c3は使用できない)

さて、このようになっているシェーダ側のコンスタントバッファですが、この定数に渡すためには、cpp側で、「シェーダに渡す」ということを明確に指定しなければなりません。
その準備を行っているのが

    m_DrawContext->CreateConstantBuffer(sizeof(m_ConstantBufferData));

ということになります。
ここで、「CreateConstantBuffer関数」には「cpp側コンスタントバッファの大きさ」を渡します。
m_ConstantBufferDataは構造体のインスタンスですが、その構造体宣言は以下のようになります。

struct ConstantBuffer
{
    Matrix4X4 World;
    Matrix4X4 View;
    Matrix4X4 Projection;
    Vector4 LightDir;
    Color4 Emissive;
    Color4 Diffuse;
    ConstantBuffer() {
        memset(this, 0, sizeof(ConstantBuffer));
    };
};

このようにシェーダ側のコンスタントバッファと同じ並びになっているのがわかります。
それでその構造体「ConstantBuffer」の「大きさ」を渡しているわけですが、その「CreateConstantBuffer関数」の実体は以下になります。

void VSPSDrawContext::CreateConstantBuffer(UINT BufferSize) {
    pImpl->CbvSrvUavDescriptorHeapChk();
    auto Dev = App::GetApp()->GetDeviceResources();
    ThrowIfFailed(Dev->GetDevice()->CreateCommittedResource(
        &CD3DX12_HEAP_PROPERTIES(D3D12_HEAP_TYPE_UPLOAD),
        D3D12_HEAP_FLAG_NONE,
        &CD3DX12_RESOURCE_DESC::Buffer(BufferSize),
        D3D12_RESOURCE_STATE_GENERIC_READ,
        nullptr,
        IID_PPV_ARGS(&pImpl->m_ConstantBufferUploadHeap)),
        L"コンスタントバッファ用のアップロードヒープ作成に失敗しました",
        L"Dev->GetDevice()->CreateCommittedResource()",
        L"VSPSDrawContext::CreateConstantBuffer()"
    );
    //コンスタントバッファのビューを作成
    D3D12_CONSTANT_BUFFER_VIEW_DESC cbvDesc = {};
    cbvDesc.BufferLocation = pImpl->m_ConstantBufferUploadHeap->GetGPUVirtualAddress();
    //コンスタントバッファは256バイトにアラインメント
    cbvDesc.SizeInBytes = (BufferSize + 255) & ~255;
    //コンスタントバッファビューを作成すべきデスクプリタヒープ上のハンドルを取得
    //シェーダリソースがある場合コンスタントバッファはシェーダリソースビューのあとに設置する
    CD3DX12_CPU_DESCRIPTOR_HANDLE cbvSrvHandle(
        pImpl->m_CbvSrvUavDescriptorHeap->GetCPUDescriptorHandleForHeapStart(),
        pImpl->m_SrvDescriptorHeapCount,
        pImpl->m_CbvSrvDescriptorHandleIncrementSize * pImpl->m_SrvDescriptorHeapCount
    );
    Dev->GetDevice()->CreateConstantBufferView(&cbvDesc, cbvSrvHandle);
    //コンスタントバッファのアップロードヒープのマップ
    CD3DX12_RANGE readRange(0, 0);
    ThrowIfFailed(pImpl->m_ConstantBufferUploadHeap->Map(0, &readRange, reinterpret_cast<void**>(&pImpl->m_pConstantBuffer)),
        L"コンスタントバッファのマップに失敗しました",
        L"pImpl->m_ConstantBufferUploadHeap->Map()",
        L"VSPSDrawContext::CreateConstantBuffer()"
    );
}

いろいろな実装がありますが、この一連の作業で、cpp側のコンスタントバッファのインスタンスから、シェーダ側のコンスタントバッファへの更新(つまりデータのコピー)が簡単になります。ですので頑張って実装します。
まず、

    ThrowIfFailed(Dev->GetDevice()->CreateCommittedResource(
        &CD3DX12_HEAP_PROPERTIES(D3D12_HEAP_TYPE_UPLOAD),
        D3D12_HEAP_FLAG_NONE,
        &CD3DX12_RESOURCE_DESC::Buffer(BufferSize),
        D3D12_RESOURCE_STATE_GENERIC_READ,
        nullptr,
        IID_PPV_ARGS(&pImpl->m_ConstantBufferUploadHeap)),
        L"コンスタントバッファ用のアップロードヒープ作成に失敗しました",
        L"Dev->GetDevice()->CreateCommittedResource()",
        L"VSPSDrawContext::CreateConstantBuffer()"
    );

で「アップロードヒープ」というのを作成します。cpp側コンスタントバッファをGPU側にコピー(アップロード)するためのメモリ領域です。
続いて、「コンスタントバッファのビュー」を作成します。

    //コンスタントバッファのビューを作成
    D3D12_CONSTANT_BUFFER_VIEW_DESC cbvDesc = {};
    cbvDesc.BufferLocation = pImpl->m_ConstantBufferUploadHeap->GetGPUVirtualAddress();
    //コンスタントバッファは256バイトにアラインメント
    cbvDesc.SizeInBytes = (BufferSize + 255) & ~255;
    //コンスタントバッファビューを作成すべきデスクプリタヒープ上のハンドルを取得
    //シェーダリソースがある場合コンスタントバッファはシェーダリソースビューのあとに設置する
    CD3DX12_CPU_DESCRIPTOR_HANDLE cbvSrvHandle(
        pImpl->m_CbvSrvUavDescriptorHeap->GetCPUDescriptorHandleForHeapStart(),
        pImpl->m_SrvDescriptorHeapCount,
        pImpl->m_CbvSrvDescriptorHandleIncrementSize * pImpl->m_SrvDescriptorHeapCount
    );
    Dev->GetDevice()->CreateConstantBufferView(&cbvDesc, cbvSrvHandle);

ここで注意したいのは、前回の「初期化処理」でも、「コンスタントバッファのデスクプリタヒープのハンドル」というのを取得して、これを設定したと思いますが、今回も「コンスタントバッファのデスクプリタヒープのハンドル」を取得してます。
しかし、よく比べてみてください。今回のハンドルは「CD3DX12_CPU_DESCRIPTOR_HANDLE」型です。前回のは、「CD3DX12_GPU_DESCRIPTOR_HANDLE」でした。違いがわかりますか?
前回のは「GPU側の」ハンドルです。今回のは「CPU側の」ハンドルなんですね。「pImpl->m_CbvSrvUavDescriptorHeap(つまりシェーダリソースビュー、コンスタントバッファ用)のデスクプリタヒープ」は、「GPU側のハンドル」と「CPU側のハンドル」を両方管理することがわかります。
そして取得したハンドルをもとに「コンスタントバッファビュー」を作成します。

最後に、先ほど作成した「アップロードヒープ」を「cppから見える」ように「マップ」というのを行います。

    //コンスタントバッファのアップロードヒープのマップ
    CD3DX12_RANGE readRange(0, 0);
    ThrowIfFailed(pImpl->m_ConstantBufferUploadHeap->Map(0, &readRange, reinterpret_cast<void**>(&pImpl->m_pConstantBuffer)),
        L"コンスタントバッファのマップに失敗しました",
        L"pImpl->m_ConstantBufferUploadHeap->Map()",
        L"VSPSDrawContext::CreateConstantBuffer()"
    );

この処理によりvoid型のポインタである、「pImpl->m_pConstantBuffer」とコンスタントバッファのデスクプリタヒープがマップされるわけです。
マップされるということは、今後「pImpl->m_pConstantBuffer」へのコピーはそのまま「アップロードヒープの更新」となり、その「アップロードヒープ」は「コンスタントバッファビュー」を介してシェーダ側に送ることができるようになったということです。

非常にまどろっこしいのですが「デスクプリタヒープ」「アップロードヒープ」「コンスタントバッファビュー」「GPU側デスクプリタハンドル」「CPU側デスクプリタハンドル」などなど、Dx12に登場する数々のリソース(オブジェクト)は、暗記してどうなるものでもないし、結局は数々のサンプルや違うタイプのオブジェクトを表示させることによって、理解していくしかないのかなと思っています。

また、ここで出てくる「コンスタントバッファビュー」という概念は、この後の処理を理解するのに重要です。次回に説明する予定の「テクスチャ」は「シェーダーリソースビュー」という、やはり「ビュー」という概念を持ちます。
さらに言えば、今後登場する「メッシュ(頂点データ)」も「頂点バッファビュー」「インデックスバッファビュー」という風に「ビュー」を持ちます。

それでは次回は「テクスチャの初期化」について説明します。

カテゴリー

ピックアップ記事

  1. 2016092201
    今回は前回のサンプルを少し機能を追加しまして、いろんなオブジェクトを追加しています。FullTuto…
  2. 2016092001
    前回更新から時間がたってしまいましたが、今回はフルバージョンチュートリアル003をアップしました。内…
  3. eyecatch
    前回更新から時間がたってしまいましたが、今回はフルバージョンチュートリアル002で懸案となっていまし…
PAGE TOP