Procedural generation: fractal noise

35

Read previous parts:

Fractal noise or value noise is an algorithm which applies to some existing function or field (grid) of values. In other words, we need something that already exist to apply fractal noise over that something.

Add some code

Before we continue, let's get back to our Grid class and add some member functions which we need in this article:

// Fill array of values with a given value
fill(value)
{
    this.values.fill(value);
}

// Add values from the given generator multiplied by the given factor to the stored values
accumulate(generator, factor)
{
    factor = factor || 1;

    for(let y = 0; y < this.height; ++y)
    {
        for(let x = 0; x < this.width; ++x)
        {
            this.values[this.indexOf(x, y)] += generator.get(x, y) * factor;
        }
    }
}

Classic approach

Assume we have a function which returns a scalar value for a given coordinates:

function f(x, y)
{
    return /* some formula */;
}

Fractal noise is a sum of series of a given function calls where each function call has exponentially growing arguments factor and exponentially decreasing magnitude. To get clear understanding consider this simple code example:

// Apply fractal noise over the function f at given coordinates
function fractalNoise(f, x, y, depth)
{
    let sum = 0;

    for(let n = 0; n < depth; ++n)
    {
        const factor = Math.pow(2, n);
        sum += f(x * factor, y * factor) / factor;
    }

    return sum;
}

// Fractal noise applied to f at (10, 5) for 8 octaves
const value = fractalNoise(f, 10, 5, 8);

So let's implement this function as well in our Grid class:

fractalNoise(depth)
{
    const buffer = this.values.slice();

    for(let y = 0; y < this.height; ++y)
    {
        for(let x = 0; x < this.width; ++x)
        {
            for(let n = 1; n < depth; ++n)
            {
                const factor = Math.pow(2, n);
                buffer[this.indexOf(x, y)] += this.get(x * factor, y * factor) / factor;
            }
        }
    }

    this.values = buffer;
}

Notice that (x, y) coordinates run through the whole grid and also multiplied by a factor. Their values get much bigger than grid width and height. And this works because grid access via get member function is tiled which we implemented in the first article.

So, fractal noise is a sum of multiple function values where function arguments are multiplied by the power of 2 (i.e. 1, 2, 4, 8, and so on), and the value returned is divided by the same factor. Let's see how does it change the picture.

Perlin noise

Let's generate Perlin noise and apply fractal noise to it:

const grid = new Grid(1200, 800);
const perlin = new Perlin(12, 8, 100);
grid.generate(perlin);
grid.fractalNoise(8);
grid.normalize();
grid.draw(document.getElementById("canvas"), colors(60));

In the example above we've created fractal noise with depth equal to 8. Here are images of every step. Notice on how does picture change with each step of the algorithm. Every level of depth adds smaller details:

Blurred white noise

Perlin noise is pretty fast, however its regular structure is noticeable. Blurred white noise looks amorphic and irregular, but it requires a huge amount of calculations.

Let's create a blurred white noise and apply fractal noise over it:

const grid = new Grid(1200, 800);
grid.noise();
grid.blur(256);
grid.fractalNoise(8);
grid.normalize();
grid.draw(document.getElementById("canvas"), colors(-30));

Refresh the page and see the results:

Unique randomization for each layer

Classic fractal noise approach has a disadvantage: as you decrease the frequency of a noise function (i.e. when you “zoom in” by increasing the scale or the blur depth), more and more unpleasant artifacts become noticeable. This happens because you repeat the same image over and over again, and the deeper the layer is — the more times the image is repeated. But we did the same with higher frequency functions and didn't notice any artifacts — you may say. Well, yes. The second part of the problem is the lower frequency you use — the less diverse is your initial data becomes. Look at how regular structure and repeating artifacts become noticeable with every zoom level:

However you don't have to use the same function (or the same image) with every fractal noise level, and you don't have to repeat it in a tiled way. You just need to keep the principle: each layer function frequency should grow exponentially, and its magnitude should decreace in the same way. So you can use different randomization for every level in the series. This approach is free from artifacts mentioned above.

Here is an example which uses different Perlin noise randomization for every fractal noise level:

function fractalNoise(grid, depth)
{
    for(let n = 0; n < depth; ++n)
    {
        const factor = Math.pow(2, n);
        const perlin = new Perlin(12 * factor, 8 * factor, 100 / factor);
        grid.accumulate(perlin, 1 / factor);
    }
}

const grid = new Grid(1200, 800);
grid.fill(0);
fractalNoise(grid, 8);
grid.normalize();
grid.draw(document.getElementById("canvas"), colors(45));

Notice that we increase Perlin grid and decrease its scale by the exponential factor with every step. We do this because we don't want the image to be repeated. Now the picture looks better and doesn't have any artifacts:

Magnitude attenuation

So, each function in the series attenuates exponentially. However following this rule is also not mandatory. You can use other rules of magnitude decreasing and get different results.

Here is Perlin noise with different randomization for every level and quadratic magnitude attenuation. The picture looks sharp:

Here is an example of a blurred white noise with different randomization for every level and linear magnitude attenuation. Just look at this beauty which took forever and a day of calculations:

Perlin noise vs blurred white noise

Those guys have their advantages and disadvantages, and some of their properties are quite opposite.

Perlin noise

  • Works pretty fast;
  • Requires more memory if you want more details;
  • Has regular structure which is noticeable.

Blurred white noise

  • Slow as hell, requires a lot of calculations. The best option is to implement it on GPU;
  • Requires more calculations if you want less details;
  • Has nice amorphic irregular structure;
  • In my opinion — has the best quality since it has the most rich entry conditions.

Resume

Feel free to experiment with everything: noise type, magnitude attenuation, exponent base, etc. There are no strict rules. The whole idea is about getting interesting procedural generated results.

Full source code for this article: source.zip. Unzip the archive and open page.html in your web browser.

Read previous parts:

Share this page: