Dithering in Motion: Keeping the Pattern Stable Across Video Frames
Why dithered footage crawls under camera motion, and the settings that fix it.
01/ ARTICLE
What Makes a Dithered Pattern Go Unstable in Motion?
A dither pattern that looks locked-in on a still frame can crawl, swim, or flicker the second the camera or the subject moves. Why: most dithering implementations key the pattern to a fixed reference, the screen's pixel grid, instead of to whatever is actually moving through the frame. Temporal dithering is dithering where the applied pattern changes from frame to frame instead of staying fixed. This piece walks through why the crawl happens, what surface-stable and animated approaches change about it, whether blue noise actually helps, a real production case (Return of the Obra Dinn), and the settings worth reaching for once you're exporting dithered footage instead of a single frame. Haven't settled on a base algorithm yet? Atkinson dithering settings is the place to start before you layer motion on top.
Why Does Screen-Space Dithering Crawl When the Camera Moves?
Most dither libraries, including the common dither2x2/dither4x4/dither8x8 GLSL functions, take a pixel position, gl_FragCoord.xy, and a brightness value, and return a thresholded output. That pixel position is screen-space. It's fixed to the display, not to the object being shaded. So when the camera pans or the subject moves, the threshold matrix stays glued to the screen while the image content slides underneath it. The same matrix cell that was thresholding an eyebrow last frame is thresholding empty background this frame, so dots re-tile and swim instead of staying attached to what they're shading. That's the crawl, sometimes called boiling.
Why bigger matrices make it worse, not better
An 8x8 Bayer matrix gives you more threshold levels and less banding than a 4x4 or 2x2. That's usually the right tradeoff for a still image. In motion, under a screen-space setup, the larger matrix is also a larger, more visible repeating unit, so instability in an 8x8 pattern reads as bigger, slower-moving artifacts than the same instability in a 4x4. Already fighting crawl and can't switch to a surface-stable technique? Dropping matrix size is a real, if blunt, mitigation. For the algorithm-level tradeoffs between ordered and error-diffusion dithering in the first place, see Floyd-Steinberg vs ordered dithering.
How Do You Keep a Bayer Pattern Stable Across Frames?
The fix isn't a better random seed. It's changing what the pattern is keyed to. Surface-stable fractal dithering keys the pattern to the surface or object instead of the screen, and enforces one rule as that surface scales: dots may only be added as the pattern enlarges, and only removed as it shrinks, never both at once. That single constraint is what stops the simultaneous appearing-and-disappearing of dots that causes the crawl in the first place.
The trick that makes it possible
This works because Bayer matrices have a fractal, self-similar property. Quadrants of the matrix align when compared at different scales, so a zoomed dither pattern is a strict subset or superset of the pattern at another zoom level, not an unrelated re-randomization. That self-similarity is exactly what the add-only, remove-only rule needs to hold.
Settings this exposes
- Dot Scale — exponentially scales dot size
- Dot Size Variability — 0 means shading changes dot count, 1 means shading changes dot size
- Dot Contrast — value 1 gives clean anti-aliasing at pattern edges
- Stretch Smoothness — controls anisotropic dot smoothing when a surface stretches
The tradeoff: this is a surface-space technique. It needs to know where the surface is (a 3D scene, a tracked plane, or similar) rather than treating the input as a flat 2D image, so it isn't a drop-in swap for a plain 2D dither shader on footage with no depth or tracking data.
What's the Flicker Tradeoff With Animated (Temporal) Dithering?
No surface tracking? The other lever is animating the pattern itself. bayer-matrix-dithering.glsl exposes this directly: an 8x8 Bayer matrix (values 0-63), with an animate parameter from 0.0 to 1.0 and a dither_size parameter from 0.0 to 0.95. The matrix lookup position shifts each frame using Scale = 3.0 + mod(2.0 * FrameCount, 32.0) * animate + dither_size, cycling the pattern across 32 frames.
At animate = 0, the pattern is static and locked to the screen, the crawl case from above. Raise animate toward 1.0 and the pattern rotates through variation every frame instead of staying pinned. That breaks up the fixed-grid look, but it comes at the cost of reintroducing flicker.
Why flicker is worse than it sounds
Temporal dithering varies the applied pattern so its time average approaches the true, undithered value. Here's the catch: human vision is most sensitive to flicker in the 4-30Hz range, peaking around 15Hz. A naive per-frame randomization dumps noise energy right into the range your eyes are built to catch. Cycle length and animate strength need to be tuned against your target frame rate, not left at whatever a shader library ships as default.
Is Blue Noise More Stable Than Bayer for Motion?
Blue noise has no visible repeating structure, which makes a single frame look cleaner than an obviously-tiled Bayer pattern. But it isn't naturally self-similar across scale the way Bayer matrices are. A zoomed or scaled blue noise pattern doesn't decompose cleanly into a stable subset the way surface-stable Bayer does.
That's still an open research question, not a solved one. Spatio-temporal dithering work presented at EGSR 2025 benchmarks screen-space and surface-based blue noise against Bayer-derived methods and Unreal Engine's dithered temporal anti-aliasing, rather than treating blue noise as a settled default. Deciding what to reach for today? Treat blue noise as a look choice for its clean single-frame appearance, not a guaranteed stability fix for footage.
Which Approach Should You Actually Use?
Here's how the main approaches stack up once motion is in the picture instead of a single still frame.
| Approach | Stability Under Motion | Flicker Risk | Setup Cost | Best For |
|---|---|---|---|---|
| Static screen-space Bayer | Poor — crawls with any camera/subject motion | None (pattern never changes) | Lowest — default in most shaders | Static shots, no camera or subject movement |
| Animated screen-space Bayer (temporal) | Moderate — breaks fixed-grid look, still screen-locked | Moderate to high, tunable via animate | Low — one extra parameter | Fast cuts and quick pans, where flicker reads as texture not distraction |
| Surface-stable fractal Bayer | Good — dots stay locked to the object across scale and movement | Low | Higher — needs surface/depth data | Slow pans, product turntables, anything with usable 3D or tracking |
| Screen-space blue noise | Poor, same limitation as screen-space Bayer | Low (no repeating structure to crawl visibly) | Low | Static shots where a non-repeating look matters more than motion |
| Surface-based blue noise | Experimental — active research, not yet standard | Low | High, research-stage tooling | Teams already deep in a custom rendering pipeline |
How Did Obra Dinn Fix Its Own Flickering Dither?
Return of the Obra Dinn is a real, shipped example of exactly this problem. Lucas Pope's team ran a fixed 1-bit dither pattern at 640x360 for most of development, and by 2018, four years in, Pope found the pattern uncomfortable to look at for long play sessions once the game was stretched to fill a modern monitor, describing it as a flickering problem tied to how large and unstable each dithered pixel read at that size.
The fix had two parts. First, a new dithering technique that "attempted to unify the 3D camera with the game's 2D dither patterns, making the output less flickery without affecting the style," the same category of fix as the surface-stable approach above: tie the pattern to the camera and 3D surfaces instead of the flat screen. Second, they raised output resolution from 640x360 to 800x450, which alone reduced how large and aliased each dither cell appeared. Between the two changes, the pattern held together under camera motion without abandoning the look. For the full breakdown of the aesthetic itself, see the Obra Dinn look, reverse-engineered with settings.
What Settings Actually Hold Up When You're Exporting Live?
For VJ work, the honest answer depends on what the footage is doing. Fast cuts and quick pans hide flicker well, an audience watching a 4-second clip rarely registers a 15Hz shimmer, so an animated screen-space Bayer setup with a moderate animate value is a reasonable, low-setup default for a live set. Slow pans and product-style turntables expose crawl immediately. That's where surface-stable dithering earns its higher setup cost: if your rig already has depth or tracking data (from a 3D scene, a depth camera, or a tracked plane), use it.
Matrix size follows the same logic as the crawl section above: an 8x8 matrix under surface-stable dithering is a safe default because the stability comes from the keying, not the size. Under plain screen-space dithering, that same 8x8 matrix just makes the crawl bigger and slower, so drop to a 4x4 there if crawl is visible and surface tracking isn't an option. Building out a full audio-reactive rig around this? A working TouchDesigner audio-reactive setup covers the MIDI and beat-detection side.
Frequently Asked Questions
Why does my dither pattern look like it's crawling or boiling when the camera moves?
Because the pattern is almost certainly screen-space, keyed to fixed pixel coordinates rather than the surface or subject. As the subject moves under a static pixel grid, the same threshold matrix lands on different parts of the image every frame, so dots appear to swim and re-tile instead of staying attached to what they're shading. Surface-stable approaches fix this by keying the pattern to the object's position and scale instead of the screen.
What's the actual tradeoff between a static dither pattern and an animated one?
A static pattern never flickers frame to frame, but under camera motion it reads as a fixed grid painted over a moving scene, the crawling look. Animating the pattern by cycling the matrix offset every frame breaks that fixed-grid look, but reintroduces frame-to-frame flicker, which is most visible to the human eye in the 4-30Hz range, peaking near 15Hz. There's no setting that eliminates both; you're choosing which artifact your footage can tolerate.
Is blue noise dithering more stable in motion than a Bayer matrix?
Not automatically. Blue noise has no visible repeating structure, which makes single frames look cleaner, but Bayer matrices have a fractal self-similarity property that blue noise doesn't naturally have, meaning it's hard to build a blue noise pattern that stays self-consistent as it scales the way surface-stable Bayer dithering does. Research presented at EGSR 2025 is still actively comparing screen-space and surface-based blue noise against Bayer-derived methods, not treating blue noise as a settled default for motion stability.
What matrix size should I use for dithering video without the pattern falling apart?
Bigger matrices, 8x8 over 4x4 or 2x2, reduce banding and give more threshold levels, but the repeating unit is also larger and more visible when it's unstable, so a big matrix on a screen-space setup makes crawl worse, not better. If your pipeline supports surface-stable or spatio-temporal dithering, an 8x8 base matrix with a slow animation rate is a reasonable starting point. Stuck with plain screen-space dithering, a smaller matrix crawls less noticeably even though it bands more.
How did Return of the Obra Dinn solve its own dither stability problem?
Lucas Pope's team shipped a fixed 1-bit dither pattern at 640x360 for most of development, and found it uncomfortable and flickery once stretched to fill a modern monitor during long play sessions. Their fix tied the dither pattern to the 3D camera and surfaces rather than the flat 2D screen, the same category of fix as surface-stable dithering, and they also raised output resolution to 800x450, which reduced how large and unstable each dither cell appeared.
Getting This Stable in Your Own Footage
Pattern stability in motion is a keying choice, screen versus surface, before it's ever a matrix-size choice. Flicker versus crawl is a tradeoff you tune, not one you eliminate. Start from what data your pipeline actually has: no depth or tracking, expect crawl and mitigate with matrix size and animate rate; usable surface data, spend the setup cost on surface-stable dithering and get both stability and clean motion.
See it on your own footage in the kott studio, with temporal Bayer dithering preloaded.
02/ OUT
Every setting described above is a real control. Open your own image and sweep it.
All journal entries