Fractal Brownian Motion (fBm)
<< Home

Fractal Brownian Motion (fBm)

Warning: There are a large amount of stuffs here using pretty modern JS and WebGL2. You might want to use a newer computer and a nice browser (say Chrome?) to view this. If you already do, never mind!

Controlling randomness is an important part in computer graphics. So let’s take a look at fractal brownian motion today, which is great at generating hills (more than perlin noise), clouds, valleys and such!

Introduction

Fractal Brownian motion, or fractional Brownian motion, is a kind of Brownian motion. And I don’t know a lot about it right now, because it kind of falls into the fractals area; but what I know is you can use it to create cool graphics. Behold!

float rand2d(vec2 uv) {
    return fract(sin(dot(uv, vec2(12.9898, 78.233))) * 43758.234);
}

float perlin(vec2 uv) {
    vec2 u = floor(uv);
    vec2 f = fract(uv);
    vec2 s = smoothstep(0.0, 1.0, f);
    
    float a = rand2d(u);
    float b = rand2d(u + vec2(1.0, 0.0));
    float c = rand2d(u + vec2(0.0, 1.0));
    float d = rand2d(u + vec2(1.0, 1.0));
    
    return mix(
        mix(a, b, s.x),
        mix(c, d, s.x),
        s.y);
}

float fbm(vec2 uv) {
    const int octaves = 6;
    float amplitude = 0.5;
    float val = 0.0;
    vec2 shift = vec2(100.0);
    
    mat2 rot = mat2(vec2(sin(0.5), cos(0.5)),
        vec2(-cos(0.5), sin(0.5)));

    for (int i = 0; i < octaves; i++) {
        val += amplitude * (perlin(uv) * 2.0 - 1.0);
        uv = rot * uv * 2.0 + shift;
        amplitude *= 0.5;
    }
    return val;
}

void main() {
    vec2 xy = uv * 2.0 - 1.0;
    vec2 q = vec2(
        fbm(uv), fbm(uv + vec2(1.0, 0.0))
    );
    vec2 r = vec2(
        fbm(uv + 1.0 * q + vec2(1.7, 9.2) + 0.15 * time),
        fbm(uv + 1.0 * q + vec2(8.3, 2.8) + 0.126 * time)
    );
    float f = fbm(uv + r);
    
    vec3 c = vec3(0.0);
    c = mix(vec3(1.0, 0.5, 0.6),
        vec3(0.8, 0.7, 0.9), clamp(f * f * 4.0, 0.0, 1.0));
        
    c = mix(c,
        vec3(0.0, 0.3, 0.16), clamp(length(q), 0.0, 1.0));
        
    c = mix(c,
        vec3(0.667, 1.0, 1.0), clamp(length(r), 0.0, 1.0));
    color = vec4(c, 1.0);
}
Recompile | Click on the canvas to toggle running. It is paused by default.

That’s pretty, isn’t it? I somehow ended up making this stuff. So let’s get started!

Implementation

1D

First, let’s take a look at the sine wave:

float graph(vec2 uv);

void main() {
    vec2 xy = uv * 2.0 - 1.0;
    xy *= 2.0;
    xy.x += time * 5.0;
    float dist = pow(
        max(1.0 - abs(xy.y - graph(xy) * 0.8), 0.0),
        9.0);
    color = vec4(dist, dist, dist, 1.0);
}
float graph(vec2 uv) {
    return sin(uv.x);
}
Recompile | Click on the canvas to toggle running. It is paused by default.

Such wave functions could be distorted by adding more sine waves:

float graph(vec2 uv);

void main() {
    vec2 xy = uv * 2.0 - 1.0;
    xy *= 2.0;
    xy.x += time * 5.0;
    float dist = pow(
        max(1.0 - abs(xy.y - graph(xy) * 0.8), 0.0),
        9.0);
    color = vec4(dist, dist, dist, 1.0);
}
float graph(vec2 uv) {
    return sin(uv.x)
        + sin(uv.x * 0.5)
        + sin(uv.x * 0.25)
        + sin(uv.x * 0.125)
        - sin(uv.x - 0.3)
        + sin(uv.x * 2.0) * 0.5;
}

Recompile | Click on the canvas to toggle running. It is paused by default.

It’s kind of similar, but at the meantime different. And with this knowledge in mind, we can replace the sin function with a more general noise function, probably perlin noise. And then, we can implement the fBm function!

float noise(vec2 uv) {
    return fract(sin(uv.x) * 42378.12345) * 2.0 - 1.0;
}

float perlin(vec2 uv) {
    vec2 u = floor(uv);
    vec2 f = smoothstep(0.0, 1.0, fract(uv));
    float a = noise(u);
    float b = noise(u + vec2(1.0, 0.0));
    return mix(a, b, f.x);
}

float fbm(vec2 uv);

void main() {
    vec2 xy = uv * 2.0 - 1.0;
    xy.x += time * 1.0;
    float dist = pow(
        max(1.0 - abs(xy.y - fbm(xy)), 0.0),
        9.0);
    float perl = pow(
        max(1.0 - abs(xy.y - perlin(xy)), 0.0),
        9.0);
    color = vec4(dist, perl, dist, 1.0);
}
// green line == perlin noise
// purple line == fbm

float fbm(vec2 uv) {
    const int octaves = 4;
    float amplitude = 0.5;
    float ret = 0.0; // Return value

    for (int i = 0; i < octaves; i++) {
        ret += amplitude * perlin(uv);
        uv *= 2.0;
        amplitude *= 0.5;
    }
    return ret;
}
Recompile | Click on the canvas to toggle running. It is paused by default.

Let’s take a look at this fbm function above. The noise value was multiplied by amplitude in every iteration; after that, the amplitude was cut by half. So as the iterations goes, the noise’s affect to the general graph plummets. This way, we can have detailed graph with little bumps here and there! More octaves = more details. And as it is based on perlin noise, it is better at generating hills that are steep, bumpy terrains & all sorts of unsmooth stuffs. The result graph itself also never wanders too far off the perlin noise graph. With fBm, we can generate detailed hills.

2D

Now let’s take it up a notch and start exploring in 2D! We don’t need to use directional perlin noise, though. The original one is enough; fBm will distort it so bad it will hardly look like perlin noise.

float rand2d(vec2 uv) {
    return fract(sin(dot(uv, vec2(42.178, 72.5353))) * 42537.52134) * 2.0 - 1.0;
}

float perlin(vec2 uv) {
    vec2 u = floor(uv);
    vec2 f = smoothstep(0.0, 1.0, fract(uv));
    float a = rand2d(u);
    float b = rand2d(u + vec2(1.0, 0.0));
    float c = rand2d(u + vec2(0.0, 1.0));
    float d = rand2d(u + vec2(1.0, 1.0));
    return mix(
        mix(a, b, f.x),
        mix(c, d, f.x),
        f.y
    );
}

float fbm(vec2 uv);

void main() {
    float v = fbm(uv * 5.0);
    color = vec4(v, v, v, 1.0);
}
float fbm(vec2 uv) {
    const int octaves = 4;
    float amplitude = 0.5;
    float ret = 0.0; // Return value

    for (int i = 0; i < octaves; i++) {
        ret += amplitude * perlin(uv);
        uv *= 2.0;
        amplitude *= 0.5;
    }
    return ret;
}
Recompile | Click on the canvas to toggle running. It is paused by default.

We don’t even need to change a single character of our original implementation! Just by changing the perlin noise to 2D perlin noise, it takes care of everything automatically. Isn’t this great? In order to distort the image further, we can apply a rotation and/or a great shift in position every iteration:

float rand2d(vec2 uv) {
    return fract(sin(dot(uv, vec2(42.178, 72.5353))) * 42537.52134) * 2.0 - 1.0;
}

float perlin(vec2 uv) {
    vec2 u = floor(uv);
    vec2 f = smoothstep(0.0, 1.0, fract(uv));
    float a = rand2d(u);
    float b = rand2d(u + vec2(1.0, 0.0));
    float c = rand2d(u + vec2(0.0, 1.0));
    float d = rand2d(u + vec2(1.0, 1.0));
    return mix(
        mix(a, b, f.x),
        mix(c, d, f.x),
        f.y
    );
}

float fbm(vec2 uv);

void main() {
    float v = fbm(uv * 5.0);
    color = vec4(v, v, v, 1.0);
}
float fbm(vec2 uv) {
    const int octaves = 6;
    float amplitude = 0.5;
    float ret = 0.0; // Return value
    mat2 rot30 = mat2(vec2(sin(0.5), cos(0.5)),
        vec2(-cos(0.5), sin(0.5)));
    vec2 shift = vec2(100.0, 0.0);

    for (int i = 0; i < octaves; i++) {
        ret += amplitude * perlin(uv);
        uv = rot30 * uv * 2.0 + shift;
        amplitude *= 0.5;
    }
    return ret;
}
Recompile | Click on the canvas to toggle running. It is paused by default.

Now it looks more cloudy in an irregular way. Also notice how the image becomes sharper as the octave increases. Done!

Usage

fBm has multiple usages in demoscene/cg area. The first usage, obviously, is cloud:

Clouds by IQ

Other usages includes heightmap:

Heightmap from thebookofshaders

Turbulence, which is kind of like carving valley. This is achieved by simply abs()ing the perlin value, so when it falls below zero, the value changes violently:

float rand2d(vec2 uv) {
    return fract(sin(dot(uv, vec2(42.178, 72.5353))) * 42537.52134) * 2.0 - 1.0;
}

float perlin(vec2 uv) {
    vec2 u = floor(uv);
    vec2 f = smoothstep(0.0, 1.0, fract(uv));
    float a = rand2d(u);
    float b = rand2d(u + vec2(1.0, 0.0));
    float c = rand2d(u + vec2(0.0, 1.0));
    float d = rand2d(u + vec2(1.0, 1.0));
    return mix(
        mix(a, b, f.x),
        mix(c, d, f.x),
        f.y
    );
}

float fbm(vec2 uv);

void main() {
    float v = fbm(uv * 5.0);
    color = vec4(v, v, v, 1.0);
}
float fbm(vec2 uv) {
    const int octaves = 6;
    float amplitude = 0.5;
    float ret = 0.0; // Return value
    mat2 rot30 = mat2(vec2(sin(0.5), cos(0.5)),
        vec2(-cos(0.5), sin(0.5)));
    vec2 shift = vec2(100.0, 0.0);

    for (int i = 0; i < octaves; i++) {
        ret += amplitude * abs(perlin(uv));
        uv = rot30 * uv * 2.0 + shift;
        amplitude *= 0.5;
    }
    return ret;
}
Recompile | Click on the canvas to toggle running. It is paused by default.

And ridge, which is a variant of turbulence, by reversing the return value and powing it up, making it even more sharper. It looks kinda like thunder:

float rand2d(vec2 uv) {
    return fract(sin(dot(uv, vec2(42.178, 72.5353))) * 42537.52134) * 2.0 - 1.0;
}

float perlin(vec2 uv) {
    vec2 u = floor(uv);
    vec2 f = smoothstep(0.0, 1.0, fract(uv));
    float a = rand2d(u);
    float b = rand2d(u + vec2(1.0, 0.0));
    float c = rand2d(u + vec2(0.0, 1.0));
    float d = rand2d(u + vec2(1.0, 1.0));
    return mix(
        mix(a, b, f.x),
        mix(c, d, f.x),
        f.y
    );
}

float fbm(vec2 uv);

void main() {
    float v = fbm(uv * 5.0);
    color = vec4(v, v, v, 1.0);
}
float fbm(vec2 uv) {
    const int octaves = 6;
    float amplitude = 0.5;
    float ret = 0.0; // Return value
    mat2 rot30 = mat2(vec2(sin(0.5), cos(0.5)),
        vec2(-cos(0.5), sin(0.5)));
    vec2 shift = vec2(100.0, 0.0);

    for (int i = 0; i < octaves; i++) {
        ret += amplitude * perlin(uv);
        uv = rot30 * uv * 2.0 + shift;
        amplitude *= 0.5;
    }
    ret = abs(ret);
    ret = 1.0 - ret; // reversing
    ret = ret * ret; // sharpening
    return ret;
}
Recompile | Click on the canvas to toggle running. It is paused by default.

Domain Warping

Domain Warping means plugging the result of fbm back into fbm. In this way, one can create really good looking visual effects, such as a even better version of cloud:

float rand2d(vec2 uv) {
    return fract(sin(dot(uv, vec2(42.178, 72.5353))) * 42537.52134) * 2.0 - 1.0;
}

float perlin(vec2 uv) {
    vec2 u = floor(uv);
    vec2 f = smoothstep(0.0, 1.0, fract(uv));
    float a = rand2d(u);
    float b = rand2d(u + vec2(1.0, 0.0));
    float c = rand2d(u + vec2(0.0, 1.0));
    float d = rand2d(u + vec2(1.0, 1.0));
    return mix(
        mix(a, b, f.x),
        mix(c, d, f.x),
        f.y
    );
}

float fbm(vec2 uv) {
    const int octaves = 6;
    float amplitude = 0.5;
    float ret = 0.0; // Return value
    mat2 rot30 = mat2(vec2(sin(0.5), cos(0.5)),
        vec2(-cos(0.5), sin(0.5)));
    vec2 shift = vec2(100.0, 0.0);

    for (int i = 0; i < octaves; i++) {
        ret += amplitude * perlin(uv);
        uv = rot30 * uv * 2.0 + shift;
        amplitude *= 0.5;
    }
    return ret;
}
void main() {
    vec3 o = vec3(0.0);

    vec2 q = vec2(
        fbm(uv), fbm(uv + vec2(1.0))
    );
    vec2 r = vec2(
        fbm(uv + 1.0 * q + vec2(1.3, 2.4) * 0.1 * time),
        fbm(uv + 1.0 * q + vec2(8.9, 10.1) * 0.01 * time)
    );
    float s = fbm(uv + r);

    o = mix(vec3(0.01, 0.05, 0.1),
        vec3(0.1, 0.3, 0.6), clamp(s * s * 4.0, 0.0, 1.0));
    o = mix(o, vec3(0.3, 0.6, 0.5), clamp(length(q), 0.0, 1.0));
    o = mix(o, vec3(0.4, 0.5, 0.6), clamp(length(r), 0.0, 1.0));
    color = vec4(o, 1.0);
}
Recompile | Click on the canvas to toggle running. It is paused by default.

Really, just plug in any color. All of them are cool!

Conclusion

Well, I can’t say this is a good guide. This is just kind of a study note when I was looking at here. Too bad The Book of Shaders is an abandoned project (or is it?) :(. If you want to learn more, check the site out!

References

  1. Fractal Brownian Motion, The Book of Shaders
  2. Clouds, iq, Shadertoy
  3. Fractional Brownian motion, Wikipedia