DirectXTK |
---|
Post-processing is a common technique applied to 3D rendering to achieve various effects. It's image-based, so it's performance is based on the size of the 2D render target rather than the complexity of the scene. The post-processing implementation in the DirectX Tool Kit performs common effects like monochrome and bloom, as well tone mapping which is essential to High Dynamic Range (HDR) rendering.
- BasicPostProcess supports post-processing that takes a single input texture such as monochrome conversion or blurring.
- DualPostProcess supports post-processing that operates on two images such as merging/blending.
- ToneMapPostProcess supports tone-map operations for HDR images such as the Reinhard operator. It also supports the HDR10 signal preparation needed for true 4k UHD wide color gamut rendering.
Related tutorial: Using HDR rendering
classDiagram
class IPostProcess{
<<Interface>>
+Process()
}
class BasicPostProcess{
+SetSourceTexture
}
IPostProcess <|-- BasicPostProcess
class DualPostProcess{
+SetSourceTexture
+SetSourceTexture2
}
IPostProcess <|-- DualPostProcess
class ToneMapPostProcess{
+SetOperator
+SetTransferFunction
}
IPostProcess <|-- ToneMapPostProcess
Header
#include "PostProcess.h"
Initialization
The built-in post-processors typically require only the device.
postProcess = std::make_unique<BasicPostProcess>(device);
Usage
To make use of post-processing, you typically render the scene to a offscreen render texture.
CD3D11_TEXTURE2D_DESC sceneDesc(
DXGI_FORMAT_R16G16B16A16_FLOAT, width, height,
1, 1, D3D11_BIND_RENDER_TARGET | D3D11_BIND_SHADER_RESOURCE);
DX::ThrowIfFailed(
device->CreateTexture2D(&sceneDesc, nullptr, sceneTex.GetAddressOf())
);
DX::ThrowIfFailed(
device->CreateShaderResourceView(sceneTex.Get(), nullptr,
sceneSRV.ReleaseAndGetAddressOf())
);
DX::ThrowIfFailed(
device->CreateRenderTargetView(sceneTex.Get(), nullptr,
sceneRT.ReleaseAndGetAddressOf()
));
Instead of rendering to the usual render target that is created as part of the DXGI swap chain, you set the offscreen texture as your scene render target:
context->ClearRenderTargetView(sceneRT.Get(), color);
context->ClearDepthStencilView(...);
context->OMSetRenderTargets(1, sceneRT.GetAddressOf(), depthStencilView.Get());
Then you render the scene as normal. When the scene is fully rendered, you then change the render target and use the previously generated render texture as your source texture (and you don't use a depth/stencil buffer for the post-processing):
context->OMSetRenderTargets(1, m_renderTargetView.GetAddressOf(), nullptr);
postProcess->SetEffect(BasicPostProcess::Sepia);
postProcess->SetSourceTexture(sceneSRV.Get());
postProcess->Process(context);
In some cases, you will perform several post-processing passes between various off-screen render targets before applying the final pass to the swapchain render target for presentation.
You can make use of the RenderTexture helper to manage the offscreen render target.
Interface
The post-processing system provides a IPostProcess
interface to simplify use. The only method in this interface is Process
which is expected to execute the post-processing pass with the result placed in the currently bound render target.
void Process(
ID3D11DeviceContext* deviceContext,
std::function<void()> setCustomState = nullptr);
Process relies on the correct render target already being set on the context, as well the correct viewport and scissor rects.
Custom render states
You modify the render state during post-processing by passing a setCustomState
callback:
postProcess->Process(context, [=]
{
ID3D11SamplerState* samplerState = states.AnsiotropicClamp();
deviceContext->PSSetSamplers(0, 1, &samplerState);
});
Example
A Bloom or Glow post-processing effect can be achieved with the following series of post-processing passes:
basicPostProcess = std::make_unique<BasicPostProcess>(device);
dualPostProcess = std::make_unique<DualPostProcess>(device);
// Scene is rendered to a render texture that is associated with sceneSRV
// blur1RT, blur1SRV is a render texture typically half-sized
// in both width & height of the original scene to save memory
// blurRT2, blur2SRV is another half-sized render texture
// Pass 1 (scene->blur1)
basicPostProcess->SetEffect(BasicPostProcess::BloomExtract);
basicPostProcess->SetBloomExtractParameter(0.25f);
auto blurRT1 = blur1RT.Get();
context->OMSetRenderTargets(1, &blurRT1, nullptr);
basicPostProcess->SetSourceTexture(m_sceneSRV.Get());
basicPostProcess->Process(context);
// Pass 2 (blur1 -> blur2)
basicPostProcess->SetEffect(BasicPostProcess::BloomBlur);
basicPostProcess->SetBloomBlurParameters(true, 4.f, 1.f);
auto blurRT2 = blur2RT.Get();
context->OMSetRenderTargets(1, &blurRT2, nullptr);
basicPostProcess->SetSourceTexture(blur1SRV.Get());
basicPostProcess->Process(context);
// Pass 3 (blur2 -> blur1)
basicPostProcess->SetBloomBlurParameters(false, 4.f, 1.f);
ID3D11ShaderResourceView* nullsrv[] = { nullptr, nullptr };
context->PSSetShaderResources(0, 2, nullsrv);
context->OMSetRenderTargets(1, &blurRT1, nullptr);
basicPostProcess->SetSourceTexture(blur2SRV.Get());
basicPostProcess->Process(context);
// Pass 4 (scene+blur1 -> rt)
dualPostProcess->SetEffect(DualPostProcess::BloomCombine);
dualPostProcess->SetBloomCombineParameters(1.25f, 1.f, 1.f, 1.f);
auto renderTarget = m_deviceResources->GetRenderTargetView();
context->OMSetRenderTargets(1, &renderTarget, nullptr);
dualPostProcess->SetSourceTexture(m_sceneSRV.Get());
dualPostProcess->SetSourceTexture2(blur1SRV.Get());
dualPostProcess->Process(context);
Feature level Notes
The built-in post-processing shaders rely on Direct3D hardware feature level 10.0 or greater. This allows the vertex shader to self-generate a full-screen rectangle without requiring any vertex buffer to be bound using the built-in SV_VertexId
system value. Applications should either have a minimum supported feature level of 10.0 or greater, or provide a run-time fallback (perhaps turning off post-processing effects or making use of simplified effects via SpriteBatch instead).
Threading model
Post-processing typically takes place on the immediate context typically after the full scene has been rendered, so it should be done single-threaded.
State management
When Process
is called on an effect, it will set the states needed to render and perform the operation by drawing a full-screen rectangle. Existing state is not save or restored. For efficiency, it simply sets the state it requires to render and assumes that any subsequent rendering will overwrite state that it needs.
Most post-process effects make use of the following states:
- BlendState
- Constant buffer (Pixel Shader stage, slot 0)
- DepthStencilState
- Pixel shader
- Primitive topology
- RasterizerState
- SamplerState (Pixel Shader stage, slot 0)
- Shader resources (Pixel Shader stage, slot 0; optionally slot 1)
- Vertex shader
Further reading
For Use
- Universal Windows Platform apps
- Windows desktop apps
- Windows 11
- Windows 10
- Windows 8.1
- Windows 7 Service Pack 1
- Xbox One
Architecture
- x86
- x64
- ARM64
For Development
- Visual Studio 2022
- Visual Studio 2019 (16.11)
- clang/LLVM v12 - v18
- MinGW 12.2, 13.2
- CMake 3.20
Related Projects
DirectX Tool Kit for DirectX 12
Tools
All content and source code for this package are subject to the terms of the MIT License.
This project has adopted the Microsoft Open Source Code of Conduct. For more information see the Code of Conduct FAQ or contact opencode@microsoft.com with any additional questions or comments.