Post-Processing: The Triangle Trick
Today will be a short post explaining a trick I've learned to implement effects such as bloom and motion blur in 3D games. Note: this post assumes the reader has a basic knowledge of what post-processing is in games, and how to code them in shaders.
So, first, let's begin at our render target. Most of the work has been done for our effects: we took a snap of what's on-screen, saved it in a texture, done the desired operations on the desired pixels, and now we want that new output on the screen. We've done the heavy lifting, but now want to apply it to the game and render it. Well, every texture needs to be set to a mesh...but what are we going to do with a texture that contains a whole frame's worth of data? We could create a giant, screen-covering flat mesh in C++ to set the texture to, but we don't have to.
Something will be drawn for the texture to be applied to, but not a mesh. The Draw() method doesn't really need vertex or index buffers. So, if no buffers are set, we can still call deviceContext->Draw(N,0), which will tell the vertex shader to execute N times. What's the point, you may ask. What is the vertex shader input going to look like? It will be an unisigned int branded with the SV_VertexID semantic; so, the int will be the index of the vertex. If we have that index, we can generate a UV from it, and a corresponding coordinate position on the screen.
The idea is to make a single triangle big enough to cover the entire screen. We want it to be twice the width and height of the screen, and to have UVs in the range of (0,0) to (2,2). When given the screen coordinates, we want to make a triangle like so:
Here is the vertex shader code that would create the triangle, dubbed "PPVS":
VertexToPixel main(uint id : SV_VertexID)
// Calculate the UV via the id
output.uv = float2((id << 1) & 2, id & 2);
// Convert the UV to the (-1, 1) to (3, -3) range for position
output.position = float4(output.uv, 0, 1);
output.position.x = output.position,x * 2 -1;
output.position.y = output.position.y * -2 + 1;
After this, polish whatever is needed in the Post-Process Pixel shader, and then create the necessary resources in the C++ code.
This was part of the process my team used to create bloom on our game Swamped, which can be viewed on the Project page.