Shader Lab
09.FBM

FBM (fractal brownian motion)

Arc 2 · 2D Screen-Space · Lesson 9 of 35
coordinate signal color
4
0.50
2.00

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

"grittier / more natural detail" detail up "cleaner / smoother" detail down "softer octaves" lower the gain (each octave contributes less) "crunchier octaves" raise the gain (detail competes more with the base) "tighter detail spacing" raise lacunarity (frequencies climb faster)
Meta-phrase you gain here: "FBM of Perlin, N octaves." A complete, executable brief. Also "structural noise vs. surface noise" — the vocabulary for directing where detail goes.
Combines with: L10 (the coordinate you feed FBM can itself be noise-warped), L8 (FBM is just repeated L8), every procedural surface in Arc 3, L12 (a CRT's subtle phosphor hum is low-amplitude animated FBM).
the fragment shader running above
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);
}