Max-Filter Dilation
by GameDev.net · GLSL ES 3.00 (WebGL2) · 30 Aug 2026
Run the shader to adjust these controls.
What it demonstrates
Dilation is mathematical morphology implemented as a neighbourhood maximum. Bright regions expand into nearby dark pixels while their original values remain unchanged. The generated input combines thin lines, isolated points, rings, and coloured blocks, making growth and kernel shape easy to see without a texture asset.
Shader inputs
void mainImage(out vec4 fragColor, in vec2 fragCoord)
Called once per pixel. Write the colour to fragColor.
-
iResolutionvec3 - Viewport size in pixels (z is the pixel aspect ratio).
-
iTimefloat - Seconds since the shader started.
-
iTimeDeltafloat - Seconds since the previous frame.
-
iFrameRatefloat - Frames per second, smoothed.
-
iFrameint - Frames rendered since the start.
-
iMousevec4 - Mouse position: xy while held, zw of the last click.
-
iDatevec4 - Year, month, day, and seconds within the day.
-
iChannel0sampler2D - Texture bound to channel 0.
-
iChannel1sampler2D - Texture bound to channel 1.
-
iChannel2sampler2D - Texture bound to channel 2.
-
iChannel3sampler2D - Texture bound to channel 3.
-
iChannelResolutionvec3[4] - Pixel size of each bound channel texture.
-
iChannelTimefloat[4] - Playback time of each channel, in seconds.
-
iSampleRatefloat - Audio sample rate, always 44100.
Common
vec3 renderMaskScene(vec2 p, float time) {
vec3 color = vec3(0.015, 0.02, 0.035);
float ring = abs(length(p - vec2(-0.22, 0.08)) - 0.17);
float ringMask = 1.0 - smoothstep(0.006, 0.012, ring);
float cuts = step(0.20, fract(atan(p.y - 0.08, p.x + 0.22) * 1.273 + 0.5));
color = mix(color, vec3(1.0, 0.18, 0.12), ringMask * cuts);
vec2 boxPoint = abs(p - vec2(0.24, -0.08));
float boxEdge = abs(max(boxPoint.x - 0.14, boxPoint.y - 0.10));
color = mix(color, vec3(0.10, 0.85, 0.32), 1.0 - smoothstep(0.004, 0.010, boxEdge));
float line = 1.0 - smoothstep(0.004, 0.009, abs(p.y - 0.30 * p.x + 0.20));
color = mix(color, vec3(0.18, 0.42, 1.0), line * smoothstep(0.42, 0.34, abs(p.x)));
for (int i = 0; i < 8; i++) {
float fi = float(i);
vec2 point = vec2(-0.42 + fi * 0.12, -0.31 + 0.018 * sin(time + fi));
float dotMask = 1.0 - smoothstep(0.004, 0.008, length(p - point));
color = mix(color, vec3(1.0, 0.85, 0.16), dotMask);
}
return color;
}
vec2 maskPoint(vec2 fragCoord) {
return (fragCoord - 0.5 * iResolution.xy) / iResolution.y;
}
Buffer A1
Main Image4
uniform float uRadius; // @param 1.0..3.0 = 2.0 "Radius"
uniform float uStrength; // @param 0.0..1.0 = 1.0 "Strength"
void mainImage(out vec4 fragColor, in vec2 fragCoord) {
vec2 uv = fragCoord / iResolution.xy;
vec3 original = texture(iChannel0, uv).rgb;
vec3 maximum = vec3(0.0);
for (int y = -3; y <= 3; y++) {
for (int x = -3; x <= 3; x++) {
float distanceFromCenter = length(vec2(float(x), float(y)));
if (distanceFromCenter <= uRadius) {
vec2 offset = vec2(float(x), float(y)) / iResolution.xy;
maximum = max(maximum, texture(iChannel0, uv + offset).rgb);
}
}
}
vec3 color = mix(original, maximum, uStrength);
fragColor = vec4(color, 1.0);
}
Inputs for this pass
- iChannel0 Buffer A
- iChannel1 Empty
- iChannel2 Empty
- iChannel3 Empty
Learn from this shader
How it works
Buffer A renders the source mask scene. For each output pixel, the image pass visits a bounded seven-by-seven neighbourhood and keeps the component-wise maximum. Only taps inside Radius form the circular structuring element, so expansion is less boxy than a full square kernel. Increasing radius thickens strokes, closes narrow gaps, and grows isolated islands. Unlike blur, the operation never averages values: a bright sample is copied outward. Strength blends between the original and the morphological result for comparison, while a nearest-filtered input preserves exact mask boundaries. The fixed maximum loop keeps GPU work predictable at no more than forty-nine reads.
Try changing
Start with Radius one and watch the single-pixel points become small discs. Increase it until gaps in the broken ring close. Set Strength to zero to inspect the source, then to one for true dilation. Compare how the red, green, and blue shapes grow independently because the maximum is calculated per component.
Using it in a game
Replace Buffer A with a binary or soft mask. Dilation can thicken screen-space outlines, grow bloom masks, create selection halos, pad signed glyph coverage, or close tiny holes before later processing. For larger radii, use separable horizontal and vertical max passes or process a lower-resolution mask instead of expanding the loop. Clamp sampling at frame edges, as this package does, and perform the operation before compositing the mask's final colour over the scene.
Explore the techniques
Continue with curated explanations and progressively related examples.
Discussion