Chain full-screen compute passes to apply blur, desaturation and other image effects at the end of the render.
In this tutorial you will add a full-screen post-processing effect to your render: a Gaussian blur followed by a desaturation. A post-processing pass is a compute effect applied to the whole image, after the color and bloom passes, and before anti-aliasing.
The renderer keeps an ordered list of passes: each one reads the image produced by the previous pass and writes into its own output. The chain is therefore built in the order of your addPostprocessing() calls.
A post-processing pass derives from FullScreenCompute. For simple effects you don't even need to subclass it: just instantiate the class with the name of a compiled compute shader and, optionally, a uniform data block.
FullScreenCompute(
const RendererConfiguration& config,
vireo::ImageFormat outputFormat,
const std::string& compShaderName, // pass identifier
void* data = nullptr, // uniform payload (optional)
uint32 dataSize = 0,
const std::string& name = "");
The shader receives a fixed set of inputs through a texture array whose indices are exposed as constants:
| Constant | Contents |
|---|---|
INPUT_BUFFER | HDR color image (output of the previous pass) |
DEPTH_BUFFER | Depth buffer |
BLOOM_BUFFER | Bloom result |
OPT1_BUFFER / OPT2_BUFFER | Free slots, defined by the pass |
The compute local workgroup size is TILE_SIZE = 16. It must stay consistent with your shader (see postprocess.inc.slang).
Passes must outlive the Renderer: declare them as persistent members of your scene, never as local variables. The add order determines the apply order.
// Persistent scene members
lysa::BlurData blurData;
lysa::FullScreenCompute blur(
renderTarget.getRendererConfiguration(),
renderTarget.getImageFormat(),
"blur", // compiled compute shader in shaders/
&blurData, // uniform block
sizeof(lysa::BlurData));
lysa::FullScreenCompute greyscale(
renderTarget.getRendererConfiguration(),
renderTarget.getImageFormat(),
"greyscale"); // no data: data == nullptr
auto& renderer = renderTarget.getRenderer();
renderer.addPostprocessing(blur); // applied first
renderer.addPostprocessing(greyscale); // then on the blur result
On registration, addPostprocessing() opens a command list, calls resize() on the pass to allocate its output image at the current extent, then appends the pointer to the list. The pass is ready on the next frame.
The uniform block pointed to at construction is re-uploaded every frame: it is enough to modify the CPU-side struct. For resolution-dependent parameters — such as the blur radius in pixels — recompute them on the resize event.
void MyScene::onResize(const vireo::Extent& extent) {
blurData.update(extent, 2.0f); // radius in pixels
}
The BlurData struct pre-computes the normalized Gaussian weights and the texel size, which are then read directly by the shader.
A pass is identified by its compute shader name. You can remove it by name or by reference to the object:
renderer.removePostprocessing("greyscale"); // by shader name
renderer.removePostprocessing(blur); // by pass reference
Because identification is by shader name, two passes sharing the same compute shader cannot be distinguished: removing by name removes all matching passes.
DisplayAttachment is a specialized post-processing pass that copies an internal attachment to the output. Handy for inspecting a G-Buffer layer, ambient occlusion, or a shadow map while debugging.
lysa::DisplayAttachment display(
renderTarget.getRendererConfiguration(),
renderTarget.getImageFormat());
renderer.addPostprocessing(display);
// Pick the attachment to visualize
display.setAttachment(
dynamic_cast<lysa::DeferredRenderer&>(renderer)
.getAOPass().getColorAttachment(frameIndex));
(1) Passes belong to the caller and must outlive the Renderer. (2) Adding sizes the pass automatically via resize(). (3) The add order fixes the apply order.