Noise

Like in the previous tutorial, we are going to continue looking at different ways to generate and use random numbers.

Random numbers are important when we are trying to make something that is difficult to predict, like when we want to create a pattern with enough variation that it doesn’t look programmed.

Let’s review the random() and randomGaussian() functions.

The random() function gives us numbers following an uniform distribution, meaning that every number within its range has an equal probability of being selected. Using uniformly distributed random numbers for the \(x\) and \(y\) locations of ellipses we get:

The randomGaussian() function gives us numbers that are centered around a mean value and spread according to a standard deviation parameter. Using normally distributed random numbers for \(x\) and \(y\) locations of ellipses, we get:

The random numbers generated by the random() and randomGaussian() functions are unpredictable because knowing something about one of the numbers doesn’t say anything about the next. But, what if we want to use sequences of random numbers where each number is somehow related to the previous value in the sequence?

Luckily, p5js has another function for generating these types of random numbers: noise().

Noise is different from the other functions because its parameters aren’t used to specify anything about the distribution of the numbers, or their range, but instead to determine how close sequentially chosen numbers should be.

For example, calling noise with the same parameter during a sketch:

print(noise(1010));
print(noise(1010));
print(noise(1010));

Will give the same number (always between \([0, 1]\)):

0.63951621
0.63951621
0.63951621

But, the parameter can be used to determine how “similar” two or more consecutive random numbers should be.

For example, calling noise with consecutive whole numbers:

print(noise(1010));
print(noise(1011));
print(noise(1012));

Gives a sequence of different numbers that are all within \(0.2\) of each other:

0.40770879
0.49712939
0.29742239

Calling noise with numbers that only vary by \(0.1\):

print(noise(1010.0));
print(noise(1010.1));
print(noise(1010.2));

we get a sequence of different numbers that are more alike, and vary by, maybe, \(0.02\):

0.36829471
0.38614744
0.39177833

So… How do we use it?

Well, it depends on how much we want our number sequence to vary, and what other values we have available to use as the parameter to the noise() function.

A simple example, similar to the ones above, would be to use noise() to pick random \(x\) and \(y\) locations for some ellipses.

Let’s say we start with something like this:

for (let i = 0; i < NUM_SAMPLES; i++) {
  let x = width * noise(i);
  let y = height * noise(i);
  ellipse(x, y, 10);
}

Since the values returned by noise() are in the range \([0, 1]\), we multiply them by width and height to get values that stretch across our whole canvas.

Hmmm….

Since noise() returns the same value when called with the same parameter, the \(x\) and \(y\) variables in the sketch above will always have the same value and our ellipses get drawn in a diagonal line. Boring.

One way to fix this is to give the noise() function for each variable a fixed offset, like:

for (let i = 0; i < NUM_SAMPLES; i++) {
  let x = width * noise(1010 + i);
  let y = height * noise(2020 + i);
  ellipse(x, y, 10);
}

This is better, but it still looks a lot like the random() or the randomGaussian() functions:

This is because the parameter that we are giving to the noise() function varies by \(1\) between iterations, and from our exploration above we saw that consecutive whole numbers return a sequence that can have variations of up to \(0.2\) between consecutive numbers. This is quite a lot. \(0.2\) is about \(\frac{1}{5}\) of our total range, and the resulting sequence can look pretty much like random() or randomGaussian().

We can make consecutive numbers more similar by incrementing the parameter to noise by \(0.01\) instead of \(1\):

for (let i = 0; i < NUM_SAMPLES; i++) {
  let x = width * noise(1010 + i / 100);
  let y = height * noise(2020 + i / 100);
  ellipse(x, y, 10);
}

Oh, whoa. So now both the \(x\) and \(y\) locations for our ellipses vary by small amounts between each iterations.

We can keep clicking, but since the noise() numbers are completely determined by the value of i and the offset constants we used (\(1010\) and \(2020\)), we’ll always get the same sequence of numbers for the \(x\) and \(y\) locations.

We can add another offset to our parameter to noise(), one that is based on time, so that every time we click the mouse, the starting parameter for our sequence of noise() calls will be different:

for (let i = 0; i < NUM_SAMPLES; i++) {
  let x = width * noise(frameCount + 1010 + i / 100);
  let y = height * noise(frameCount + 2020 + i / 100);
  ellipse(x, y, 10);
}

Which results in this:

Pretty cool.


Another great property of the noise() function is that it works in \(2\) and \(3\) dimensions.

Instead of giving noise() just one parameter, we can give it \(2\) or \(3\), and it will use all of them to determine how much the numbers in our sequence will vary. The 2-dimensional version, noise(x,y), will return values within the range \([0, 1]\) that change in proportion to how much \(x\) and \(y\) change between iterations.

We can use this to visualize a 2D noise field by iterating through the canvas and assigning a grayscale color value to each pixel based on its \(x\) and \(y\) location:

Using both \(x\) and \(y\) as parameters to the noise() function lets us ensure that pixels that are near have similar random color values.

We can add some thresholding and colors to create a low-fi map generator:

We can add a frameCount offset to one or both of the parameters, so we get a different map every time we click:

Or, we can redraw the map every frame using this frameCount offset, but since the two parameters to noise() are now related to the position in which a pixel is going to be drawn, this gives us a sliding animation effect, because it’s as if the \(x\) parameter is increasing by \(1\) between frames:

What we can do is pass a time-based value to the third parameter of the noise() function to make our 2D noise map change over time:


Noise can be used to animate elements/characters/particles. This moves an ellipse in a way that makes it look like a balloon in the wind:

The part of the code that calculates the velocity components uses something like this:

let nx = noise(pos.x, pos.y, frameCount);
let ny = noise(frameCount, pos.x, pos.y);

In addition to frameCount, we use the \(x\) and \(y\) positions for calculating the new velocity components, but when we call noise() the second time the two argument types are swapped!

The reason is that, if we had this:

let nx = noise(pos.x, pos.y, frameCount);
let ny = noise(pos.x, pos.y, frameCount);

both calls to the noise() function would return the same value for nx and ny and our ellipse would be stuck moving in a diagonal. So we swap the parameter order to avoid this situation.

If we modify the above sketch to draw many points over time, we can get some organic looking motion that looks like water flow or currents:

And if we remove the time parameter from the noise() calls, don’t clear the background, and restart the points only when they slow down, we get some drawings that could look like hair or leafs: