FBM (fractal brownian motion)
Purpose
Stack several copies of Perlin noise at doubling frequency and halving amplitude, add them up — and the result gains coarse shape and fine detail at the same time. This one trick produces virtually every "natural-looking" texture in CG.
Key insight
A single Perlin octave either has big rolling shapes or fine surface detail, never both — frequency is one knob. FBM sidesteps this by summing octaves: noise at frequency 1 contributes the biggest shapes, noise at frequency 2 adds smaller wrinkles, frequency 4 adds finer still, each at half the amplitude of the last.
The natural world has this same multi-scale structure (a mountain has ridges, and the ridges have bumps, and the bumps have grain), and the eye reads an FBM image as "real" because it matches that structure. Three numbers control the recipe: how many octaves, lacunarity (how much faster each octave is — usually 2), and gain (how much quieter — usually 0.5).
Break it
Set detail to 1, then to 8, alternating. At 1 the image is clean and soft. At 8 it's busy and dense but the overall layout is the same — the same big shapes, just with increasingly fine grit painted onto them. Teaches: adding octaves changes the feel, not the composition. If you want different big shapes, change the first octave's coordinate or seed. If you want more grit on the same shapes, add octaves. This separation between structural noise (low octaves) and surface noise (high octaves) is a direct tool for art direction.
Direct Claude
uniform int u_detail;
uniform float u_gain;
uniform float u_lacunarity;
vec2 hash2(vec2 p) {
p = vec2(dot(p, vec2(127.1, 311.7)),
dot(p, vec2(269.5, 183.3)));
return -1.0 + 2.0 * fract(sin(p) * 43758.5453123);
}
float perlin(vec2 p) {
vec2 i = floor(p);
vec2 f = fract(p);
vec2 u = f * f * f * (f * (f * 6.0 - 15.0) + 10.0);
float a = dot(hash2(i + vec2(0.0, 0.0)), f - vec2(0.0, 0.0));
float b = dot(hash2(i + vec2(1.0, 0.0)), f - vec2(1.0, 0.0));
float c = dot(hash2(i + vec2(0.0, 1.0)), f - vec2(0.0, 1.0));
float d = dot(hash2(i + vec2(1.0, 1.0)), f - vec2(1.0, 1.0));
return mix(mix(a, b, u.x), mix(c, d, u.x), u.y);
}
// FBM — sum perlin at increasing frequency, decreasing amplitude.
float fbm(vec2 p, int octaves) {
float total = 0.0;
float amp = 0.5;
float freq = 1.0;
for (int i = 0; i < 8; i++) {
if (i >= octaves) break;
total += amp * perlin(p * freq);
freq *= u_lacunarity; // lacunarity
amp *= u_gain; // gain
}
return total;
}
void main() {
vec2 uv = (gl_FragCoord.xy - 0.5 * u_resolution) / min(u_resolution.x, u_resolution.y);
// SIGNAL LAYER — FBM at a fixed base frequency so Max compares N=1..8 cleanly.
float n = fbm(uv * 3.0, u_detail);
float v = 0.5 + 0.5 * n;
gl_FragColor = vec4(vec3(v), 1.0);
}