Shader concept

fBm (fractal Brownian motion): layered noise for shaders

fBm, short for fractal Brownian motion, adds several copies of a noise together, each with twice the detail and half the strength. The result looks like natural things: clouds, mountains, smoke and marble.

Running live in your browser. The code below is exactly what draws it.

The recipe

Each layer of noise is called an octave. The first octave gives big, slow shapes. The second has twice the frequency (lacunarity, usually 2) and half the amplitude (gain, usually 0.5), so it adds smaller detail. A third adds smaller still. After five or six octaves you have large shapes with finer and finer wrinkles on them, which is how real clouds and coastlines look at every scale.

In the loop above, p = p * 2.03 + offset raises the frequency a little more than 2 and shifts each layer, so the grid lines of the layers never line up and the pattern does not repeat visibly.

Where it is used

  • Clouds and fog: threshold the value with smoothstep, as in the picture.
  • Terrain: use it as a height map, then colour by height.
  • Smoke, fire and water: feed it into domain warping to get swirling motion.

Common mistakes

  • Too many octaves. Past the point where a layer is smaller than a pixel it only adds cost (and shimmer). Four to six is usually enough.
  • Forgetting the output range: with gain 0.5 the sum stays below 1, but it rarely reaches the extremes, so remap with smoothstep to use the full range of colours.

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);
}
float fbm2(float2 p)
{
    float v = 0.0;
    float amp = 0.5;
    for (int i = 0; i < 5; i++)
    {
        v += amp * noise2(p);
        p = p * 2.03 + float2(5.1, 1.7);
        amp *= 0.5;
    }
    return v;
}
float3 palette(float t)
{
    return 0.5 + 0.5 * cos(6.28318 * (t + float3(0.0, 0.33, 0.67)));
}

float4 frag(float2 uv)
{
    float2 p = (uv - 0.5) * float2(aspect, 1.0) * 3.0;
    float n = fbm2(p + float2(time * 0.15, 0.0));
    float3 col = lerp(float3(0.05, 0.1, 0.25), float3(0.95, 0.97, 1.0), smoothstep(0.3, 0.75, n));
    return float4(col, 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);
}
float fbm2(vec2 p)
{
    float v = 0.0;
    float amp = 0.5;
    for (int i = 0; i < 5; i++)
    {
        v += amp * noise2(p);
        p = p * 2.03 + vec2(5.1, 1.7);
        amp *= 0.5;
    }
    return v;
}
vec3 palette(float t)
{
    return 0.5 + 0.5 * cos(6.28318 * (t + vec3(0.0, 0.33, 0.67)));
}

vec4 frag(vec2 uv)
{
    vec2 p = (uv - 0.5) * vec2(aspect, 1.0) * 3.0;
    float n = fbm2(p + vec2(time * 0.15, 0.0));
    vec3 col = mix(vec3(0.05, 0.1, 0.25), vec3(0.95, 0.97, 1.0), smoothstep(0.3, 0.75, n));
    return vec4(col, 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 fbm2(pIn: vec2f) -> f32
{
    var p = pIn;
    var v = 0.0;
    var amp = 0.5;
    for (var i = 0; i < 5; i++)
    {
        v += amp * noise2(p);
        p = p * 2.03 + vec2f(5.1, 1.7);
        amp *= 0.5;
    }
    return v;
}
fn palette(t: f32) -> vec3f
{
    return 0.5 + 0.5 * cos(6.28318 * (t + vec3f(0.0, 0.33, 0.67)));
}

fn frag(uv: vec2f) -> vec4f
{
    let p = (uv - 0.5) * vec2f(aspect, 1.0) * 3.0;
    let n = fbm2(p + vec2f(time * 0.15, 0.0));
    let col = mix(vec3f(0.05, 0.1, 0.25), vec3f(0.95, 0.97, 1.0), smoothstep(0.3, 0.75, n));
    return vec4f(col, 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);
}
float fbm2(vec2 p)
{
    float v = 0.0;
    float amp = 0.5;
    for (int i = 0; i < 5; i++)
    {
        v += amp * noise2(p);
        p = p * 2.03 + vec2(5.1, 1.7);
        amp *= 0.5;
    }
    return v;
}
vec3 palette(float t)
{
    return 0.5 + 0.5 * cos(6.28318 * (t + vec3(0.0, 0.33, 0.67)));
}

vec4 frag(vec2 uv)
{
    vec2 p = (uv - 0.5) * vec2(aspect, 1.0) * 3.0;
    float n = fbm2(p + vec2(TIME * 0.15, 0.0));
    vec3 col = mix(vec3(0.05, 0.1, 0.25), vec3(0.95, 0.97, 1.0), smoothstep(0.3, 0.75, n));
    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
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);
}
float fbm2(float2 p)
{
    float v = 0.0;
    float amp = 0.5;
    for (int i = 0; i < 5; i++)
    {
        v += amp * noise2(p);
        p = p * 2.03 + float2(5.1, 1.7);
        amp *= 0.5;
    }
    return v;
}
float3 palette(float t)
{
    return 0.5 + 0.5 * cos(6.28318 * (t + float3(0.0, 0.33, 0.67)));
}

float4 frag(float2 uv)
{
    float2 p = (uv - 0.5) * float2(aspect, 1.0) * 3.0;
    float n = fbm2(p + float2(time * 0.15, 0.0));
    float3 col = mix(float3(0.05, 0.1, 0.25), float3(0.95, 0.97, 1.0), smoothstep(0.3, 0.75, n));
    return float4(col, 1.0);
}

Practise it in the lessons

Start with "Noise on noise: add layers by hand"

Questions

What do lacunarity and gain mean?

Lacunarity is how much the frequency grows each octave (about 2). Gain is how much the amplitude shrinks each octave (about 0.5). Lower gain gives smoother results; higher gain gives rougher ones.

Is fBm expensive?

Each octave costs one noise call, so five octaves is five times the cost of one. For full-screen effects keep it to four to six octaves.

What is the difference between fBm and Perlin noise?

Perlin noise is one layer. fBm is the sum of several layers of any noise (Perlin, value or simplex) at growing frequencies.

Related concepts