BRE Architecture Series Part 10 – Tone Mapping Pass and Post Process Pass

In this opportunity, we are going to talk about tone mapping pass and post process pass. Before ToneMappingPass we have SkyBoxPass and just later is PostProcessPass that is the last one (at least at the time of writing this article). You can check BRE Architecture Series Part 9 – Skybox Pass. Before giving more details we are going to talk a little about tone mapping.

What is Tone Mapping?

Tone mapping is a technique used in image processing and computer graphics to map one set of colors to another to approximate the appearance of high-dynamic-range images in a medium that has a more limited dynamic range. Print-outs, CRT or LCD monitors, and projectors all have a limited dynamic range that is inadequate to reproduce the full range of light intensities present in natural scenes. Tone mapping addresses the problem of strong contrast reduction from the scene radiance to the displayable range while preserving the image details and color appearance important to appreciate the original scene content.

I learned about it in a John Hable’s presentation and after I understood its purpose, I tested several implementations.

In the following picture, you can see in the left a frame without tone mapping, and in the right a frame with tone mapping.

tone_mapping_comparison

You can check the result in BRE in the following video

How do we implement tone mapping in BRE?

In BRE, we have a type of command list recorder named ToneMappingCommandListRecorder that generates a command list related with tone mapping and pushes it to the CommandListExecutor to be executed. This command list recorder needs an input texture where it will apply tone mapping which is the output texture of the SkyBoxPass. Its resulting texture will be used by the PostProcessPass. You can see a diagram of this pass in the following picture

tone_mapping_workflow.jpg

Its implementation is the following

ToneMappingCommandListRecorder.h

#pragma once

#include <CommandManager\CommandListPerFrame.h>

namespace BRE {
///
/// Responsible to record command lists for the tone mapping pass
///
class ToneMappingCommandListRecorder {
public:
    ToneMappingCommandListRecorder() = default;
    ~ToneMappingCommandListRecorder() = default;
    ToneMappingCommandListRecorder(const ToneMappingCommandListRecorder&) = delete;
    const ToneMappingCommandListRecorder& operator=(const ToneMappingCommandListRecorder&) = delete;
    ToneMappingCommandListRecorder(ToneMappingCommandListRecorder&&) = default;
    ToneMappingCommandListRecorder& operator=(ToneMappingCommandListRecorder&&) = default;

    ///
    /// @brief Initializes pipeline state object and root signature
    ///
    /// This method must be called at the beginning of the application, and once
    ///
    static void InitSharedPSOAndRootSignature() noexcept;

    ///
    /// @brief Initializes the command list recorder
    ///
    /// InitSharedPSOAndRootSignature() must be called before
    ///
    /// @param inputColorBuffer Input color buffer to apply the tone mapping
    /// @param renderTargetView Render target view
    ///
    void Init(ID3D12Resource& inputColorBuffer,
              const D3D12_CPU_DESCRIPTOR_HANDLE& renderTargetView) noexcept;

    ///
    /// @brief Records and pushes command lists to the CommandListExecutor
    ///
    /// Init() must be called first
    ///
    void RecordAndPushCommandLists() noexcept;

    ///
    /// @brief Checks if internal data is valid. Typically, used for assertions
    /// @return True if valid. Otherwise, false
    ///
    bool IsDataValid() const noexcept;

private:
    ///
    /// @brief Initializes shader resource views
    /// @param inputColorBuffer Input color buffer to apply tone mapping
    ///
    void InitShaderResourceViews(ID3D12Resource& inputColorBuffer) noexcept;

    CommandListPerFrame mCommandListPerFrame;

    D3D12_CPU_DESCRIPTOR_HANDLE mRenderTargetView{ 0UL };
    D3D12_GPU_DESCRIPTOR_HANDLE mStartPixelShaderResourceView{ 0UL };
};
}

ToneMappingCommandListRecorder.cpp

#include "ToneMappingCommandListRecorder.h"

#include <d3d12.h>
#include <DirectXMath.h>

#include <CommandListExecutor\CommandListExecutor.h>
#include <DescriptorManager\CbvSrvUavDescriptorManager.h>
#include <PSOManager/PSOManager.h>
#include <RootSignatureManager\RootSignatureManager.h>
#include <ShaderManager\ShaderManager.h>
#include <Utils/DebugUtils.h>

namespace BRE {
// Root Signature:
// "DescriptorTable(SRV(t0), visibility = SHADER_VISIBILITY_PIXEL)" 0 -> Color Buffer Texture

namespace {
ID3D12PipelineState* sPSO{ nullptr };
ID3D12RootSignature* sRootSignature{ nullptr };
}

void
ToneMappingCommandListRecorder::InitSharedPSOAndRootSignature() noexcept
{
    BRE_ASSERT(sPSO == nullptr);
    BRE_ASSERT(sRootSignature == nullptr);

    PSOManager::PSOCreationData psoData{};
    psoData.mDepthStencilDescriptor = D3DFactory::GetDisabledDepthStencilDesc();

    psoData.mPixelShaderBytecode = ShaderManager::LoadShaderFileAndGetBytecode("ToneMappingPass/Shaders/PS.cso");
    psoData.mVertexShaderBytecode = ShaderManager::LoadShaderFileAndGetBytecode("ToneMappingPass/Shaders/VS.cso");

    ID3DBlob* rootSignatureBlob = &ShaderManager::LoadShaderFileAndGetBlob("ToneMappingPass/Shaders/RS.cso");
    psoData.mRootSignature = &RootSignatureManager::CreateRootSignatureFromBlob(*rootSignatureBlob);
    sRootSignature = psoData.mRootSignature;

    psoData.mNumRenderTargets = 1U;
    psoData.mRenderTargetFormats[0U] = ApplicationSettings::sColorBufferFormat;
    for (std::size_t i = psoData.mNumRenderTargets; i < _countof(psoData.mRenderTargetFormats); ++i) {
        psoData.mRenderTargetFormats[i] = DXGI_FORMAT_UNKNOWN;
    }
    psoData.mPrimitiveTopologyType = D3D12_PRIMITIVE_TOPOLOGY_TYPE_TRIANGLE;
    sPSO = &PSOManager::CreateGraphicsPSO(psoData);

    BRE_ASSERT(sPSO != nullptr);
    BRE_ASSERT(sRootSignature != nullptr);
}

void
ToneMappingCommandListRecorder::Init(ID3D12Resource& inputColorBuffer,
                                     const D3D12_CPU_DESCRIPTOR_HANDLE& renderTargetView) noexcept
{
    BRE_ASSERT(IsDataValid() == false);

    mRenderTargetView = renderTargetView;

    InitShaderResourceViews(inputColorBuffer);

    BRE_ASSERT(IsDataValid());
}

void
ToneMappingCommandListRecorder::RecordAndPushCommandLists() noexcept
{
    BRE_ASSERT(IsDataValid());
    BRE_ASSERT(sPSO != nullptr);
    BRE_ASSERT(sRootSignature != nullptr);

    ID3D12GraphicsCommandList& commandList = mCommandListPerFrame.ResetCommandListWithNextCommandAllocator(sPSO);

    commandList.RSSetViewports(1U, &ApplicationSettings::sScreenViewport);
    commandList.RSSetScissorRects(1U, &ApplicationSettings::sScissorRect);
    commandList.OMSetRenderTargets(1U, &mRenderTargetView, false, nullptr);

    ID3D12DescriptorHeap* heaps[] = { &CbvSrvUavDescriptorManager::GetDescriptorHeap() };
    commandList.SetDescriptorHeaps(_countof(heaps), heaps);

    commandList.SetGraphicsRootSignature(sRootSignature);
    commandList.SetGraphicsRootDescriptorTable(0U, mStartPixelShaderResourceView);

    commandList.IASetPrimitiveTopology(D3D_PRIMITIVE_TOPOLOGY_TRIANGLELIST);
    commandList.DrawInstanced(6U, 1U, 0U, 0U);

    commandList.Close();
    CommandListExecutor::Get().AddCommandList(commandList);
}

bool
ToneMappingCommandListRecorder::IsDataValid() const noexcept
{
    const bool result =
        mStartPixelShaderResourceView.ptr != 0UL &&
        mRenderTargetView.ptr != 0UL;

    return result;
}

void
ToneMappingCommandListRecorder::InitShaderResourceViews(ID3D12Resource& inputColorBuffer) noexcept
{
    D3D12_SHADER_RESOURCE_VIEW_DESC srvDescriptor{};
    srvDescriptor.Shader4ComponentMapping = D3D12_DEFAULT_SHADER_4_COMPONENT_MAPPING;
    srvDescriptor.ViewDimension = D3D12_SRV_DIMENSION_TEXTURE2D;
    srvDescriptor.Texture2D.MostDetailedMip = 0;
    srvDescriptor.Texture2D.ResourceMinLODClamp = 0.0f;
    srvDescriptor.Format = inputColorBuffer.GetDesc().Format;
    srvDescriptor.Texture2D.MipLevels = inputColorBuffer.GetDesc().MipLevels;
    mStartPixelShaderResourceView = CbvSrvUavDescriptorManager::CreateShaderResourceView(inputColorBuffer,
                                                                                         srvDescriptor);
}
}

Regarding shaders implementation, we use vertex and pixel shaders. we send a triangle list of 2 triangles, and in the vertex shader, we expand these triangles to match a full-screen quad dimensions in NDC space. Its implementation is the following

VertexShader

#include <ShaderUtils/CBuffers.hlsli>

#include "RS.hlsl"

struct Input {
    uint mVertexId : SV_VertexID;
};

static const float2 gQuadUVs[6] = {
    float2(0.0f, 1.0f),
    float2(0.0f, 0.0f),
    float2(1.0f, 0.0f),
    float2(0.0f, 1.0f),
    float2(1.0f, 0.0f),
    float2(1.0f, 1.0f)
};

struct Output {
    float4 mPositionNDC : SV_POSITION;
};

[RootSignature(RS)]
Output main(in const Input input)
{
    Output output;

    // Quad covering screen in NDC space ([-1.0, 1.0] x [-1.0, 1.0] x [0.0, 1.0] x [1.0])
    const float2 texCoord = gQuadUVs[input.mVertexId];
    output.mPositionNDC = float4(2.0f * texCoord.x - 1.0f, 1.0f - 2.0f * texCoord.y, 0.0f, 1.0f);

    return output;
}

In the pixel shader, we apply the tone mapping operation that was explained in John Hable’s presentation. Its implementation is the following

PixelShader

#include "RS.hlsl"

float3 FilmicToneMapping(float3 color)
{
    const float A = 0.15f; // Shoulder Strength
    const float B = 0.5f; // Linear Strength
    const float C = 0.1f; // Linear Angle
    const float D = 0.2f; // Toe Strength
    const float E = 0.02f; // Toe Numerator
    const float F = 0.3f; // Toe Denominator
    float3 linearWhite = float3(11.2f, 11.2f, 11.2f);

    color = ((color * (A * color + C * B) + D * E) / (color * (A * color + B) + D * F)) - (E / F);
    linearWhite = ((linearWhite * (A * linearWhite + C * B) + D * E) / (linearWhite * (A * linearWhite + B) + D * F)) - (E / F);
    return color / linearWhite;
}

//#define SKIP_TONE_MAPPING

struct Input {
    float4 mPositionNDC : SV_POSITION;
};

Texture2D<float4> ColorBufferTexture : register(t0);

struct Output {
    float4 mColor : SV_Target0;
};

[RootSignature(RS)]
Output main(const in Input input)
{
    Output output = (Output)0;

    const int3 fragmentScreenSpace = int3(input.mPositionNDC.xy, 0);

    const float4 color = ColorBufferTexture.Load(fragmentScreenSpace);

#ifdef SKIP_TONE_MAPPING
    output.mColor = color;
#else
    output.mColor = float4(FilmicToneMapping(color.rgb),
                           color.a);
#endif

    return output;
}

The pass responsible to create ToneMappingCommandListRecorder, to execute it and to set resource barriers is ToneMappingPass, and its implementation is the following

ToneMappingPass.h

#pragma once

#include <memory>

#include <CommandManager\CommandListPerFrame.h>
#include <ToneMappingPass\ToneMappingCommandListRecorder.h>

namespace BRE {
///
/// Responsible to generate command lists recorders for the post process tone mapping effect
///
class ToneMappingPass {
public:
    ToneMappingPass() = default;
    ~ToneMappingPass() = default;
    ToneMappingPass(const ToneMappingPass&) = delete;
    const ToneMappingPass& operator=(const ToneMappingPass&) = delete;
    ToneMappingPass(ToneMappingPass&&) = delete;
    ToneMappingPass& operator=(ToneMappingPass&&) = delete;

    ///
    /// @brief Initializes the tone mapping pass
    /// @param inputColorBuffer Input buffer to apply tone mapping
    /// @param outputColorBuffer Output buffer with the tone mapping applied
    /// @param renderTargetView CPU descriptor of the render target
    ///
    void Init(ID3D12Resource& inputColorBuffer,
              ID3D12Resource& outputColorBuffer,
              const D3D12_CPU_DESCRIPTOR_HANDLE& renderTargetView) noexcept;

    ///
    /// @brief Executes the pass
    ///
    /// Init() must be called before
    ///
    void Execute() noexcept;

private:
    ///
    /// @brief Checks if internal data is valid. Typically, used for assertions
    /// @return True if valid. Otherwise, false
    ///
    bool IsDataValid() const noexcept;

    ///
    /// @brief Executes begin task
    ///
    void ExecuteBeginTask() noexcept;

    CommandListPerFrame mCommandListPerFrame;

    ID3D12Resource* mInputColorBuffer{ nullptr };
    ID3D12Resource* mOutputColorBuffer{ nullptr };

    std::unique_ptr<ToneMappingCommandListRecorder> mCommandListRecorder;
};
}

ToneMappingPass.cpp

#include "ToneMappingPass.h"

#include <d3d12.h>
#include <DirectXColors.h>

#include <CommandListExecutor/CommandListExecutor.h>
#include <DXUtils/d3dx12.h>
#include <ResourceManager\ResourceManager.h>
#include <ResourceStateManager\ResourceStateManager.h>
#include <ShaderUtils\CBuffers.h>
#include <Utils\DebugUtils.h>

namespace BRE {
void
ToneMappingPass::Init(ID3D12Resource& inputColorBuffer,
                      ID3D12Resource& outputColorBuffer,
                      const D3D12_CPU_DESCRIPTOR_HANDLE& renderTargetView) noexcept
{
    BRE_ASSERT(IsDataValid() == false);

    mInputColorBuffer = &inputColorBuffer;
    mOutputColorBuffer = &outputColorBuffer;

    ToneMappingCommandListRecorder::InitSharedPSOAndRootSignature();

    mCommandListRecorder.reset(new ToneMappingCommandListRecorder());
    mCommandListRecorder->Init(*mInputColorBuffer, renderTargetView);

    BRE_ASSERT(IsDataValid());
}

void
ToneMappingPass::Execute() noexcept
{
    BRE_ASSERT(IsDataValid());

    ExecuteBeginTask();

    CommandListExecutor::Get().ResetExecutedCommandListCount();
    mCommandListRecorder->RecordAndPushCommandLists();

    // Wait until all previous tasks command lists are executed
    while (CommandListExecutor::Get().GetExecutedCommandListCount() < 1) {         Sleep(0U);     } } bool ToneMappingPass::IsDataValid() const noexcept {     const bool b =         mCommandListRecorder.get() != nullptr &&         mInputColorBuffer != nullptr &&         mOutputColorBuffer != nullptr;     return b; } void ToneMappingPass::ExecuteBeginTask() noexcept {     BRE_ASSERT(IsDataValid());     CD3DX12_RESOURCE_BARRIER barriers[2U];     std::uint32_t barrierCount = 0UL;     if (ResourceStateManager::GetResourceState(*mInputColorBuffer) != D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE) {         barriers[barrierCount] = ResourceStateManager::ChangeResourceStateAndGetBarrier(*mInputColorBuffer,                                                                                         D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE);         ++barrierCount;     }     if (ResourceStateManager::GetResourceState(*mOutputColorBuffer) != D3D12_RESOURCE_STATE_RENDER_TARGET) {         barriers[barrierCount] = ResourceStateManager::ChangeResourceStateAndGetBarrier(*mOutputColorBuffer,                                                                                         D3D12_RESOURCE_STATE_RENDER_TARGET);         ++barrierCount;     }     if (barrierCount > 0UL) {
        ID3D12GraphicsCommandList& commandList = mCommandListPerFrame.ResetCommandListWithNextCommandAllocator(nullptr);
        commandList.ResourceBarrier(barrierCount, barriers);
        BRE_CHECK_HR(commandList.Close());
        CommandListExecutor::Get().ExecuteCommandListAndWaitForCompletion(commandList);
    }
}
}

What is the Post Process Pass?

In BRE, we have a type of command list recorder named PostProcessCommandListRecorder that generates a command list related with post processing effects and pushes it to the CommandListExecutor to be executed. This command list recorder needs an input texture which is the output texture of the ToneMappingPass. It writes to the frame buffer, that will be presented to the user. You can see a diagram of this pass in the following picture

post_process_workflow

Its implementation is the following

PostProcessCommandListRecorder.h

#pragma once

#include <CommandManager\CommandListPerFrame.h>

struct D3D12_CPU_DESCRIPTOR_HANDLE;
struct D3D12_GPU_DESCRIPTOR_HANDLE;
struct ID3D12Resource;

namespace BRE {
///
/// Responsible to generate command list recorderes for post processing effects (anti aliasing, color grading, etc).
///
class PostProcessCommandListRecorder {
public:
    PostProcessCommandListRecorder() = default;
    ~PostProcessCommandListRecorder() = default;
    PostProcessCommandListRecorder(const PostProcessCommandListRecorder&) = delete;
    const PostProcessCommandListRecorder& operator=(const PostProcessCommandListRecorder&) = delete;
    PostProcessCommandListRecorder(PostProcessCommandListRecorder&&) = default;
    PostProcessCommandListRecorder& operator=(PostProcessCommandListRecorder&&) = default;

    ///
    /// @brief Initializes pipeline state object and root signature
    ///
    /// This method must be called at the beginning of application, and once.
    ///
    static void InitSharedPSOAndRootSignature() noexcept;

    ///
    /// @brief Initializes the command list recorder
    ///
    /// InitSharedPSOAndRootSignature() must be called before
    ///
    /// @param inputColorBuffer Input color buffer
    ///
    void Init(ID3D12Resource& inputColorBuffer) noexcept;

    ///
    /// @brief Records and pushes command lists to CommandListExecutor
    ///
    /// Init() must be called first
    ///
    /// @param renderTargetView Render target view
    ///
    void RecordAndPushCommandLists(const D3D12_CPU_DESCRIPTOR_HANDLE& renderTargetView) noexcept;

    ///
    /// @brief Checks if internal data is valid. Typically, used with assertions
    /// @return True if valid. Otherwise, false
    ///
    bool IsDataValid() const noexcept;

private:
    ///
    /// @brief Initializes shader resource views
    /// @param inputColorBuffer Input color buffer
    ///
    void InitShaderResourceViews(ID3D12Resource& inputColorBuffer) noexcept;

    CommandListPerFrame mCommandListPerFrame;

    D3D12_GPU_DESCRIPTOR_HANDLE mStartPixelShaderResourceView{ 0UL };
};
}

PostProcessCommandListRecorder.cpp

#include "PostProcessCommandListRecorder.h"

#include <d3d12.h>
#include <DirectXMath.h>

#include <CommandListExecutor\CommandListExecutor.h>
#include <DescriptorManager\CbvSrvUavDescriptorManager.h>
#include <PSOManager/PSOManager.h>
#include <RootSignatureManager\RootSignatureManager.h>
#include <ShaderManager\ShaderManager.h>
#include <Utils/DebugUtils.h>

namespace BRE {
// Root Signature:
// "DescriptorTable(SRV(t0), visibility = SHADER_VISIBILITY_PIXEL)" 0 -> Color Buffer Texture

namespace {
ID3D12PipelineState* sPSO{ nullptr };
ID3D12RootSignature* sRootSignature{ nullptr };
}

void
PostProcessCommandListRecorder::InitSharedPSOAndRootSignature() noexcept
{
    BRE_ASSERT(sPSO == nullptr);
    BRE_ASSERT(sRootSignature == nullptr);

    PSOManager::PSOCreationData psoData{};
    psoData.mDepthStencilDescriptor = D3DFactory::GetDisabledDepthStencilDesc();

    psoData.mPixelShaderBytecode = ShaderManager::LoadShaderFileAndGetBytecode("PostProcessPass/Shaders/PS.cso");
    psoData.mVertexShaderBytecode = ShaderManager::LoadShaderFileAndGetBytecode("PostProcessPass/Shaders/VS.cso");

    ID3DBlob* rootSignatureBlob = &ShaderManager::LoadShaderFileAndGetBlob("PostProcessPass/Shaders/RS.cso");
    psoData.mRootSignature = &RootSignatureManager::CreateRootSignatureFromBlob(*rootSignatureBlob);
    sRootSignature = psoData.mRootSignature;

    psoData.mNumRenderTargets = 1U;
    psoData.mRenderTargetFormats[0U] = ApplicationSettings::sFrameBufferRTFormat;
    for (std::size_t i = psoData.mNumRenderTargets; i < _countof(psoData.mRenderTargetFormats); ++i) {
        psoData.mRenderTargetFormats[i] = DXGI_FORMAT_UNKNOWN;
    }
    psoData.mPrimitiveTopologyType = D3D12_PRIMITIVE_TOPOLOGY_TYPE_TRIANGLE;
    sPSO = &PSOManager::CreateGraphicsPSO(psoData);

    BRE_ASSERT(sPSO != nullptr);
    BRE_ASSERT(sRootSignature != nullptr);
}

void
PostProcessCommandListRecorder::Init(ID3D12Resource& inputColorBuffer) noexcept
{
    BRE_ASSERT(IsDataValid() == false);

    InitShaderResourceViews(inputColorBuffer);

    BRE_ASSERT(IsDataValid());
}

void
PostProcessCommandListRecorder::RecordAndPushCommandLists(const D3D12_CPU_DESCRIPTOR_HANDLE& renderTargetView) noexcept
{
    BRE_ASSERT(IsDataValid());
    BRE_ASSERT(sPSO != nullptr);
    BRE_ASSERT(sRootSignature != nullptr);

    ID3D12GraphicsCommandList& commandList = mCommandListPerFrame.ResetCommandListWithNextCommandAllocator(sPSO);

    commandList.RSSetViewports(1U, &ApplicationSettings::sScreenViewport);
    commandList.RSSetScissorRects(1U, &ApplicationSettings::sScissorRect);
    commandList.OMSetRenderTargets(1U, &renderTargetView, false, nullptr);

    ID3D12DescriptorHeap* heaps[] = { &CbvSrvUavDescriptorManager::GetDescriptorHeap() };
    commandList.SetDescriptorHeaps(_countof(heaps), heaps);

    commandList.SetGraphicsRootSignature(sRootSignature);
    commandList.SetGraphicsRootDescriptorTable(0U, mStartPixelShaderResourceView);

    commandList.IASetPrimitiveTopology(D3D_PRIMITIVE_TOPOLOGY_TRIANGLELIST);
    commandList.DrawInstanced(6U, 1U, 0U, 0U);

    commandList.Close();
    CommandListExecutor::Get().AddCommandList(commandList);
}

bool
PostProcessCommandListRecorder::IsDataValid() const noexcept
{
    const bool result = mStartPixelShaderResourceView.ptr != 0UL;

    return result;
}

void
PostProcessCommandListRecorder::InitShaderResourceViews(ID3D12Resource& inputColorBuffer) noexcept
{
    D3D12_SHADER_RESOURCE_VIEW_DESC srvDescriptor{};
    srvDescriptor.Shader4ComponentMapping = D3D12_DEFAULT_SHADER_4_COMPONENT_MAPPING;
    srvDescriptor.ViewDimension = D3D12_SRV_DIMENSION_TEXTURE2D;
    srvDescriptor.Texture2D.MostDetailedMip = 0;
    srvDescriptor.Texture2D.ResourceMinLODClamp = 0.0f;
    srvDescriptor.Format = inputColorBuffer.GetDesc().Format;
    srvDescriptor.Texture2D.MipLevels = inputColorBuffer.GetDesc().MipLevels;
    mStartPixelShaderResourceView = CbvSrvUavDescriptorManager::CreateShaderResourceView(inputColorBuffer,
                                                                                         srvDescriptor);
}
}

At the time of writing this article, the only post-processing technique BRE has is gamma correction. I followed a SIGGRAPH paper about Moving Frostbite to PBR where they implement an accurate linear to SRGB conversion. In the following picture, you can see in the left a frame without gamma correction and in the right a frame with gamma correction.

gamma_correction

Regarding shaders implementation, we use vertex and pixel shaders. we send a triangle list of 2 triangles, and in the vertex shader, we expand these triangles to match a full-screen quad dimensions in NDC space. Its implementation is the following

VertexShader

#include <ShaderUtils/CBuffers.hlsli>

#include "RS.hlsl"

struct Input {
    uint mVertexId : SV_VertexID;
};

static const float2 gQuadUVs[6] = {
    float2(0.0f, 1.0f),
    float2(0.0f, 0.0f),
    float2(1.0f, 0.0f),
    float2(0.0f, 1.0f),
    float2(1.0f, 0.0f),
    float2(1.0f, 1.0f)
};

struct Output {
    float4 mPositionNDC : SV_POSITION;
    float2 mUV : TEXCOORD0;
};

[RootSignature(RS)]
Output main(in const Input input)
{
    Output output;

    // Quad covering screen in NDC space ([-1.0, 1.0] x [-1.0, 1.0] x [0.0, 1.0] x [1.0])
    output.mUV = gQuadUVs[input.mVertexId];
    output.mPositionNDC = float4(2.0f * output.mUV.x - 1.0f,
                                 1.0f - 2.0f * output.mUV.y,
                                 0.0f,
                                 1.0f);

    return output;
}

In the pixel shader, as we mentioned, we apply the color transformation from linear space to SRGB space. The method that does this is accurateLinearToSRGB. Its implementation is the following

PixelShader

#include "RS.hlsl"

float3 accurateLinearToSRGB(in float3 linearCol)
{
    const float3 sRGBLo = linearCol * 12.92;
    const float3 sRGBHi = (pow(abs(linearCol), 1.0 / 2.4) * 1.055) - 0.055;
    const float3 sRGB = (linearCol <= 0.0031308) ? sRGBLo : sRGBHi;
    return sRGB;
}

struct Input {
    float4 mPositionScreenSpace : SV_POSITION;
    float2 mUV : TEXCOORD0;
};

Texture2D<float4> ColorBufferTexture : register(t0);

struct Output {
    float4 mColor : SV_Target0;
};

[RootSignature(RS)]
Output main(const in Input input)
{
    Output output = (Output)0;

    const int3 fragmentScreenSpace = int3(input.mPositionScreenSpace.xy, 0);
    output.mColor = ColorBufferTexture.Load(fragmentScreenSpace);
    output.mColor = float4(accurateLinearToSRGB(output.mColor.xyz),
                           1.0f);

    return output;
}

The pass responsible to create PostProcessCommandListRecorder, to execute it and to set resource barriers is PostProcessPass, and its implementation is the following

PostProcessPass.h

#pragma once

#include <memory>

#include <CommandManager\CommandListPerFrame.h>
#include <PostProcessPass\PostProcessCommandListRecorder.h>

namespace BRE {
///
/// @brief Pass responsible to apply post processing effects (anti aliasing, color grading, etc)
///
class PostProcessPass {
public:
    PostProcessPass() = default;
    ~PostProcessPass() = default;
    PostProcessPass(const PostProcessPass&) = delete;
    const PostProcessPass& operator=(const PostProcessPass&) = delete;
    PostProcessPass(PostProcessPass&&) = delete;
    PostProcessPass& operator=(PostProcessPass&&) = delete;

    ///
    /// @brief Initializes post process pass
    /// @param inputColorBuffer Input color buffer to apply post processing
    ///
    void Init(ID3D12Resource& inputColorBuffer) noexcept;

    ///
    /// @brief Execute post process pass
    ///
    /// Init() must be called first
    ///
    /// @param renderTargetBuffer Render target buffer
    /// @param renderTargetView Render target view
    ///
    void Execute(ID3D12Resource& renderTargetBuffer,
                 const D3D12_CPU_DESCRIPTOR_HANDLE& renderTargetView) noexcept;

private:
    ///
    /// @brief Checks if internal data is valid. Typically, used with assertions
    /// @return True if valid. Otherwise, false.
    ///
    bool IsDataValid() const noexcept;

    ///
    /// @brief Execute begin task for pass
    /// @param renderTargetBuffer Render target buffer
    /// @param renderTargetView Render target view
    ///
    void ExecuteBeginTask(ID3D12Resource& renderTargetBuffer,
                          const D3D12_CPU_DESCRIPTOR_HANDLE& renderTargetView) noexcept;

    CommandListPerFrame mCommandListPerFrame;

    ID3D12Resource* mInputColorBuffer{ nullptr };

    std::unique_ptr<PostProcessCommandListRecorder> mCommandListRecorder;
};
}

PostProcessPass.cpp

#include "PostProcessPass.h"

#include <d3d12.h>
#include <DirectXColors.h>

#include <CommandListExecutor/CommandListExecutor.h>
#include <DXUtils/d3dx12.h>
#include <ResourceStateManager\ResourceStateManager.h>
#include <Utils\DebugUtils.h>

using namespace DirectX;

namespace BRE {
void
PostProcessPass::Init(ID3D12Resource& inputColorBuffer) noexcept
{
    BRE_ASSERT(IsDataValid() == false);

    mInputColorBuffer = &inputColorBuffer;

    PostProcessCommandListRecorder::InitSharedPSOAndRootSignature();

    mCommandListRecorder.reset(new PostProcessCommandListRecorder());
    mCommandListRecorder->Init(inputColorBuffer);

    BRE_ASSERT(IsDataValid());
}

void
PostProcessPass::Execute(ID3D12Resource& renderTargetBuffer,
                         const D3D12_CPU_DESCRIPTOR_HANDLE& renderTargetView) noexcept
{
    BRE_ASSERT(IsDataValid());
    BRE_ASSERT(renderTargetView.ptr != 0UL);

    ExecuteBeginTask(renderTargetBuffer, renderTargetView);

    CommandListExecutor::Get().ResetExecutedCommandListCount();
    mCommandListRecorder->RecordAndPushCommandLists(renderTargetView);

    // Wait until all previous tasks command lists are executed
    while (CommandListExecutor::Get().GetExecutedCommandListCount() < 1) {
        Sleep(0U);
    }
}

bool
PostProcessPass::IsDataValid() const noexcept
{
    const bool b =
        mCommandListRecorder.get() != nullptr &&
        mInputColorBuffer != nullptr;

    return b;
}

void
PostProcessPass::ExecuteBeginTask(ID3D12Resource& renderTargetBuffer,
                                  const D3D12_CPU_DESCRIPTOR_HANDLE& renderTargetView) noexcept
{
    BRE_ASSERT(IsDataValid());
    BRE_ASSERT(renderTargetView.ptr != 0UL);    

    CD3DX12_RESOURCE_BARRIER barriers[2U];
    std::uint32_t barrierCount = 0UL;
    if (ResourceStateManager::GetResourceState(*mInputColorBuffer) != D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE) {
        barriers[barrierCount] = ResourceStateManager::ChangeResourceStateAndGetBarrier(*mInputColorBuffer,
                                                                                        D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE);
        ++barrierCount;
    }#include "PostProcessPass.h"

#include <d3d12.h>
#include <DirectXColors.h>

#include <CommandListExecutor/CommandListExecutor.h>
#include <DXUtils/d3dx12.h>
#include <ResourceStateManager\ResourceStateManager.h>
#include <Utils\DebugUtils.h>

        using namespace DirectX;

    namespace BRE {
    void
        PostProcessPass::Init(ID3D12Resource& inputColorBuffer) noexcept
    {
        BRE_ASSERT(IsDataValid() == false);

        mInputColorBuffer = &inputColorBuffer;

        PostProcessCommandListRecorder::InitSharedPSOAndRootSignature();

        mCommandListRecorder.reset(new PostProcessCommandListRecorder());
        mCommandListRecorder->Init(inputColorBuffer);

        BRE_ASSERT(IsDataValid());
    }

    void
        PostProcessPass::Execute(ID3D12Resource& renderTargetBuffer,
                                 const D3D12_CPU_DESCRIPTOR_HANDLE& renderTargetView) noexcept
    {
        BRE_ASSERT(IsDataValid());
        BRE_ASSERT(renderTargetView.ptr != 0UL);

        ExecuteBeginTask(renderTargetBuffer, renderTargetView);

        CommandListExecutor::Get().ResetExecutedCommandListCount();
        mCommandListRecorder->RecordAndPushCommandLists(renderTargetView);

        // Wait until all previous tasks command lists are executed
        while (CommandListExecutor::Get().GetExecutedCommandListCount() < 1) {             Sleep(0U);         }     }     bool         PostProcessPass::IsDataValid() const noexcept     {         const bool b =             mCommandListRecorder.get() != nullptr &&             mInputColorBuffer != nullptr;         return b;     }     void         PostProcessPass::ExecuteBeginTask(ID3D12Resource& renderTargetBuffer,                                           const D3D12_CPU_DESCRIPTOR_HANDLE& renderTargetView) noexcept     {         BRE_ASSERT(IsDataValid());         BRE_ASSERT(renderTargetView.ptr != 0UL);         CD3DX12_RESOURCE_BARRIER barriers[2U];         std::uint32_t barrierCount = 0UL;         if (ResourceStateManager::GetResourceState(*mInputColorBuffer) != D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE) {             barriers[barrierCount] = ResourceStateManager::ChangeResourceStateAndGetBarrier(*mInputColorBuffer,                                                                                             D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE);             ++barrierCount;         }         if (ResourceStateManager::GetResourceState(renderTargetBuffer) != D3D12_RESOURCE_STATE_RENDER_TARGET) {             barriers[barrierCount] = ResourceStateManager::ChangeResourceStateAndGetBarrier(renderTargetBuffer,                                                                                             D3D12_RESOURCE_STATE_RENDER_TARGET);             ++barrierCount;         }         if (barrierCount > 0UL) {
            ID3D12GraphicsCommandList& commandList = mCommandListPerFrame.ResetCommandListWithNextCommandAllocator(nullptr);
            commandList.ResourceBarrier(barrierCount, barriers);
            BRE_CHECK_HR(commandList.Close());
            CommandListExecutor::Get().ExecuteCommandListAndWaitForCompletion(commandList);
        }
    }
    }

    if (ResourceStateManager::GetResourceState(renderTargetBuffer) != D3D12_RESOURCE_STATE_RENDER_TARGET) {
        barriers[barrierCount] = ResourceStateManager::ChangeResourceStateAndGetBarrier(renderTargetBuffer,
                                                                                        D3D12_RESOURCE_STATE_RENDER_TARGET);
        ++barrierCount;
    }

    if (barrierCount > 0UL) {
        ID3D12GraphicsCommandList& commandList = mCommandListPerFrame.ResetCommandListWithNextCommandAllocator(nullptr);
        commandList.ResourceBarrier(barrierCount, barriers);
        BRE_CHECK_HR(commandList.Close());
        CommandListExecutor::Get().ExecuteCommandListAndWaitForCompletion(commandList);
    }
}
}

Future Work

  • Try different tone mapping operators and compare results
  • Add post processing effects like color grading, bloom, lens flare, antialiasing, etc
  • Advertisements

One thought on “BRE Architecture Series Part 10 – Tone Mapping Pass and Post Process Pass

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s