Shader concept
Voronoi (Worley) noise in a shader
Voronoi noise scatters random points over the plane and, for each pixel, measures the distance to the nearest point. It makes cells, cracked-earth patterns, scales and bubbles.
Running live in your browser. The code below is exactly what draws it.
How it works
Split the plane into square cells and put one random point in each cell. For a pixel, the nearest point must be in the pixel's own cell or one of its eight neighbours, so you check those nine cells (the two small loops) and keep the smallest distance. That is the whole algorithm, and it costs nine hash calls per pixel however many points there are in total.
What you can read out of it
- The distance to the nearest point: bubbles, glowing cells, soft blobs.
- Which point is nearest (return its cell id too): flat coloured cells, like stained glass or a giraffe's coat.
- The difference between the nearest and second-nearest distance: thin lines between cells, for cracks, scales and cobblestones.
Common mistakes
- Searching only the pixel's own cell. Points in the neighbouring cells can be closer, and you get hard seams along the grid lines.
- Using a poor hash for the point positions, which makes the points line up in visible patterns. Use a proper 2D hash like
hash22above.
The code, in every shader language
The same example for each language family. Open one to see it.
HLSL Unity, Unreal
float2 hash22(float2 p)
{
float3 p3 = frac(float3(p.x, p.y, p.x) * float3(0.1031, 0.1030, 0.0973));
p3 += dot(p3, p3.yzx + 33.33);
return frac((p3.xx + p3.yz) * p3.zy);
}
float voronoi(float2 p)
{
float2 i = floor(p);
float2 f = frac(p);
float d = 8.0;
for (int y = -1; y <= 1; y++)
{
for (int x = -1; x <= 1; x++)
{
float2 g = float2(float(x), float(y));
float2 o = hash22(i + g); // one random point per cell
o = 0.5 + 0.5 * sin(time * 0.8 + 6.2831 * o); // let it wander
d = min(d, length(g + o - f)); // keep the nearest
}
}
return d;
}
float4 frag(float2 uv)
{
float d = voronoi(uv * float2(aspect, 1.0) * 6.0);
float edge = smoothstep(0.0, 0.5, d);
float3 col = lerp(float3(0.95, 0.75, 0.25), float3(0.1, 0.12, 0.3), edge);
return float4(col, 1.0);
} GLSL Shadertoy, three.js, OpenGL
vec2 hash22(vec2 p)
{
vec3 p3 = fract(vec3(p.x, p.y, p.x) * vec3(0.1031, 0.1030, 0.0973));
p3 += dot(p3, p3.yzx + 33.33);
return fract((p3.xx + p3.yz) * p3.zy);
}
float voronoi(vec2 p)
{
vec2 i = floor(p);
vec2 f = fract(p);
float d = 8.0;
for (int y = -1; y <= 1; y++)
{
for (int x = -1; x <= 1; x++)
{
vec2 g = vec2(float(x), float(y));
vec2 o = hash22(i + g); // one random point per cell
o = 0.5 + 0.5 * sin(time * 0.8 + 6.2831 * o); // let it wander
d = min(d, length(g + o - f)); // keep the nearest
}
}
return d;
}
vec4 frag(vec2 uv)
{
float d = voronoi(uv * vec2(aspect, 1.0) * 6.0);
float edge = smoothstep(0.0, 0.5, d);
vec3 col = mix(vec3(0.95, 0.75, 0.25), vec3(0.1, 0.12, 0.3), edge);
return vec4(col, 1.0);
} WGSL WebGPU, Bevy
fn hash22(p: vec2f) -> vec2f
{
var p3 = fract(vec3f(p.x, p.y, p.x) * vec3f(0.1031, 0.1030, 0.0973));
p3 += dot(p3, p3.yzx + 33.33);
return fract((p3.xx + p3.yz) * p3.zy);
}
fn voronoi(p: vec2f) -> f32
{
let i = floor(p);
let f = fract(p);
var d = 8.0;
for (var y = -1; y <= 1; y++)
{
for (var x = -1; x <= 1; x++)
{
let g = vec2f(f32(x), f32(y));
var o = hash22(i + g); // one random point per cell
o = 0.5 + 0.5 * sin(time * 0.8 + 6.2831 * o); // let it wander
d = min(d, length(g + o - f)); // keep the nearest
}
}
return d;
}
fn frag(uv: vec2f) -> vec4f
{
let d = voronoi(uv * vec2f(aspect, 1.0) * 6.0);
let edge = smoothstep(0.0, 0.5, d);
let col = mix(vec3f(0.95, 0.75, 0.25), vec3f(0.1, 0.12, 0.3), edge);
return vec4f(col, 1.0);
} Godot Godot 4 shading language
shader_type canvas_item;
vec2 hash22(vec2 p)
{
vec3 p3 = fract(vec3(p.x, p.y, p.x) * vec3(0.1031, 0.1030, 0.0973));
p3 += dot(p3, p3.yzx + 33.33);
return fract((p3.xx + p3.yz) * p3.zy);
}
float voronoi(vec2 p)
{
vec2 i = floor(p);
vec2 f = fract(p);
float d = 8.0;
for (int y = -1; y <= 1; y++)
{
for (int x = -1; x <= 1; x++)
{
vec2 g = vec2(float(x), float(y));
vec2 o = hash22(i + g); // one random point per cell
o = 0.5 + 0.5 * sin(TIME * 0.8 + 6.2831 * o); // let it wander
d = min(d, length(g + o - f)); // keep the nearest
}
}
return d;
}
vec4 frag(vec2 uv)
{
float d = voronoi(uv * vec2(aspect, 1.0) * 6.0);
float edge = smoothstep(0.0, 0.5, d);
vec3 col = mix(vec3(0.95, 0.75, 0.25), vec3(0.1, 0.12, 0.3), edge);
return vec4(col, 1.0);
}
void fragment()
{
// Godot's UV starts at the top-left, the lessons use bottom-left
COLOR = frag(vec2(UV.x, 1.0 - UV.y));
} Metal iOS, macOS
float2 hash22(float2 p)
{
float3 p3 = fract(float3(p.x, p.y, p.x) * float3(0.1031, 0.1030, 0.0973));
p3 += dot(p3, p3.yzx + 33.33);
return fract((p3.xx + p3.yz) * p3.zy);
}
float voronoi(float2 p)
{
float2 i = floor(p);
float2 f = fract(p);
float d = 8.0;
for (int y = -1; y <= 1; y++)
{
for (int x = -1; x <= 1; x++)
{
float2 g = float2(float(x), float(y));
float2 o = hash22(i + g); // one random point per cell
o = 0.5 + 0.5 * sin(time * 0.8 + 6.2831 * o); // let it wander
d = min(d, length(g + o - f)); // keep the nearest
}
}
return d;
}
float4 frag(float2 uv)
{
float d = voronoi(uv * float2(aspect, 1.0) * 6.0);
float edge = smoothstep(0.0, 0.5, d);
float3 col = mix(float3(0.95, 0.75, 0.25), float3(0.1, 0.12, 0.3), edge);
return float4(col, 1.0);
} Practise it in the lessons
- Randomness without random()Make TV-static blocks: a 20×15 grid where each cell is a grey level from hash(cell).
- Repeating: floor & fracDraw an 8×6 checkerboard of black and white.
- Value noise (smooth randomness)Write vnoise() and display it at scale 8x6 as grey.
Questions
Is Voronoi noise the same as Worley noise?
They are the same family. Steven Worley described using the distance to the nearest feature points as a procedural texture (Worley noise); a Voronoi diagram is the set of cells you get from the same points.
Why check nine cells?
Each cell holds one point, so the nearest point to any pixel is always in the pixel's own cell or one of the eight around it.