TAA - Jitter - Advanced Rendering - Shader Learning

Advanced Rendering

TAA - Jitter

Task

The current Temporal Anti-Aliasing (TAA) implementation blends the current frame with the previous one using velocity-based. However, it fails to improve image quality in static scenes, where no motion vectors are present.


In this task, you will fix that by introducing sub-pixel jitter via the projection matrix.

Provided

  • projectionMatrix: the unmodified projection matrix.
  • rx and ry: per-frame random values in the range $[−1, +1]$.
  • iResolution: screen resolution.

Theory

Imagine a static scene: the camera isn’t moving, the object isn’t moving, and the fragment you are rendering lands on the exact same pixel as in the previous frame. In this case, Temporal Anti-Aliasing (TAA) has no new information to work with. It is just blending the same sample over and over again. So it can’t resolve sub-pixel detail or smooth out jagged edges.


The Problem

  • Aliasing happens at sub-pixel level: Thin lines, sharp edges, and high-frequency details fall between pixel centers.
  • If the sample position doesn’t change, TAA sees the same data every frame.
  • No motion = no variation = no anti-aliasing.

Jittering


To break this deadlock, we introduce jittering - a tiny, controlled offset to the camera’s projection matrix each frame. This causes the entire image to shift by a fraction of a pixel, even if the scene is static.


As result, the same fragment lands on a slightly different pixel in each frame. Over time, TAA collects multiple samples from slightly different positions and blends them to reconstruct smoother geometry and finer detail.


Use the Projection Matrix for Jittering


Instead of offsetting UVs or screen-space positions manually, we can inject jitter directly into the projection matrix. This has several advantages:

  • Scene-wide consistency: all geometry is shifted uniformly.
  • Perspective-correct: the offset is scaled properly with depth.
  • No extra logic in shaders: jitter is baked into the camera transform.

In a standard perspective projection matrix, the third column (index $[2][0]$ and $[2][1]$ ) controls the center of projection in clip space. By modifying these values, we shift the projected image slightly in $X$ and $Y$.

mat4 jitteredProj = originalProj;
jitteredProj[2][0] += jitter.x; // Horizontal shift
jitteredProj[2][1] += jitter.y; // Vertical shift

This shift is multiplied by the vertex’s $z$ during projection, and then divided by the same $z$ during the perspective divide. The result: a constant sub-pixel offset in screen space, regardless of depth. Exactly what TAA needs to gather varied samples over time.


Compute the Jitter Offset


To achieve a half-pixel shift in screen space, we must convert the jitter to normalized device coordinates (NDC). Since NDC ranges from $−1$ to $+1$, one pixel corresponds to $2.0 / resolution$ . Therefore, a half-pixel jitter is:

// Sub-pixel offset
jitter = vec2(rx, ry) / iResolution.xy; 

Where rx and ry are random values in the range $[−1, +1]$, typically generated per frame using a low-discrepancy sequence or hash-based noise.


Do Not Apply Jitter to Velocity Calculation


While jittering the projection matrix is essential for sub-pixel sampling in TAA, we must not apply jitter when computing motion vectors or velocity buffers. If jitter is applied to both the current and previous projection matrices, the resulting motion vectors will include artificial displacement caused by the jitter itself, but not actual object or camera movement. This introduces noise into the velocity buffer and leads to unstable reprojection.


Practical notes


In practice, it’s often more efficient to compute the jittered projection matrix on the CPU and pass it to the shader as a uniform. This avoids recomputing jitter in the shader and keeps the vertex pipeline clean. It also allows for centralized control over jitter amplitude, sequence type, and frame indexing.