- **IMPORTANT:** All declared `uniform` variables must be used in the shader code, otherwise the compiler will optimize them away. After optimization, `gl.getUniformLocation()` returns `null`, and setting that uniform triggers a WebGL `INVALID_OPERATION` error, which may cause rendering failure. Ensure uniforms like `iTime` are actually used in `main()` (e.g., `float t = iTime * 1.0;`) # Voronoi & Cellular Noise ## Use Cases - Natural textures: cells, cracked soil, stone, skin pores - Structured patterns: crystals, honeycombs, shattered glass, mosaics - Effects: fire/nebula (fBm stacking), crack generation - Procedural materials: cloud noise, terrain height maps, stylized partitioning ## Core Principles Voronoi noise = **spatial partitioning**: scatter feature points, assign each pixel to the "cell" of its nearest feature point. Algorithm flow: 1. `floor` divides into an integer grid; each cell contains a randomly offset feature point 2. Search the 3x3 (2D) or 3x3x3 (3D) neighborhood for all feature points 3. Record the nearest distance F1 (optionally second-nearest F2) 4. Map F1, F2, or F2-F1 to color/height/shape Distance metrics: - Euclidean: `dot(r,r)` (squared, fast) -> final `sqrt` - Manhattan: `abs(r.x)+abs(r.y)` - Chebyshev: `max(abs(r.x), abs(r.y))` Exact border distance (two-pass algorithm): `dot(0.5*(mr+r), normalize(r-mr))` Rounded borders (harmonic mean): `1/(1/(d2-d1) + 1/(d3-d1))` ## Implementation Steps ### Step 1: Hash Functions ```glsl // sin-dot hash (suitable for most cases) 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); } // 3D version vec3 hash3(vec3 p) { float n = sin(dot(p, vec3(7.0, 157.0, 113.0))); return fract(vec3(2097152.0, 262144.0, 32768.0) * n); } // High-quality integer hash (ES 3.0+, more uniform) vec3 hash3_uint(vec3 p) { uvec3 q = uvec3(ivec3(p)) * uvec3(1597334673U, 3812015801U, 2798796415U); q = (q.x ^ q.y ^ q.z) * uvec3(1597334673U, 3812015801U, 2798796415U); return vec3(q) / float(0xffffffffU); } ``` ### Step 2: Basic F1 Voronoi ```glsl // Returns (F1 distance, cell ID) vec2 voronoi(vec2 x) { vec2 n = floor(x); vec2 f = fract(x); vec3 m = vec3(8.0); for (int j = -1; j <= 1; j++) for (int i = -1; i <= 1; i++) { vec2 g = vec2(float(i), float(j)); vec2 o = hash2(n + g); vec2 r = g - f + o; float d = dot(r, r); if (d < m.x) { m = vec3(d, o); } } return vec2(sqrt(m.x), m.y + m.z); } ``` ### Step 3: F1 + F2 (Edge Detection) ```glsl // Returns vec2(F1, F2), edge value = F2 - F1 vec2 voronoi_f1f2(vec2 x) { vec2 p = floor(x); vec2 f = fract(x); vec2 res = vec2(8.0); for (int j = -1; j <= 1; j++) for (int i = -1; i <= 1; i++) { vec2 b = vec2(i, j); vec2 r = b - f + hash2(p + b); float d = dot(r, r); if (d < res.x) { res.y = res.x; res.x = d; } else if (d < res.y) { res.y = d; } } return sqrt(res); } ``` ### Step 4: Exact Border Distance (Two-Pass Algorithm) ```glsl // Returns vec3(border distance, nearest point offset) vec3 voronoi_border(vec2 x) { vec2 ip = floor(x); vec2 fp = fract(x); // First pass: find nearest feature point vec2 mg, mr; float md = 8.0; for (int j = -1; j <= 1; j++) for (int i = -1; i <= 1; i++) { vec2 g = vec2(float(i), float(j)); vec2 o = hash2(ip + g); vec2 r = g + o - fp; float d = dot(r, r); if (d < md) { md = d; mr = r; mg = g; } } // Second pass: exact border distance (5x5 range) md = 8.0; for (int j = -2; j <= 2; j++) for (int i = -2; i <= 2; i++) { vec2 g = mg + vec2(float(i), float(j)); vec2 o = hash2(ip + g); vec2 r = g + o - fp; if (dot(mr - r, mr - r) > 0.00001) md = min(md, dot(0.5 * (mr + r), normalize(r - mr))); } return vec3(md, mr); } ``` ### Step 5: Feature Point Animation ```glsl // Replace static hash inside the neighborhood search loop: vec2 o = hash2(n + g); o = 0.5 + 0.5 * sin(iTime + 6.2831 * o); // different phase per point vec2 r = g - f + o; ``` ### Step 6: Coloring & Visualization ```glsl void mainImage(out vec4 fragColor, in vec2 fragCoord) { // Must use iTime, otherwise the compiler will optimize away the uniform float time = iTime * 1.0; vec2 p = fragCoord.xy / iResolution.xy; vec2 uv = p * SCALE; vec2 c = voronoi(uv); float dist = c.x; float id = c.y; // Cell coloring (ID-driven palette) vec3 col = 0.5 + 0.5 * cos(id * 6.2831 + vec3(0.0, 1.0, 2.0)); // Distance falloff col *= clamp(1.0 - 0.4 * dist * dist, 0.0, 1.0); // Border lines col -= (1.0 - smoothstep(0.08, 0.09, dist)); fragColor = vec4(col, 1.0); } ``` ## Complete Code Template ```glsl // === Voronoi Cellular Noise — Complete ShaderToy Template === // Supports F1/F2/F2-F1 modes, multiple distance metrics, animation, exact borders #define SCALE 8.0 // Cell density #define ANIMATE 1 // 0=static, 1=animated #define MODE 0 // 0=F1 fill, 1=F2-F1 edges, 2=exact borders #define DIST_METRIC 0 // 0=Euclidean, 1=Manhattan, 2=Chebyshev 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); } float distFunc(vec2 r) { #if DIST_METRIC == 0 return dot(r, r); #elif DIST_METRIC == 1 return abs(r.x) + abs(r.y); #elif DIST_METRIC == 2 return max(abs(r.x), abs(r.y)); #endif } vec2 getPoint(vec2 cellId) { vec2 o = hash2(cellId); #if ANIMATE o = 0.5 + 0.5 * sin(iTime + 6.2831 * o); #endif return o; } vec4 voronoi(vec2 x) { vec2 n = floor(x); vec2 f = fract(x); float d1 = 8.0, d2 = 8.0; vec2 nearestCell = vec2(0.0); for (int j = -1; j <= 1; j++) for (int i = -1; i <= 1; i++) { vec2 g = vec2(float(i), float(j)); vec2 o = getPoint(n + g); vec2 r = g - f + o; float d = distFunc(r); if (d < d1) { d2 = d1; d1 = d; nearestCell = n + g; } else if (d < d2) { d2 = d; } } #if DIST_METRIC == 0 d1 = sqrt(d1); d2 = sqrt(d2); #endif return vec4(d1, d2, nearestCell); } vec3 voronoiBorder(vec2 x) { vec2 ip = floor(x); vec2 fp = fract(x); vec2 mg, mr; float md = 8.0; for (int j = -1; j <= 1; j++) for (int i = -1; i <= 1; i++) { vec2 g = vec2(float(i), float(j)); vec2 o = getPoint(ip + g); vec2 r = g + o - fp; float d = dot(r, r); if (d < md) { md = d; mr = r; mg = g; } } md = 8.0; for (int j = -2; j <= 2; j++) for (int i = -2; i <= 2; i++) { vec2 g = mg + vec2(float(i), float(j)); vec2 o = getPoint(ip + g); vec2 r = g + o - fp; if (dot(mr - r, mr - r) > 0.00001) md = min(md, dot(0.5 * (mr + r), normalize(r - mr))); } return vec3(md, mr); } void mainImage(out vec4 fragColor, in vec2 fragCoord) { // Must use iTime, otherwise the compiler will optimize away the uniform (especially important when ANIMATE=1) float time = iTime * 1.0; vec2 p = fragCoord.xy / iResolution.xy; p.x *= iResolution.x / iResolution.y; vec2 uv = p * SCALE; vec3 col = vec3(0.0); #if MODE == 0 vec4 v = voronoi(uv); float id = dot(v.zw, vec2(127.1, 311.7)); col = 0.5 + 0.5 * cos(id * 6.2831 + vec3(0.0, 1.0, 2.0)); col *= clamp(1.0 - 0.4 * v.x * v.x, 0.0, 1.0); col -= (1.0 - smoothstep(0.08, 0.09, v.x)); #elif MODE == 1 vec4 v = voronoi(uv); float edge = v.y - v.x; col = vec3(1.0 - smoothstep(0.0, 0.15, edge)); col *= vec3(0.2, 0.6, 1.0); #elif MODE == 2 vec3 c = voronoiBorder(uv); col = c.x * (0.5 + 0.5 * sin(64.0 * c.x)) * vec3(1.0); col = mix(vec3(1.0, 0.6, 0.0), col, smoothstep(0.04, 0.07, c.x)); float dd = length(c.yz); col = mix(vec3(1.0, 0.6, 0.1), col, smoothstep(0.0, 0.12, dd)); #endif fragColor = vec4(col, 1.0); } ``` ## Common Variants ### Variant 1: 3D Voronoi + fBm Fire ```glsl #define NUM_OCTAVES 5 vec3 hash3(vec3 p) { float n = sin(dot(p, vec3(7.0, 157.0, 113.0))); return fract(vec3(2097152.0, 262144.0, 32768.0) * n); } float voronoi3D(vec3 p) { vec3 g = floor(p); p = fract(p); float d = 1.0; for (int j = -1; j <= 1; j++) for (int i = -1; i <= 1; i++) for (int k = -1; k <= 1; k++) { vec3 b = vec3(i, j, k); vec3 r = b - p + hash3(g + b); d = min(d, dot(r, r)); } return d; } float fbmVoronoi(vec3 p) { vec3 t = vec3(0.0, 0.0, p.z + iTime * 1.5); float tot = 0.0, sum = 0.0, amp = 1.0; for (int i = 0; i < NUM_OCTAVES; i++) { tot += voronoi3D(p + t) * amp; p *= 2.0; t *= 1.5; sum += amp; amp *= 0.5; } return tot / sum; } // Blackbody radiation palette vec3 firePalette(float i) { float T = 1400.0 + 1300.0 * i; vec3 L = vec3(7.4, 5.6, 4.4); L = pow(L, vec3(5.0)) * (exp(1.43876719683e5 / (T * L)) - 1.0); return 1.0 - exp(-5e8 / L); } ``` ### Variant 2: Rounded Borders (3rd-Order Voronoi) ```glsl float voronoiRounded(vec2 p) { vec2 g = floor(p); p -= g; vec3 d = vec3(1.0); // F1, F2, F3 for (int y = -1; y <= 1; y++) for (int x = -1; x <= 1; x++) { vec2 o = vec2(x, y); o += hash2(g + o) - p; float r = dot(o, o); d.z = max(d.x, max(d.y, min(d.z, r))); d.y = max(d.x, min(d.y, r)); d.x = min(d.x, r); } d = sqrt(d); return min(2.0 / (1.0 / max(d.y - d.x, 0.001) + 1.0 / max(d.z - d.x, 0.001)), 1.0); } ``` ### Variant 3: Voronoise (Unified Noise-Voronoi Framework) ```glsl #define JITTER 1.0 // 0=regular grid, 1=fully random #define SMOOTH 0.0 // 0=sharp Voronoi, 1=smooth noise vec3 hash3(vec2 p) { vec3 q = vec3(dot(p, vec2(127.1, 311.7)), dot(p, vec2(269.5, 183.3)), dot(p, vec2(419.2, 371.9))); return fract(sin(q) * 43758.5453); } float voronoise(vec2 p, float u, float v) { float k = 1.0 + 63.0 * pow(1.0 - v, 6.0); vec2 i = floor(p); vec2 f = fract(p); vec2 a = vec2(0.0); for (int y = -2; y <= 2; y++) for (int x = -2; x <= 2; x++) { vec2 g = vec2(x, y); vec3 o = hash3(i + g) * vec3(u, u, 1.0); vec2 d = g - f + o.xy; float w = pow(1.0 - smoothstep(0.0, 1.414, length(d)), k); a += vec2(o.z * w, w); } return a.x / a.y; } ``` ### Variant 4: Crack Texture (Multi-Layer Recursive Voronoi) ```glsl #define CRACK_DEPTH 3.0 #define CRACK_WIDTH 0.0 #define CRACK_SLOPE 50.0 float ofs = 0.5; #define disp(p) (-ofs + (1.0 + 2.0 * ofs) * hash2(p)) // Main loop vec4 O = vec4(0.0); vec2 U = uv; for (float i = 0.0; i < CRACK_DEPTH; i++) { vec2 D = fbm22(U) * 0.67; vec3 H = voronoiBorder(U + D); float d = H.x; d = min(1.0, CRACK_SLOPE * pow(max(0.0, d - CRACK_WIDTH), 1.0)); O += vec4(1.0 - d) / exp2(i); U *= 1.5 * rot(0.37); } ``` ### Variant 5: Tileable 3D Worley (Cloud Noise) ```glsl #define TILE_FREQ 4.0 float worleyTileable(vec3 uv, float freq) { vec3 id = floor(uv); vec3 p = fract(uv); float minDist = 1e4; for (float x = -1.0; x <= 1.0; x++) for (float y = -1.0; y <= 1.0; y++) for (float z = -1.0; z <= 1.0; z++) { vec3 offset = vec3(x, y, z); vec3 h = hash3_uint(mod(id + offset, vec3(freq))) * 0.5 + 0.5; h += offset; vec3 d = p - h; minDist = min(minDist, dot(d, d)); } return 1.0 - minDist; } float worleyFbm(vec3 p, float freq) { return worleyTileable(p * freq, freq) * 0.625 + worleyTileable(p * freq * 2.0, freq * 2.0) * 0.25 + worleyTileable(p * freq * 4.0, freq * 4.0) * 0.125; } float remap(float x, float a, float b, float c, float d) { return (((x - a) / (b - a)) * (d - c)) + c; } // cloud = remap(perlinNoise, worleyFbm - 1.0, 1.0, 0.0, 1.0); ``` ## Performance & Composition **Performance:** - Use `dot(r,r)` instead of `length` during comparison; only `sqrt` for final output - 3D loops can be manually unrolled along the z-axis to reduce nesting - Search range: basic F1 uses 3x3; exact borders/Voronoise/extended jitter uses 5x5 - Hash choice: `sin(dot(...))` is fastest; integer hash is more uniform but requires ES 3.0+ - fBm layers: 3 is sufficient, 5 is the upper limit **Combinations:** - **+fBm distortion**: `uv + 0.5*fbm22(uv*2.0)` -> organic cell shapes - **+Bump Mapping**: finite-difference normal computation -> pseudo-3D bumps - **+Palette**: `0.5+0.5*cos(6.2831*(t+vec3(0,0.33,0.67)))` -> rich colors - **+Raymarching**: Voronoi distance as part of the SDF -> cellular surfaces - **+Multi-scale stacking**: Voronoi at different frequencies stacked -> primary structure + fine detail ## Further Reading For complete step-by-step tutorials, mathematical derivations, and advanced usage, see [reference](../reference/voronoi-cellular-noise.md)