Shader concept
Value noise: smooth randomness for shaders
Value noise is smooth randomness: random values are placed on a grid and the space between them is blended, so you get soft blobs instead of grain. It is the simplest kind of procedural noise.
Running live in your browser. The code below is exactly what draws it.
How it is built
Take the position and split it into a whole part (which grid cell you are in) and a fractional part (where you are inside the cell). Use a hash to get a random value for each of the four corners of that cell, then blend them: first along x, then along y. If you blend with the raw fraction, the seams between cells show as visible creases, so the fraction is first eased with the same S-curve smoothstep uses (f * f * (3 - 2 * f)).
What it looks like and what it is good for
Value noise is blobby with a slight grid feel. It is a fast way to get clouds, dirt, water movement and wobble. Stack several scales of it and you get fractal noise, which looks natural. If the grid pattern bothers you, gradient noise (Perlin or simplex) looks smoother, at a small extra cost.
Common mistakes
- Skipping the ease on the fraction: the picture then shows diamond-shaped creases along the grid lines.
- Using too high a frequency, which collapses back into grain. Scale the position up slowly (4 to 10 for a screen-sized pattern).
The code, in every shader language
The same example for each language family. Open one to see it.
HLSL Unity, Unreal
float hash21(float2 p)
{
float3 p3 = frac(float3(p.x, p.y, p.x) * 0.1031);
p3 += dot(p3, p3.yzx + 33.33);
return frac((p3.x + p3.y) * p3.z);
}
float noise2(float2 p)
{
float2 i = floor(p);
float2 f = frac(p);
f = f * f * (3.0 - 2.0 * f);
float a = hash21(i);
float b = hash21(i + float2(1.0, 0.0));
float c = hash21(i + float2(0.0, 1.0));
float d = hash21(i + float2(1.0, 1.0));
return lerp(lerp(a, b, f.x), lerp(c, d, f.x), f.y);
}
float4 frag(float2 uv)
{
float n = noise2(uv * float2(aspect, 1.0) * 6.0 + time * 0.3);
return float4(n, n, n, 1.0);
} GLSL Shadertoy, three.js, OpenGL
float hash21(vec2 p)
{
vec3 p3 = fract(vec3(p.x, p.y, p.x) * 0.1031);
p3 += dot(p3, p3.yzx + 33.33);
return fract((p3.x + p3.y) * p3.z);
}
float noise2(vec2 p)
{
vec2 i = floor(p);
vec2 f = fract(p);
f = f * f * (3.0 - 2.0 * f);
float a = hash21(i);
float b = hash21(i + vec2(1.0, 0.0));
float c = hash21(i + vec2(0.0, 1.0));
float d = hash21(i + vec2(1.0, 1.0));
return mix(mix(a, b, f.x), mix(c, d, f.x), f.y);
}
vec4 frag(vec2 uv)
{
float n = noise2(uv * vec2(aspect, 1.0) * 6.0 + time * 0.3);
return vec4(n, n, n, 1.0);
} WGSL WebGPU, Bevy
fn hash21(p: vec2f) -> f32
{
var p3 = fract(vec3f(p.x, p.y, p.x) * 0.1031);
p3 += dot(p3, p3.yzx + 33.33);
return fract((p3.x + p3.y) * p3.z);
}
fn noise2(p: vec2f) -> f32
{
let i = floor(p);
var f = fract(p);
f = f * f * (3.0 - 2.0 * f);
let a = hash21(i);
let b = hash21(i + vec2f(1.0, 0.0));
let c = hash21(i + vec2f(0.0, 1.0));
let d = hash21(i + vec2f(1.0, 1.0));
return mix(mix(a, b, f.x), mix(c, d, f.x), f.y);
}
fn frag(uv: vec2f) -> vec4f
{
let n = noise2(uv * vec2f(aspect, 1.0) * 6.0 + time * 0.3);
return vec4f(n, n, n, 1.0);
} Godot Godot 4 shading language
shader_type canvas_item;
float hash21(vec2 p)
{
vec3 p3 = fract(vec3(p.x, p.y, p.x) * 0.1031);
p3 += dot(p3, p3.yzx + 33.33);
return fract((p3.x + p3.y) * p3.z);
}
float noise2(vec2 p)
{
vec2 i = floor(p);
vec2 f = fract(p);
f = f * f * (3.0 - 2.0 * f);
float a = hash21(i);
float b = hash21(i + vec2(1.0, 0.0));
float c = hash21(i + vec2(0.0, 1.0));
float d = hash21(i + vec2(1.0, 1.0));
return mix(mix(a, b, f.x), mix(c, d, f.x), f.y);
}
vec4 frag(vec2 uv)
{
float n = noise2(uv * vec2(aspect, 1.0) * 6.0 + TIME * 0.3);
return vec4(n, n, n, 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
float hash21(float2 p)
{
float3 p3 = fract(float3(p.x, p.y, p.x) * 0.1031);
p3 += dot(p3, p3.yzx + 33.33);
return fract((p3.x + p3.y) * p3.z);
}
float noise2(float2 p)
{
float2 i = floor(p);
float2 f = fract(p);
f = f * f * (3.0 - 2.0 * f);
float a = hash21(i);
float b = hash21(i + float2(1.0, 0.0));
float c = hash21(i + float2(0.0, 1.0));
float d = hash21(i + float2(1.0, 1.0));
return mix(mix(a, b, f.x), mix(c, d, f.x), f.y);
}
float4 frag(float2 uv)
{
float n = noise2(uv * float2(aspect, 1.0) * 6.0 + time * 0.3);
return float4(n, n, n, 1.0);
} Practise it in the lessons
- Smooth randomness, step 1: blend two valuesPlot the curve n as a thin cyan line on the dark background (8 cells across, hash from the Toolbox).
- Smooth randomness, step 2: soften the cornersDraw the plain blend as a red curve and the softened blend as a green curve in the same picture.
- Value noise (smooth randomness)Write vnoise() and display it at scale 8x6 as grey.
- Noise on noise: add layers by handShow three layers of noise added together as grey.
Questions
What is the difference between value noise and Perlin noise?
Value noise blends random values stored at grid points. Perlin (gradient) noise blends random directions stored at the grid points, which hides the grid better but costs a little more.
How do I animate noise?
Add time to the position, or use a 3D noise function and move through the third axis. Moving the position slowly looks like drifting; moving through the third axis looks like boiling.