/u/7434365
/joeiddon

Creating Perlin Noise in JS

Perlin noise is a type of random, yet smooth, noise that can be easily generated using a particular algorithm.

Before delving into the steps to generate Perlin noise, let's inspect the output.

Static Noise

Perlin Noise

Okay so how do we write this in JavaScript? First we will write it the straightforward way, then we will optimise it for efficiency and simplicity.


Step 1: We need a two-dimensional grid of "nodes". The size of this grid determines roughly how zoomed in our noise is going to be.

In JavaScript we can use a nested array to describe this grid. This means it is initialised as empty then rows are appended, but this will come later.

The variable nodes defines the number of nodes we have in each dimension.

let grid = [];
const nodes = 4;

At this point you may be wondering: what is a node? It is simply a unit vector describing the slope of the noise at fixed intervals in the image.


Step 2: Assign a random gradient vector of unit length to each node of the grid.

A unit vector is simply a pair of numbers, (x, y), that has a length of one. (I.e. x2 + y2 = 1.)

There are a couple of diferent ways to randomly generate these, but possibly the simplest is to generate a random angle and then use the sine and cosine trigonometric functions built in to JavaScript.

Additional: We refer to these as "gradient" vectors because they are going to determine the slope of the terrain/noise at each point in the grid. A continuous function is then created through interpolation between these gradients.

Let's make a function to do this random vector generation. It will return the coordinate as an object with x and y attributes.

function random_unit_vector(){
    let theta = Math.random() * 2 * Math.PI;
    return {x: Math.cos(theta), y: Math.sin(theta)};
}

Now we need to populate the grid with these random vectors. A nested for-loop can be used.

for (let i = 0; i < nodes; i++) {
    let row = [];
    for (let j = 0; j < nodes; j++) {
        row.push(random_unit_vector());
    }
    grid.push(row);
}

Step 3: We need to accept the input to the noise function as an (x,y) coordinate.

Taking a step back - what functionality are we trying to provide here? The Perlin noise algorithm describes a function. This takes an input of a 2D coordinate, and returns a number. The number determines the intensity/"height" of the noise at that coordinate.

But how can a function that returns an intensity be used to create the smooth image shown at the start of this article? The answer - the image is not continuous, but discrete or "pixelated". So the image is created by iterating over each pixel coordinate, running the Perlin function for that coordinate, and assigning the intensity to that pixel.

Alternatively, if we wanted to generate a 3D landscape using the noise function, we would have to make it discrete by sampling heights only at fixed positions (nodes in a pixelated grid) and piece together triangular pieces to give a 3D landscape.

Therefore we need a function. Lets call it perlin_get, and it will take a two-dimensional input (x and y), and return an intensity.

function perlin_get(x, y) {
    ...
    return intensity;
}

Step 4: Determine its cell in the grid, where the "cell" is the square of integer coordinates around our x and y. For example, if x = 1.4 and y = 3.6, then the cell is from x = 1 to 2 and y = 3 to 4.

How do we do this in JavaScript? Well we can use the Math.floor(...) function.

let x0 = Math.floor(x);
let x1 = x0 + 1;
let y0 = Math.floor(y);
let y1 = y0 + 1;

Having found the cell nodes, we can calculate the dot product between the distance vector between the point and the node and the random gradient vector from our array. Calculating the distance vector and the dot product can be condensed together into one function:

function dot_prod_grid(x, y, vert_x, vert_y){
    var g_vect = gradients[vert_y][vert_x];
    var d_vect = {x: x - vert_x, y: y - vert_y};
    return d_vect.x * g_vect.x + d_vect.y * g_vect.y;
}

Finally, we need to interpolate between the four cell node dot-products (scalars). Interpolation is a way of constructing new data points within a range of others. To do this, a function is used, the simplest being linear:

function lin_interp(x, a, b){
    return a + x * (b-a);
}

So if we run linTerp(0, 56, 98) we will get 56 and if we run with x = 1 we get b so 98 in this case. The point of the interpolation is that now, any value of x will map between the two outer values. So running linTerp(0.5, 24, 64) will give us the value exactly in the middle so 44. However, linear interpolation produces slight 'ridges' inbetween each cell as their is no curves, just straight lines. To add a very rounded and smooth looking curve/ fade inbetween the cells, we can use Perlin's smootherstep function (a quintic polynomial):

smootherstep: function(x){
    return 6*x**5 - 15*x**4 + 10*x**3;
},
interp: function(x, a, b){
    return a + smootherstep(x) * (b-a);
},

To interpolate between the four cell nodes with this function, we must do so in three steps (bilenear interpolation). First by interpolating how far the x distance of the input is into the cell (between 0 and 1) between the top two dot-products and then interpolating the inputs x value into the cell again between the bottom two dot-products of the cell. Finally, interpolate between the two previous values to get the final value for that noise which will be returned from the library. Note that as the function returns a value between -1 and 1, and I would like it to return a value between 0 and 1, the final value is mapped to my range by incrementing and dividing by 2.

And that's it for the explanation. Head over to GitHub where you can view my finished Perlin Noise library.

Update 06/01/2021: Shoutout to Julian Fietkau for his use of my library.

To see how this result can be used for something more interesting than these blurry gradients, checkout my 3D terrain generator.