13 KiB
13 KiB
- IMPORTANT: All declared
uniformvariables must be used in the shader code, otherwise the compiler will optimize them away. After optimization,gl.getUniformLocation()returnsnull, and setting that uniform triggers a WebGLINVALID_OPERATIONerror, which may cause rendering failure. Ensure uniforms likeiTimeare actually used inmain()(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:
floordivides into an integer grid; each cell contains a randomly offset feature point- Search the 3x3 (2D) or 3x3x3 (3D) neighborhood for all feature points
- Record the nearest distance F1 (optionally second-nearest F2)
- Map F1, F2, or F2-F1 to color/height/shape
Distance metrics:
- Euclidean:
dot(r,r)(squared, fast) -> finalsqrt - 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
// 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
// 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)
// 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)
// 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
// 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
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
// === 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
#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)
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)
#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)
#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)
#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 oflengthduring comparison; onlysqrtfor 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