On this put up, we’ll take a more in-depth take a look at the dithering-shader challenge: a minimal, real-time ordered dithering impact constructed utilizing GLSL and the Publish Processing library.
Relatively than simply making a one-off visible impact, the aim was to construct one thing clear, composable, and extendable: a drop-in shader move that brings pixel-based texture into fashionable WebGL pipelines.
What It Does
This shader applies ordered dithering as a postprocessing impact. It transforms clean gradients into stylized, binary (or quantized) pixel patterns, simulating the visible language of early bitmap shows, dot matrix printers, and 8-bit video games.
It helps:
- Dynamic decision through
pixelSize
- Non-obligatory grayscale mode
- Composability with bloom, blur, or different passes
- Straightforward integration through
postprocessing
‘sImpact
class

Fragment Shader
Our dithering shader implementation consists of two most important elements:
1. The Core Shader
The guts of the impact lies within the GLSL fragment shader that implements ordered dithering:
bool getValue(float brightness, vec2 pos) {
// Early return for excessive values
if (brightness > 16.0 / 17.0) return false;
if (brightness < 1.0 / 17.0) return true;
// Calculate place in 4x4 dither matrix
vec2 pixel = ground(mod(pos.xy / gridSize, 4.0));
int x = int(pixel.x);
int y = int(pixel.y);
// 4x4 Bayer matrix threshold map
// ... threshold comparisons primarily based on matrix place
}
The getValue
perform is the core of the dithering algorithm. It:
- Takes brightness and place: Makes use of the pixel’s luminance worth and display place
- Maps to dither matrix: Calculates which cell of the 4×4 Bayer matrix the pixel belongs to
- Applies threshold: Compares the brightness towards a predetermined threshold for that matrix place
- Returns binary resolution: Whether or not the pixel needs to be black or coloured
Key Shader Options
- gridSize: Controls the scale of the dithering sample
- pixelSizeRatio: Provides pixelation impact for enhanced retro really feel
- grayscaleOnly: Converts the picture to grayscale earlier than dithering
- invertColor: Inverts the ultimate colours for various aesthetic results
2. Pixelation Integration
float pixelSize = gridSize * pixelSizeRatio;
vec2 pixelatedUV = ground(fragCoord / pixelSize) * pixelSize / decision;
baseColor = texture2D(inputBuffer, pixelatedUV).rgb;
The shader combines dithering with non-obligatory pixelation, making a compound retro impact that’s excellent for game-like visuals.
Making a Customized Postprocessing Impact
The shader is wrapped utilizing the Impact
base class from the postprocessing
library. This abstracts away the boilerplate of managing framebuffers and passes, permitting the shader to be dropped right into a scene with minimal setup.
export class DitheringEffect extends Impact {
uniforms: Map>;
constructor({
time = 0,
decision = new THREE.Vector2(1, 1),
gridSize = 4.0,
luminanceMethod = 0,
invertColor = false,
pixelSizeRatio = 1,
grayscaleOnly = false
}: DitheringEffectOptions = {}) {
const uniforms = new Map>([
["time", new THREE.Uniform(time)],
["resolution", new THREE.Uniform(resolution)],
["gridSize", new THREE.Uniform(gridSize)],
["luminanceMethod", new THREE.Uniform(luminanceMethod)],
["invertColor", new THREE.Uniform(invertColor ? 1 : 0)],
["ditheringEnabled", new THREE.Uniform(1)],
["pixelSizeRatio", new THREE.Uniform(pixelSizeRatio)],
["grayscaleOnly", new THREE.Uniform(grayscaleOnly ? 1 : 0)]
]);
tremendous("DitheringEffect", ditheringShader, { uniforms });
this.uniforms = uniforms;
}
...
}
Non-obligatory: Integrating with React Three Fiber
As soon as outlined, the impact is registered and utilized utilizing @react-three/postprocessing
. Right here’s a minimal utilization instance with bloom and dithering:
You may as well tweak pixelSize
dynamically to scale the impact with decision, or toggle grayscale mode primarily based on UI controls or scene context.
Extending the Shader
This shader is deliberately saved easy, a basis quite than a full system. It’s straightforward to customise or lengthen. Listed here are some concepts you possibly can attempt:
- Add coloration quantization: convert
coloration.rgb
to listed palettes - Pack depth-based dither layers for pretend shadows
- Animate the sample for VHS-like shimmer
- Interactive pixelation: use mouse proximity to have an effect on
u_pixelSize
Why Not Use a Texture?
Some dithering shaders depend on threshold maps or pre-baked noise textures. This one doesn’t. The matrix sample is deterministic and screen-space primarily based, which suggests:
- No texture loading required
- Absolutely procedural
- Clear pixel alignment
It’s not meant for photorealism. It’s for styling and flattening. Suppose extra zine than render farm.
Ultimate Ideas
This challenge began as a aspect experiment to discover what it will seem like to carry tactile, stylized “non-photorealism” again into postprocessing workflows. However I discovered it had broader use circumstances, particularly in circumstances the place design route favors abstraction or managed distortion.
Should you’re constructing UIs, video games, or interactive 3D scenes the place “excellent” isn’t the aim, perhaps a little bit pixel grit is precisely what you want.