Calculating Per-Fragment Motion Vectors in GLSL

Motion vector generation calculates the 2D screen-space displacement of surfaces between consecutive frames to power post-processing effects such as temporal anti-aliasing (TAA) and motion blur. In GLSL, this process requires computing both the current and previous clip-space coordinates of a fragment, transforming them into Normalized Device Coordinates (NDC), and finding the delta between them. This article explains the underlying mathematics and GLSL implementation required to generate an accurate per-fragment velocity buffer.

The Underlying Mathematics

To find how far a fragment has traveled across the screen, the rendering pipeline must know where that exact world-space point was rendered in the previous frame compared to where it renders currently.

  1. Current Clip Coordinates: Calculated using the current model matrix and the current View-Projection matrix: \[\mathbf{P}_{\text{current}} = \mathbf{VP}_{\text{current}} \times \mathbf{M}_{\text{current}} \times \mathbf{v}\]
  2. Previous Clip Coordinates: Calculated using the previous frame's model matrix and previous View-Projection matrix: \[\mathbf{P}_{\text{previous}} = \mathbf{VP}_{\text{previous}} \times \mathbf{M}_{\text{previous}} \times \mathbf{v}_{\text{previous}}\]
  3. Perspective Division: Both positions are converted from 4D homogeneous clip coordinates into 3D Normalized Device Coordinates (NDC) by dividing by their respective \(w\) components: \[\mathbf{NDC} = \frac{\mathbf{P}.xy}{\mathbf{P}.w}\]
  4. Screen-Space Displacement: The motion vector is the difference between current and previous NDC positions: \[\mathbf{V} = \mathbf{NDC}_{\text{current}} - \mathbf{NDC}_{\text{previous}}\]

Vertex Shader Implementation

The vertex shader is responsible for computing positions in both coordinate frames and passing them as interpolated outputs to the fragment shader.

#version 450 core

layout(location = 0) in vec3 aPos;

uniform mat4 u_CurrentVP;
uniform mat4 u_CurrentModel;
uniform mat4 u_PreviousVP;
uniform mat4 u_PreviousModel;

out vec4 v_CurrentClipPos;
out vec4 v_PreviousClipPos;

void main()
{
    vec4 worldPos = u_CurrentModel * vec4(aPos, 1.0);
    v_CurrentClipPos = u_CurrentVP * worldPos;

    vec4 prevWorldPos = u_PreviousModel * vec4(aPos, 1.0);
    v_PreviousClipPos = u_PreviousVP * prevWorldPos;

    gl_Position = v_CurrentClipPos;
}

Passing the raw clip-space positions (vec4) ensures that perspective interpolation across the triangle surface remains linear and mathematically correct. Performing the perspective divide (\(/ w\)) inside the vertex shader would produce non-linear interpolation artifacts.

Fragment Shader Implementation

The fragment shader receives the interpolated clip positions, completes the perspective division to obtain 2D screen positions, and outputs the displacement vector into the velocity target.

#version 450 core

in vec4 v_CurrentClipPos;
in vec4 v_PreviousClipPos;

layout(location = 0) out vec2 out_Velocity;

void main()
{
    // Convert clip space to Normalized Device Coordinates [-1, 1]
    vec2 currentNDC = v_CurrentClipPos.xy / v_CurrentClipPos.w;
    vec2 previousNDC = v_PreviousClipPos.xy / v_PreviousClipPos.w;

    // Calculate displacement in NDC space
    vec2 velocity = currentNDC - previousNDC;

    // Optional: Convert from NDC [-1, 1] to UV texture coordinates [0, 1]
    // velocity *= 0.5;

    out_Velocity = velocity;
}

Key Considerations

  • Handling Camera Jitter: If implementing TAA, camera projection matrices typically incorporate sub-pixel jitter offsets. When generating velocity buffers for TAA reconstruction, remove the sub-pixel jitter from both current and previous projection matrices; otherwise, the motion vectors will capture artificial camera shake.
  • Static Geometry Optimization: For non-moving world geometry, the previous model matrix is identical to the current model matrix. A separate shader pass can reconstruct previous positions entirely from depth buffer values and an inverse view-projection matrix, saving memory bandwidth.
  • Target Format: Velocity buffers should use a 16-bit or 32-bit floating-point format (such as GL_RG16F or GL_RG32F) to retain the sub-pixel precision necessary for smooth post-processing integration.