42.Dx12でのシャドウマップ

前回更新から時間がたってしまいましたが、今回はフルバージョンチュートリアル002で懸案となっていました、Dx12版でのシャドウマップを実装しました。
動画に関しましては前々回の動画(チュートリアル002、Dx11版)と同じです。

この記事は、
コミットFullTutorial002動作修正。
から、
コミットシャドウマップの大きさ指定をマジックナンバーから変数化。ライブラリ修正あり。
の間の作業です。コミットがいくつか飛んでますが、これまで出てきた不具合修正や、ドキュメント修正です。
GitHubサイト

https://github.com/WiZFramework/BaseCross

を参照して下さい。

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

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

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

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

今回追加したサンプルについて

今回のテーマはDx12におけるシャドウマップ描画はどうしたらいいのかというものです。
シャドウマップはです。ではありません。のほうはライティングで実装しますので、あまり問題にはなりません。
影の描画にはいろんな方法があり、リアルな表現もありますがBaseCrossで実装されているのは一番単純なものです。1つの平行光源をつかって、何かオブジェクトに当たった場所に影を描画します。複雑なものを実装したい場合は、現時点では各自対応という形です。

シャドウマップコンポーネント

シャドウマップは2パスで行いますが、そのパス1である、シャドウマップへの書き込みShadowmapコンポーネントを実装します。
ただし、影の描画ができるのはVertexPositionNormalTexture型の頂点を持つオブジェクトのみです。
それ以外の頂点型については、今後の実装になります(実装するかどうかわかりませんが・・・)
たとえばサンプルのBoxクラスだったらOnCreate関数

//初期化
void Box::OnCreate() {
    //中略

    //影をつける
    auto ShadowPtr = AddComponent<Shadowmap>();
    ShadowPtr->SetMeshResource(L"DEFAULT_CUBE");

    //描画コンポーネント
    auto PtrDraw = AddComponent<PNTStaticDraw>();
    PtrDraw->SetMeshResource(L"DEFAULT_CUBE");
    PtrDraw->SetTextureResource(L"TRACE_TX");
    //透過処理
    SetAlphaActive(true);
}

この例では、DEFAULT_CUBE形状の影を付けます。つまり物体の形と同じ影ですね。

影を受け止める

シャドウマップコンポーネントは影を出すほうです。受けるほうは、PNTStaticDraw描画コンポーネントで、

    //描画コンポーネントの追加
    auto DrawComp = Ptr->AddComponent<PNTStaticDraw>();
    //描画コンポーネントに形状(メッシュ)を設定
    DrawComp->SetMeshResource(L"DEFAULT_SQUARE");
    //自分に影が映りこむようにする
    DrawComp->SetOwnShadowActive(true);

    //描画コンポーネントテクスチャの設定
    DrawComp->SetTextureResource(L"SKY_TX");

のように指定します。

シャドウマップについて

シャドウマップは2パスで描画します。すなわち影を出すほう影をうけるほうです。
影を出すほうデプスバッファ(深度バッファ)として作成したテクスチャに、ライトの方角から見た状態を書き込みます。
それで、2番目のパスで、その深度バッファと通常のオブジェクトの重なりを計算して影を付けるわけです。原理を説明するとボロが出るので、この辺にして、Dx12版における実装を見ていきます。
シャドウマップを作成するには、デプスステンシルバッファを作成します。影を書き込むためのテクスチャと思ってください。フレームワークではDx12LibプロジェクトのDeviceResources.h/cppにその記述があります。
今後、実装を少し変える予定なので、現時点での実装はShadowMapRenderTargetクラスで、デプスステンシルバッファを管理しています。ShadowMapRenderTargetのImplに実体があります。

struct ShadowMapRenderTarget::Impl {
    //シャドウマップの大きさ
    const float m_ShadowMapDimension;
    //シャドウマップのデスクプリタヒープ
    ComPtr<ID3D12DescriptorHeap> m_ShadowmapDsvHeap;
    //シャドウマップのデプスステンシル
    ComPtr<ID3D12Resource> m_ShadowmapDepthStencil;
    //クリア用オブジェクト
    ComPtr<ID3D12RootSignature> m_RootSignature;
    ComPtr<ID3D12GraphicsCommandList> m_CommandList;

    Impl(float ShadowMapDimension) :
        m_ShadowMapDimension(ShadowMapDimension)
    {}
    ~Impl() {}
};

こんな感じで実装されてます。まず、これらをすべて初期化するわけですが、ようは、m_ShadowmapDsvHeapに影を書き込める環境を整えてます。ShadowMapRenderTargetのコンストラクタに記述があります。以下がその実体です。

ShadowMapRenderTarget::ShadowMapRenderTarget(float ShadowMapDimension):
    pImpl(new Impl(ShadowMapDimension))
{
    try {
        //シャドウマップはcolは未使用
        auto Dev = App::GetApp()->GetDeviceResources();

        //シャドウマップ用デスクプリタヒープ
        D3D12_DESCRIPTOR_HEAP_DESC dsvHeapDesc = {};
        dsvHeapDesc.NumDescriptors = 1;
        dsvHeapDesc.Type = D3D12_DESCRIPTOR_HEAP_TYPE_DSV;
        dsvHeapDesc.Flags = D3D12_DESCRIPTOR_HEAP_FLAG_NONE;
        ThrowIfFailed(Dev->GetDevice()->CreateDescriptorHeap(&dsvHeapDesc, IID_PPV_ARGS(&pImpl->m_ShadowmapDsvHeap)),
            L"シャドウマップデプスステンシルビューのデスクプリタヒープ作成に失敗しました",
            L"Dev->GetDevice()->CreateDescriptorHeap(&dsvHeapDesc, IID_PPV_ARGS(&m_ShadowmapDsvHeap)",
            L"ShadowMapRenderTarget::ShadowMapRenderTarget()"
        );

        D3D12_CLEAR_VALUE depthOptimizedClearValue = {};
        depthOptimizedClearValue.Format = DXGI_FORMAT_D32_FLOAT;
        depthOptimizedClearValue.DepthStencil.Depth = 1.0f;
        depthOptimizedClearValue.DepthStencil.Stencil = 0;

        CD3DX12_HEAP_PROPERTIES heapProperties(D3D12_HEAP_TYPE_DEFAULT);

        CD3DX12_RESOURCE_DESC shadowTextureDesc(
            D3D12_RESOURCE_DIMENSION_TEXTURE2D,
            0,
            static_cast<UINT>(ShadowMapDimension),
            static_cast<UINT>(ShadowMapDimension),
            1,
            1,
            DXGI_FORMAT_R32_TYPELESS,
            1,
            0,
            D3D12_TEXTURE_LAYOUT_UNKNOWN,
            D3D12_RESOURCE_FLAG_ALLOW_DEPTH_STENCIL);

        ThrowIfFailed(Dev->GetDevice()->CreateCommittedResource(
            &heapProperties,
            D3D12_HEAP_FLAG_NONE,
            &shadowTextureDesc,
            D3D12_RESOURCE_STATE_DEPTH_WRITE,
            &depthOptimizedClearValue,
            IID_PPV_ARGS(&pImpl->m_ShadowmapDepthStencil)
        ),
            L"シャドウマップデプスステンシルリソース作成に失敗しました",
            L"Dev->GetDevice()->CreateCommittedResource()",
            L"ShadowMapRenderTarget::ShadowMapRenderTarget()"
        );

        D3D12_DEPTH_STENCIL_VIEW_DESC depthStencilViewDesc = {};
        depthStencilViewDesc.Format = DXGI_FORMAT_D32_FLOAT;
        depthStencilViewDesc.ViewDimension = D3D12_DSV_DIMENSION_TEXTURE2D;
        depthStencilViewDesc.Texture2D.MipSlice = 0;



        //デプスステンシルビューの作成
        Dev->GetDevice()->CreateDepthStencilView(pImpl->m_ShadowmapDepthStencil.Get(), &depthStencilViewDesc,
            pImpl->m_ShadowmapDsvHeap->GetCPUDescriptorHandleForHeapStart());

        //ルートシグネチャ
        {
            CD3DX12_ROOT_SIGNATURE_DESC rootSignatureDesc;
            rootSignatureDesc.Init(0, nullptr, 0, nullptr, D3D12_ROOT_SIGNATURE_FLAG_ALLOW_INPUT_ASSEMBLER_INPUT_LAYOUT);

            ComPtr<ID3DBlob> signature;
            ComPtr<ID3DBlob> error;

            ThrowIfFailed(D3D12SerializeRootSignature(&rootSignatureDesc, D3D_ROOT_SIGNATURE_VERSION_1, &signature, &error),
                L"空のルートシグネチャのシリアライズに失敗しました",
                L"D3D12SerializeRootSignature(&rootSignatureDesc, D3D_ROOT_SIGNATURE_VERSION_1, &signature, &error)",
                L"ShadowMapRenderTarget::ShadowMapRenderTarget()"
            );
            ThrowIfFailed(Dev->GetDevice()->CreateRootSignature(0, signature->GetBufferPointer(), signature->GetBufferSize(), IID_PPV_ARGS(&pImpl->m_RootSignature)),
                L"ルートシグネチャの作成に失敗しました",
                L"Dev->GetDevice()->CreateRootSignature)",
                L"ShadowMapRenderTarget::ShadowMapRenderTarget()"
            );
        }

        ComPtr<ID3D12PipelineState> PipelineState;

        ThrowIfFailed(Dev->GetDevice()->CreateCommandList(
            0,
            D3D12_COMMAND_LIST_TYPE_DIRECT,
            Dev->GetCommandAllocator().Get(),
            PipelineState.Get(),
            IID_PPV_ARGS(&pImpl->m_CommandList)),
            L"コマンドリストの作成に失敗しました",
            L"Dev->GetDevice()->CreateCommandList()",
            L"ShadowMapRenderTarget::ShadowMapRenderTarget()"
        );
        CommandList::Close(pImpl->m_CommandList);
    }
    catch (...) {
        throw;
    }
}

上から順番に見ていきますと、まず

        //シャドウマップ用デスクプリタヒープ
        D3D12_DESCRIPTOR_HEAP_DESC dsvHeapDesc = {};
        dsvHeapDesc.NumDescriptors = 1;
        dsvHeapDesc.Type = D3D12_DESCRIPTOR_HEAP_TYPE_DSV;
        dsvHeapDesc.Flags = D3D12_DESCRIPTOR_HEAP_FLAG_NONE;
        ThrowIfFailed(Dev->GetDevice()->CreateDescriptorHeap(&dsvHeapDesc, IID_PPV_ARGS(&pImpl->m_ShadowmapDsvHeap)),
            L"シャドウマップデプスステンシルビューのデスクプリタヒープ作成に失敗しました",
            L"Dev->GetDevice()->CreateDescriptorHeap(&dsvHeapDesc, IID_PPV_ARGS(&m_ShadowmapDsvHeap)",
            L"ShadowMapRenderTarget::ShadowMapRenderTarget()"
        );

デスクプリタヒープの定義を作成します。D3D12_DESCRIPTOR_HEAP_TYPE_DSVのタイプを指定します。デプスステンシルビュー用デスクプリタヒープという意味ですね。続いてクリアする値ヒーププロパティ、そしてテクスチャの構造を初期化して、コミットリソースでバッファを確保します。その際、気を付けたいのはCD3DX12_RESOURCE_DESC構造体の設定です。

        CD3DX12_RESOURCE_DESC shadowTextureDesc(
            D3D12_RESOURCE_DIMENSION_TEXTURE2D,
            0,
            static_cast<UINT>(ShadowMapDimension),
            static_cast<UINT>(ShadowMapDimension),
            1,
            1,
            DXGI_FORMAT_R32_TYPELESS,
            1,
            0,
            D3D12_TEXTURE_LAYOUT_UNKNOWN,
            D3D12_RESOURCE_FLAG_ALLOW_DEPTH_STENCIL);

ここで、テクスチャのフォーマットをDXGI_FORMAT_R32_TYPELESS(1 成分、32 ビット型なしフォーマット)にしています。このフォーマットの設定は結構重要で、ここを間違えると正常に動作しないこともありますので気を付けましょう。
その後Dev->GetDevice()->CreateCommittedResource関数呼び出しデプスステンシルを作成します。
デプスステンシルを作成したら、今度はデプスステンシルビューという、そののぞき窓みたいなものを作成します。ここでもそのフォーマットが重要になります。

        D3D12_DEPTH_STENCIL_VIEW_DESC depthStencilViewDesc = {};
        depthStencilViewDesc.Format = DXGI_FORMAT_D32_FLOAT;
        depthStencilViewDesc.ViewDimension = D3D12_DSV_DIMENSION_TEXTURE2D;
        depthStencilViewDesc.Texture2D.MipSlice = 0;

ここではDXGI_FORMAT_D32_FLOAT(1 成分、32 ビット浮動小数点フォーマット)デプスステンシルビューを作成します。つまり、作成はDXGI_FORMAT_R32_TYPELESSで行って、ビューはDXGI_FORMAT_D32_FLOATで行う形になります。
ここでのフォーマットのD32Dの意味はデプスバッファという意味なのだそうです。なのだそうですというのは、マイクロソフトのドキュメントにそう記されているのですが、このあたりDx11とDx12でどうも動作が違っています。
例えば、Dx11の場合は、かならずしも上記の設定でなくてもシャドウマップを作成できます。しかしDx12の場合は上記の設定でないとスワップチェーンのエラーが出ます。

このようにして作成したシャドウマップデプスバッファにパス1で書き込むわけですが、書き込む処理をしているのはShadowmapコンポーネントです。
Shadowmapコンポーネントの初期化はルートシグネチャデスクプリタヒープ、コンスタントバッファなどを作成しています。注意したいのは、シャドウマップに書き込む場合に、頂点シェーダのみ使うというところです。VSShadowmap.hlslという頂点シェーダで書き込みます。
また書き込むレンダリングターゲットはデプスステンシルビューのみ設定します。

最後に、パス2ですが、これはPNTStaticDrawコンポーネントで行います。このコンポーネントでは、影がある場合と影がない場合とDx12のリソースを切り替えなければいけないので、ちょっと大変です。ですが一つ一つ丁寧に作成していけば、実装できます。
影付き描画の場合の、重要な部分だけ説明します。リソース作成は、PNTStaticDraw::CreateWithShadow関数で行ってます。以下はその一部です。
まず、ルートシグネチャの作成です。

        //ルートシグネチャシャドウ付き
        pImpl->m_RootSignature = RootSignature::CreateSrv2Smp2Cbv();

となってますが、追いかけていくと

//シェーダリソース2つとサンプラー2つとコンスタントバッファ
static inline ComPtr<ID3D12RootSignature> CreateSrv2Smp2Cbv() {
    auto Dev = App::GetApp()->GetDeviceResources();
    ComPtr<ID3D12RootSignature> Ret = Dev->GetRootSignature(L"Srv2Smp2Cbv");
    if (Ret != nullptr) {
        return Ret;
    }


    CD3DX12_DESCRIPTOR_RANGE ranges[5];
    ranges[0].Init(D3D12_DESCRIPTOR_RANGE_TYPE_SRV, 1, 0);
    ranges[1].Init(D3D12_DESCRIPTOR_RANGE_TYPE_SRV, 1, 1);
    ranges[2].Init(D3D12_DESCRIPTOR_RANGE_TYPE_SAMPLER, 1, 0);
    ranges[3].Init(D3D12_DESCRIPTOR_RANGE_TYPE_SAMPLER, 1, 1);
    ranges[4].Init(D3D12_DESCRIPTOR_RANGE_TYPE_CBV, 1, 0);

    CD3DX12_ROOT_PARAMETER rootParameters[5];
    rootParameters[0].InitAsDescriptorTable(1, &ranges[0], D3D12_SHADER_VISIBILITY_PIXEL);
    rootParameters[1].InitAsDescriptorTable(1, &ranges[1], D3D12_SHADER_VISIBILITY_PIXEL);
    rootParameters[2].InitAsDescriptorTable(1, &ranges[2], D3D12_SHADER_VISIBILITY_PIXEL);
    rootParameters[3].InitAsDescriptorTable(1, &ranges[3], D3D12_SHADER_VISIBILITY_PIXEL);
    rootParameters[4].InitAsDescriptorTable(1, &ranges[4], D3D12_SHADER_VISIBILITY_ALL);

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

    Ret = CreateDirect(rootSignatureDesc);
    Dev->SetRootSignature(L"Srv2Smp2Cbv", Ret);
    return Ret;
}

ここではシェーダリソース2つととサンプラー2つとコンスタントバッファを持つルートシグネチャを作成します。
シェーダリソース通常のテクスチャ用と、シャドウマップ用です。サンプラーも同じです。つまりここでは、パス1で書き込んだシャドウマップをシェーダーリソースと見立てて、ピクセルシェーダで処理する形になります。
次にデスクプリタヒープですが

pImpl->m_CbvSrvDescriptorHandleIncrementSize =
    Dev->GetDevice()->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV);
pImpl->m_SamplerDescriptorHandleIncrementSize =
    Dev->GetDevice()->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_SAMPLER);

//CbvSrvデスクプリタヒープ
pImpl->m_CbvSrvUavDescriptorHeap = DescriptorHeap::CreateCbvSrvUavHeap(1 + 2);
//サンプラーデスクプリタヒープ
pImpl->m_SamplerDescriptorHeap = DescriptorHeap::CreateSamplerHeap(2);
//GPU側デスクプリタヒープのハンドルの配列の作成
pImpl->m_GPUDescriptorHandleVec.clear();
CD3DX12_GPU_DESCRIPTOR_HANDLE SrvHandle1(
    pImpl->m_CbvSrvUavDescriptorHeap->GetGPUDescriptorHandleForHeapStart(),
    0,
    0
);
pImpl->m_GPUDescriptorHandleVec.push_back(SrvHandle1);
CD3DX12_GPU_DESCRIPTOR_HANDLE SrvHandle2(
    pImpl->m_CbvSrvUavDescriptorHeap->GetGPUDescriptorHandleForHeapStart(),
    1,
    pImpl->m_CbvSrvDescriptorHandleIncrementSize
);
pImpl->m_GPUDescriptorHandleVec.push_back(SrvHandle2);

CD3DX12_GPU_DESCRIPTOR_HANDLE SamplerHandle1(
    pImpl->m_SamplerDescriptorHeap->GetGPUDescriptorHandleForHeapStart(),
    0,
    0
);
pImpl->m_GPUDescriptorHandleVec.push_back(SamplerHandle1);

CD3DX12_GPU_DESCRIPTOR_HANDLE SamplerHandle2(
    pImpl->m_SamplerDescriptorHeap->GetGPUDescriptorHandleForHeapStart(),
    1,
    pImpl->m_SamplerDescriptorHandleIncrementSize
);
pImpl->m_GPUDescriptorHandleVec.push_back(SamplerHandle2);


CD3DX12_GPU_DESCRIPTOR_HANDLE CbvHandle(
    pImpl->m_CbvSrvUavDescriptorHeap->GetGPUDescriptorHandleForHeapStart(),
    2,
    pImpl->m_CbvSrvDescriptorHandleIncrementSize
);
pImpl->m_GPUDescriptorHandleVec.push_back(CbvHandle);

こんな感じで初期化します。CbvSrvデスクプリタヒープは3つ、サンプラーデスクプリタヒープは2つです。
このような感じで、サンプラー、コンスタントバッファ、パイプラインステート、コマンドリストを作成します。
シェーダーリソースビューについては、テクスチャが動的に変化するので、初期化時には作成しません。どのタイミングで作成するかというと、PNTStaticDrawSetTextureResource関数が呼び出されたタイミングです。
以下は、そこで呼び出されたpImpl->CreateShaderResourceView関数の中身です。

///シェーダーリソースビュー(テクスチャ)作成
void PNTStaticDraw::Impl::CreateShaderResourceView(bool IsShadow) {
    auto ShPtr = m_TextureResource.lock();
    if (!ShPtr) {
        return;
    }
    auto Dev = App::GetApp()->GetDeviceResources();
    //テクスチャハンドルを作成
    CD3DX12_CPU_DESCRIPTOR_HANDLE Handle(
        m_CbvSrvUavDescriptorHeap->GetCPUDescriptorHandleForHeapStart(),
        0,
        0
    );
    //テクスチャのシェーダリソースビューを作成
    D3D12_SHADER_RESOURCE_VIEW_DESC srvDesc = {};
    srvDesc.Shader4ComponentMapping = D3D12_DEFAULT_SHADER_4_COMPONENT_MAPPING;
    //フォーマット
    srvDesc.Format = ShPtr->GetTextureResDesc().Format;
    srvDesc.ViewDimension = D3D12_SRV_DIMENSION_TEXTURE2D;
    srvDesc.Texture2D.MipLevels = ShPtr->GetTextureResDesc().MipLevels;
    //シェーダリソースビュー
    Dev->GetDevice()->CreateShaderResourceView(
        ShPtr->GetTexture().Get(),
        &srvDesc,
        Handle);

    if (IsShadow) {
        auto ShdowRender = Dev->GetShadowMapRenderTarget();

        CD3DX12_CPU_DESCRIPTOR_HANDLE ShadowHandle(
            m_CbvSrvUavDescriptorHeap->GetCPUDescriptorHandleForHeapStart(),
            1,
            m_CbvSrvDescriptorHandleIncrementSize
        );

        D3D12_SHADER_RESOURCE_VIEW_DESC shadowSrvDesc = {};
        shadowSrvDesc.Format = DXGI_FORMAT_R32_FLOAT;
        shadowSrvDesc.ViewDimension = D3D12_SRV_DIMENSION_TEXTURE2D;
        shadowSrvDesc.Texture2D.MipLevels = 1;
        shadowSrvDesc.Shader4ComponentMapping = D3D12_DEFAULT_SHADER_4_COMPONENT_MAPPING;

        Dev->GetDevice()->CreateShaderResourceView(ShdowRender->GetDepthStencil().Get(), &shadowSrvDesc, ShadowHandle);
    }
}

ここで注意したいのは影付きの場合の

        D3D12_SHADER_RESOURCE_VIEW_DESC shadowSrvDesc = {};
        shadowSrvDesc.Format = DXGI_FORMAT_R32_FLOAT;
        shadowSrvDesc.ViewDimension = D3D12_SRV_DIMENSION_TEXTURE2D;
        shadowSrvDesc.Texture2D.MipLevels = 1;
        shadowSrvDesc.Shader4ComponentMapping = D3D12_DEFAULT_SHADER_4_COMPONENT_MAPPING;

の箇所です。シャドウマップの作成の時にも説明しましたが、フォーマットをDXGI_FORMAT_R32_FLOATでシェーダリソースビューを作成します。
ここまでを整理しますと、シャドウマップは、作成はDXGI_FORMAT_R32_TYPELESSで行って、デプスステンシルビューはDXGI_FORMAT_D32_FLOATで書き込んで、影描画時にDXGI_FORMAT_R32_FLOATでシェーダリソースビューとして読み込む、という処理になります。
なんともややこしい話で、もっと単純な方法はないものかと思うのですが、DirectX-Graphics-Samplesの影実装のサンプルとしてD3D12Multithreadingを参考にしますと、このようにして影を描画しています。

では今回はこの辺で。
次回もよろしくお願いします。

カテゴリー

ピックアップ記事

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