Per-Pixel Motion Blur Reconstruction in DirectX
Motion blur reconstruction in modern DirectX graphics pipelines simulates camera exposure over time by using a 2D velocity buffer, commonly known as a motion vector buffer. Instead of performing costly multi-sample accumulation across temporal sub-frames, the rendering engine generates per-pixel vectors that describe screen-space movement between the current and previous frames. A specialized post-processing shader then reads these vectors, reconstructs the direction and magnitude of motion, and gathers filtered samples along the trajectory to create physically plausible motion blur in real time.
1. Generating Per-Pixel Velocity Vectors
The velocity vector for each pixel represents its displacement in
screen space from the previous frame to the current frame. In DirectX
(Direct3D 11 and 12), this data is generated during geometry rendering
(either in the G-Buffer pass of a deferred renderer or a dedicated
pre-pass/depth-velocity pass) and written to a target typically
formatted as DXGI_FORMAT_R16G16_FLOAT.
For static geometry, screen-space movement is driven solely by camera translation and rotation:
- The vertex shader receives the model-space vertex position.
- The vertex is transformed by the current frame's World-View-Projection matrix to obtain current clip-space coordinates.
- The same vertex position is transformed by the previous frame's View-Projection matrix to compute historical clip-space coordinates.
For dynamic or skinned geometry (such as characters), the vertex shader requires two sets of transformation matrices—the current bone matrices and the previous frame's bone matrices.
In the pixel shader, both clip-space positions are converted to Normalized Device Coordinates (NDC) via perspective division:
float2 currentNDC = input.CurrentClipPos.xy / input.CurrentClipPos.w;
float2 previousNDC = input.PrevClipPos.xy / input.PrevClipPos.w;
// Screen-space velocity in UV coordinates
float2 velocity = (currentNDC - previousNDC) * float2(0.5f, -0.5f);
return velocity;
Engines often optimize static object rendering by omitting them from the dynamic velocity target entirely. Instead, the post-process pass computes static velocity on the fly by reconstructing the world-space position from the current depth buffer and projecting it using the previous frame's camera matrices.
2. Handling Discontinuities: The Tile-Max and Neighbor-Max Passes
A naive gather pass that simply blurs along the pixel's local velocity vector produces visual artifacts, such as sharp background pixels bleeding over blurry foreground edges, or foreground silhouettes failing to blur outward onto static backgrounds. Modern reconstruction techniques (such as McGuire’s tile-based motion blur) resolve this using intermediate dispatch passes in DirectX compute shaders:
- Tile-Max Classification: The screen is divided into tiles (typically \(16 \times 16\) pixels). A compute shader finds the vector with the largest magnitude within each tile and outputs it to a low-resolution texture.
- Neighbor-Max Filter: A second pass samples a \(3 \times 3\) neighborhood of the tile-max texture to identify the maximum velocity vector in adjacent screen regions. This identifies whether a rapidly moving object is about to move into or out of the current pixel’s neighborhood.
3. Reconstruction and Sample Weighting
The final reconstruction pass is executed via a full-screen pixel shader or compute shader. The shader takes the color buffer, depth buffer, and velocity vectors as inputs.
For each pixel:
- The shader compares the pixel's own velocity vector with the neighbor-max velocity vector.
- It determines whether the pixel represents a foreground object moving over a background, a background being revealed, or a uniformly moving surface.
- The shader steps along the dominant vector direction across multiple samples (typically between 8 and 16 taps), often applying a pseudo-random jitter offset to distribute banding artifacts into high-frequency noise.
Each sample's contribution to the final accumulated color is weighted using depth and velocity comparisons:
- Depth Weight: Prevents background samples from blurring on top of foreground objects.
- Spread Weight: Ensures that pixels only contribute to the blur if their own velocity aligns with the sampling trajectory.
The weighted color samples are normalized and written to the output render target, completing the reconstruction of optical motion blur within a single frame budget.