Voronoi Patterns and Cellular Noise — Organic Worlds from Simple Math

181 views 0 replies
Live Shader
Loading versions...

Voronoi diagrams are one of the most versatile tools in a shader programmer's arsenal. These patterns partition space into regions based on proximity to a set of seed points. Every pixel belongs to the cell of whichever point it is closest to. The result is an organic, cell-like tessellation that appears everywhere in nature — from the skin of a giraffe to dried mud cracks, soap bubbles, turtle shells, and the structure of biological tissue.

Complete Animated Voronoi Shader

Below is a complete, standalone WebGL1 fragment shader. It generates an animated Voronoi pattern with glowing edges, per-cell color variation, and smooth organic movement.

precision mediump float;

uniform vec2 iResolution;
uniform float iTime;

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.5453123);
}

vec3 voronoi(vec2 uv, float time) {
    vec2 cellInt = floor(uv);
    vec2 cellFrac = fract(uv);
    float d1 = 1e10;
    float d2 = 1e10;
    vec2 closestCell = vec2(0.0);

    for (int y = -2; y <= 2; y++) {
        for (int x = -2; x <= 2; x++) {
            vec2 neighbor = vec2(float(x), float(y));
            vec2 point = hash2(cellInt + neighbor);
            point = 0.5 + 0.45 * sin(time * 0.6 + 6.2831 * point);
            vec2 diff = neighbor + point - cellFrac;
            float dist = length(diff);
            if (dist < d1) {
                d2 = d1;
                d1 = dist;
                closestCell = cellInt + neighbor;
            } else if (dist < d2) {
                d2 = dist;
            }
        }
    }

    float id = dot(closestCell, vec2(7.0, 113.0));
    return vec3(d1, d2, id);
}

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

    vec3 v = voronoi(uv, iTime);
    float d1 = v.x;
    float d2 = v.y;
    float id = v.z;

    vec3 cellColor = 0.5 + 0.5 * cos(
        6.2831 * (fract(id * 0.1) + vec3(0.0, 0.33, 0.67))
    );

    float innerShade = smoothstep(0.0, 0.6, d1);
    cellColor *= 0.55 + 0.45 * innerShade;

    float edgeDist = d2 - d1;
    float edgeLine = 1.0 - smoothstep(0.0, 0.08, edgeDist);
    float edgeGlow = exp(-12.0 * edgeDist);

    vec3 glowColor = 0.6 + 0.4 * cos(
        iTime * 0.4 + vec3(0.0, 1.0, 2.0) + uv.x * 0.3
    );

    vec3 color = cellColor;
    color += glowColor * edgeGlow * 0.5;
    color = mix(color, vec3(1.0, 0.97, 0.9), edgeLine * 0.9);

    vec2 center = gl_FragCoord.xy / iResolution.xy - 0.5;
    float vignette = 1.0 - 0.4 * dot(center, center);
    color *= vignette;

    gl_FragColor = vec4(color, 1.0);
}

How Voronoi Tessellation Works

The fundamental idea is simple. Scatter a set of seed points across space. For every pixel, find the closest seed point. All pixels that share the same closest point belong to the same cell. The boundaries between cells form the characteristic edges of the Voronoi diagram.

In a shader, we use a grid-based approach. We divide UV space into a regular grid, place one randomized seed point in each grid cell, and for each pixel only check neighboring grid cells.

vec2 cellInt = floor(uv);   // integer cell coordinates
vec2 cellFrac = fract(uv);  // position within the cell

for (int y = -1; y <= 1; y++) {
    for (int x = -1; x <= 1; x++) {
        vec2 neighbor = vec2(float(x), float(y));
        vec2 point = hash2(cellInt + neighbor);
        vec2 diff = neighbor + point - cellFrac;
        float dist = length(diff);
        if (dist < minDist) minDist = dist;
    }
}

Distance Metrics — Changing the Look

Euclidean distance produces natural, rounded cells. But you can swap in different distance metrics for dramatically different aesthetics.

// Euclidean — natural, rounded cells
float dist = length(diff);

// Manhattan — diamond-shaped, angular cells (great for crystal/ice)
float dist = abs(diff.x) + abs(diff.y);

// Chebyshev — square cells (circuit board, tile patterns)
float dist = max(abs(diff.x), abs(diff.y));

Edge Detection — The Power of d2 - d1

One of the most useful tricks in Voronoi shading is tracking both the nearest distance (d1) and the second-nearest distance (d2). The difference d2 - d1 tells you how close a pixel is to the boundary between two cells.

float edgeDist = d2 - d1;
float edge = 1.0 - smoothstep(0.0, 0.05, edgeDist);
float glow = exp(-10.0 * edgeDist);

This opens up countless creative possibilities: glowing cracks in lava, pulsing neural networks, Tron-style grids, force-field impacts, or biological membrane walls.

Practical Applications

Lava and magma: Use deep reds and oranges with bright yellow-white glowing edges. Ice cracks: Switch to Manhattan distance for angular crack patterns. Biological tissue: Use Euclidean distance with per-cell color variation in organic tones. Force fields: Render only the edges and pulse the glow intensity. Terrain generation: Use cell IDs to assign biomes with edge distance controlling blending.

Moonjump
Forum Search Shader Sandbox
Sign In Register