Voronoi / Worley cells
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
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);
}