Shader Lab
15.Voronoi / Worley cells

Voronoi / Worley cells

Arc 3 · Procedural Patterns · Lesson 15 of 35
coordinate signal color
10
1.00

Purpose

Scatter points in space, measure the distance from every pixel to the nearest of them, and the resulting field is cellular. Read the same field two different ways and you get both stone mosaics and their crack patterns.

Key insight

Every earlier lesson measured distance to one thing: the origin, a center, a curve. Voronoi measures distance to the nearest of many things. Scatter feature points across a grid — one per cell, offset by a hash so they don't sit on a lattice — then at every pixel, find the closest feature point. The distance to it is called F1. That field alone gives a mosaic of soft domes, one per point.

Now at the same pixel also find the second-closest point, call its distance F2, and compute F2 − F1. That difference is near-zero exactly at the boundary between two cells (both points equidistant) and grows inside each cell. So F2 − F1 is a crack map — the lines where cells meet. Two readings of one field, two completely different materials: stone tiles vs. mud cracks vs. cell walls.

Break it

1. Pull jitter to 0. A perfectly regular square grid appears. Teaches: Voronoi is not intrinsically organic. Its organic look comes entirely from jittering the lattice. The underlying structure is a grid with one point per cell; the randomness lives only in where inside the cell that point lands.

2. Click the mode button to toggle between F1 cell-fill and F2 − F1 cracks with all other parameters identical. Same feature points, same density, same jitter — but the material goes from "cobblestones" to "dry lakebed cracks." Teaches: the reading of a distance field is a first-class choice separate from the field itself.

Direct Claude

"more organic cell shapes" jitter up "mechanical / grid-like cells" jitter down "bigger stones / fewer cells" cell-density down "finer cracks / smaller cells" cell-density up "fill the cells / stone mosaic" read F1, palette-map "emphasize the cracks / dry-mud" read F2 − F1, invert, palette-map "softer cell edges" smoothstep on F2 − F1, wider band "harder cell edges" step (or narrow smoothstep) on F2 − F1
Meta-phrase you gain here: "F1 for the cells, F2 minus F1 for the cracks" — when briefing anything involving Worley, specifying which read is required rules out 90% of ambiguity.
Combines with: Lesson 9 (FBM overlaid on F1 for rock surface roughness), Lessons 13 / 14 (swap sine-of-coordinate for Worley F1 to get cell-based materials instead of band-based), Lesson 22 (cellular signal as input to a lighting normal for stone bumps).
the fragment shader running above
uniform float u_cellDensity;  // coordinate: grid frequency
uniform float u_jitter;       // coordinate: how far points wander inside cell
uniform int   u_mode;         // color:      0 = F1 cell-fill, 1 = F2-F1 cracks

// 2D hash — returns a point in [0,1] per integer cell
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);
}

// Scan a 3x3 block of neighbor cells, track the two smallest distances.
// F1 = nearest feature point;  F2 = second-nearest.
void worley(vec2 p, out float F1, out float F2, out vec2 nearCell) {
  vec2 i = floor(p);
  vec2 f = fract(p);
  F1 = 10.0;
  F2 = 10.0;
  nearCell = i;
  for (int y = -1; y <= 1; y++) {
    for (int x = -1; x <= 1; x++) {
      vec2 n = vec2(float(x), float(y));
      vec2 pt = 0.5 + u_jitter * (hash2(i + n) - 0.5);  // point inside that cell
      float d = length(n + pt - f);
      if (d < F1) { F2 = F1; F1 = d; nearCell = i + n; }
      else if (d < F2) { F2 = d; }
    }
  }
}

void main() {
  vec2 uv = gl_FragCoord.xy / u_resolution;
  float aspect = u_resolution.x / u_resolution.y;
  vec2 p = vec2(uv.x * aspect, uv.y) * u_cellDensity;

  float F1, F2;  vec2 nearCell;
  worley(p, F1, F2, nearCell);

  vec3 col;
  if (u_mode == 0) {
    // F1 CELL-FILL MODE — each cell tinted by its own hash; cracks where F2-F1 is small
    vec3 tint = 0.5 + 0.5 * cos(
      hash2(nearCell).x * 6.28 + vec3(0.0, 2.1, 4.2));
    vec3 stone = mix(vec3(0.35, 0.32, 0.36), vec3(0.82, 0.76, 0.68), tint.x);
    float crack = smoothstep(0.02, 0.08, F2 - F1);  // 1 inside cell, 0 on boundary
    col = mix(vec3(0.08, 0.07, 0.09), stone, crack);
  } else {
    // F2-F1 CRACKS MODE — pure boundary map, dry-mud feel
    float cracks = smoothstep(0.0, 0.18, F2 - F1);   // 0 on boundary, 1 inside
    vec3 mud  = vec3(0.62, 0.45, 0.30);
    vec3 deep = vec3(0.12, 0.08, 0.06);
    col = mix(deep, mud, cracks);
  }

  gl_FragColor = vec4(col, 1.0);
}