Voronoi Stained Glass / Cracked Ice Shader - Full GLSL

11 views 0 replies
Live Shader
Loading versions...

Been messing around with Voronoi diagrams for a while now and finally got something I'm really happy with. It looks like stained glass windows or cracked ice depending on how you tweak the colors. Figured I'd share since there aren't a ton of complete Voronoi examples floating around that actually look good out of the box.

precision mediump float;

uniform vec2 iResolution;
uniform float iTime;

// pseudo-random 2D hash
vec2 hash2(vec2 p) {
    p = vec2(dot(p, vec2(127.1, 311.7)),
             dot(p, vec2(269.5, 183.3)));
    return fract(sin(p) * 43758.5453);
}

void main() {
    vec2 uv = gl_FragCoord.xy / iResolution.xy;
    uv.x *= iResolution.x / iResolution.y;

    float scale = 6.0;
    vec2 st = uv * scale;
    vec2 i_st = floor(st);
    vec2 f_st = fract(st);

    float minDist = 1.0;
    float secondDist = 1.0;
    vec2 minPoint = vec2(0.0);
    vec2 minCell = vec2(0.0);

    // search 3x3 neighborhood
    for (int y = -1; y <= 1; y++) {
        for (int x = -1; x <= 1; x++) {
            vec2 neighbor = vec2(float(x), float(y));
            vec2 point = hash2(i_st + neighbor);

            // animate the cell points
            point = 0.5 + 0.45 * sin(iTime * 0.6 + 6.2831 * point);

            vec2 diff = neighbor + point - f_st;
            float d = length(diff);

            if (d < minDist) {
                secondDist = minDist;
                minDist = d;
                minPoint = point;
                minCell = i_st + neighbor;
            } else if (d < secondDist) {
                secondDist = d;
            }
        }
    }

    // edge detection: difference between closest and second closest
    float edge = secondDist - minDist;
    float edgeLine = 1.0 - smoothstep(0.0, 0.08, edge);

    // per-cell color from cell hash
    vec2 cellHash = hash2(minCell);
    float hue = cellHash.x;
    float sat = 0.5 + 0.3 * cellHash.y;

    // hue to RGB (simplified HSV with S and V baked in)
    vec3 cellColor = vec3(0.0);
    float h = hue * 6.0;
    float c = sat * 0.85;
    float xc = c * (1.0 - abs(mod(h, 2.0) - 1.0));
    if      (h < 1.0) cellColor = vec3(c,  xc, 0.0);
    else if (h < 2.0) cellColor = vec3(xc, c,  0.0);
    else if (h < 3.0) cellColor = vec3(0.0, c,  xc);
    else if (h < 4.0) cellColor = vec3(0.0, xc, c);
    else if (h < 5.0) cellColor = vec3(xc, 0.0, c);
    else              cellColor = vec3(c,  0.0, xc);
    cellColor += vec3(0.15); // lift the blacks a bit

    // subtle brightness variation inside each cell based on distance
    float innerShade = 0.85 + 0.15 * smoothstep(0.0, 0.5, minDist);
    cellColor *= innerShade;

    // glow on the edges - slightly warm white
    vec3 edgeColor = vec3(1.0, 0.95, 0.8);

    // also add a faint secondary glow that pulses
    float pulse = 0.5 + 0.5 * sin(iTime * 1.5);
    float edgeGlow = 1.0 - smoothstep(0.0, 0.18, edge);
    vec3 glowColor = mix(vec3(0.4, 0.6, 1.0), vec3(1.0, 0.5, 0.3), pulse);

    vec3 col = mix(cellColor, edgeColor, edgeLine);
    col += glowColor * edgeGlow * 0.3;

    // slight vignette for that stained glass backlit feel
    vec2 center = (gl_FragCoord.xy / iResolution.xy) - 0.5;
    float vig = 1.0 - 0.4 * dot(center, center);
    col *= vig;

    gl_FragColor = vec4(col, 1.0);
}

So the basic idea for anyone not familiar with Voronoi: you scatter random points across a grid, and for each pixel you find which point is closest. That gives you the "cells". The interesting part is the edges -- I'm computing both the nearest and second-nearest distances, then using the difference between them to detect where cell boundaries are. Where that difference is small, you're right on the border between two cells.

The `smoothstep(0.0, 0.08, edge)` controls how sharp the cracks look. Smaller second value = thinner, sharper lines. If you bump it up to like 0.15 or 0.2 you get softer, fatter edges which can look more like frosted glass.

For the cell coloring I'm doing a cheap HSV-to-RGB conversion using a hash of the cell coordinate. Each cell gets a unique hue this way. There's also a subtle distance-based shading inside each cell so they're not perfectly flat -- gives it a bit of that concave glass panel look.

The edge glow has two layers: a hard white line for the actual crack/lead, and a softer colored glow around it that slowly pulses between warm and cool. That pulsing makes it feel a bit magical, like the glass is radiating energy.

The animation comes from the cell points themselves moving around in slow sine loops. This means the whole pattern shifts and morphs organically. If you want it static (for cracked ice, broken ground, etc.), just remove the sin() animation and use the raw hash values as fixed points.

Places I've used this or plan to:

  • Magic shield effects - put it on a sphere with additive blending, looks incredible
  • Frozen surface - desaturate the colors, make them white/blue, thicken the edges
  • Procedural floor tiles - bake to texture, works great for alien/organic environments
  • Lava cracks - invert it so edges are bright orange/red and cells are dark
  • Shattered glass UI - for damage indicators or broken screen effects

One thing worth noting: the 3x3 neighborhood search is important. If you only check the current cell you'll get artifacts at cell boundaries where the nearest point is actually in an adjacent cell. Some tutorials skip this and it always looks wrong at the edges.

Let me know if anyone has questions or improvements. I've seen some implementations that use a second Voronoi pass at a different scale for more complex crack patterns -- might try that next.