On this submit, we’ll take a more in-depth have a look at the dithering-shader venture: a minimal, real-time ordered dithering impact constructed utilizing GLSL and the Publish Processing library.
Moderately than simply making a one-off visible impact, the objective was to construct one thing clear, composable, and extendable: a drop-in shader cross 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 by way of
pixelSize
- Non-compulsory grayscale mode
- Composability with bloom, blur, or different passes
- Straightforward integration by way of
postprocessing
‘sImpact
class

Fragment Shader
Our dithering shader implementation consists of two important parts:
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
operate is the core of the dithering algorithm. It:
- Takes brightness and place: Makes use of the pixel’s luminance worth and display screen place
- Maps to dither matrix: Calculates which cell of the 4×4 Bayer matrix the pixel belongs to
- Applies threshold: Compares the brightness in opposition to a predetermined threshold for that matrix place
- Returns binary determination: Whether or not the pixel must be black or coloured
Key Shader Options
- gridSize: Controls the dimensions 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 elective 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<string, THREE.Uniform<quantity | THREE.Vector2>>;
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<string, THREE.Uniform<quantity | THREE.Vector2>>([
["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-compulsory: 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:
<Canvas>
{/* ... your scene ... */}
<EffectComposer>
<Bloom depth={0.5} />
<Dithering pixelSize={2} grayscale />
</EffectComposer>
</Canvas>
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 stored easy, a basis moderately than a full system. It’s straightforward to customise or prolong. Listed here are some concepts you’ll be able to attempt:
- Add shade quantization: convert
shade.rgb
to listed palettes - Pack depth-based dither layers for faux 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 implies:
- No texture loading required
- Totally procedural
- Clear pixel alignment
It’s not meant for photorealism. It’s for styling and flattening. Suppose extra zine than render farm.
Closing Ideas
This venture began as a facet experiment to discover what it could appear like to convey 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.
If you happen to’re constructing UIs, video games, or interactive 3D scenes the place “excellent” isn’t the objective, possibly somewhat pixel grit is strictly what you want.