A good random number generator produces numbers that have no relationship and show no discernible pattern. As we are beginning to see, a little bit of randomness can be a good thing when programming organic, lifelike behaviors. However, randomness as the single guiding principle is not necessarily natural. An algorithm known as “Perlin noise,” named for its inventor Ken Perlin, takes this concept into account. Perlin developed the noise function while working on the original Tron movie in the early 1980s; it was designed to create procedural textures for computer-generated effects. In 1997 Perlin won an Academy Award in technical achievement for this work. Perlin noise can be used to generate various effects with natural qualities, such as clouds, landscapes, and patterned textures like marble.
Perlin noise has a more organic appearance because it produces a naturally ordered (“smooth”) sequence of pseudo-random numbers. The graph below shows Perlin noise over time, with the x-axis representing time; note the smoothness of the curve.
Contrastingly, the next graph below shows pure random numbers over time.
ProcessingJS has a built-in implementation of the Perlin noise algorithm: the function
noise() function takes one, two, or three arguments, as noise is computed in one, two, or three dimensions. Let’s start by looking at one-dimensional noise.
The noise reference tells us that noise is calculated over several “octaves.” Calling the
noiseDetail() function will change both the number of octaves and their importance relative to one another. This in turn changes how the noise function behaves.
An online lecture by Ken Perlin lets you learn more about how noise works from Perlin himself.
Consider drawing a circle in our ProcessingJS window at a random x-location.
var x = random(0, width); ellipse(x, 180, 16, 16);
Now, instead of a random x-location, we want a Perlin noise x-location that is “smoother.” You might think that all you need to do is replace
var x = noise(0, width); ellipse(x, 180, 16, 16);
While conceptually this is exactly what we want to do—calculate an x-value that ranges between 0 and the width according to Perlin noise—this is not the correct implementation. While the arguments to the
random() function specify a range of values between a minimum and a maximum,
noise() does not work this way. Instead,
noise() expects us to pass in an argument that signifies a "moment in time," and always returns a value between 0 and 1. We can think of one-dimensional Perlin noise as a linear sequence of values over time. For example, here are example inputs and return values:
Now, in order to access one of those noise values in ProcessingJS, we have to pass a specific moment in time to the
noise() function. For example:
var n = noise(0.03);
According to the above table,
noise(3) will return 0.490 at time equals 0.03. We could improve this by using a variable for time and asking for a noise value continuously in
The above code results in the same value printed over and over. This happens because we are asking for the result of the
noise() function at the same point in time—3—over and over. If we increment the time variable
t, however, we’ll get a different result.
How quickly we increment t also affects the smoothness of the noise. If we make large jumps in time, then we are skipping ahead and the values will be more random.
Try running the code above several times, incrementing t by 0.01, 0.02, 0.05, 0.1, 0.0001, and you will see different results.
Now we’re ready to answer the question of what to do with the noise value. Once we have the value with a range between 0 and 1, it’s up to us to map that range to what we want. We could just multiply by the max number in the range, but this is also a good opportunity to introduce the ProcessingJS’s
map() function, which will help us in more situations later on. The
map() function takes five arguments. First up is the value we want to map, in this case
n. Then we have to give it the value’s current range (minimum and maximum), followed by our desired range.
In this case, we know that noise has a range between 0 and 1, but we’d like to draw a rectangle with a width between 0 and the current width.
We can apply the exact same logic to our random walker, and assign both its x- and y-values according to Perlin noise.
Notice how the above example requires an additional pair of variables:
ty. This is because we need to keep track of two time variables, one for the x-location of the
Walker object and one for the y-location. But there is something a bit odd about these variables. Why does
tx start at 0 and
ty at 10,000? While these numbers are arbitrary choices, we have very specifically initialized our two time variables with different values. This is because the noise function is deterministic: it gives you the same result for a specific time
t each and every time. If we asked for the noise value at the same time
t for both
y would always be equal, meaning that the
Walker object would only move along a diagonal. Instead, we simply use two different parts of the noise space, starting at 0 for
x and 10,000 for
y so that
y can appear to act independently of each other.
In truth, there is no actual concept of time at play here. It’s a useful metaphor to help us understand how the noise function works, but really what we have is space, rather than time. The graph above depicts a linear sequence of noise values in a one-dimensional space, and we can ask for a value at a specific x-location whenever we want. In examples, you will often see a variable named
xoff to indicate the x-offset along the noise graph, rather than
t for time (as noted in the diagram).
In the next challenge, you'll try using noise with the
Walker in a slightly different way. Have fun!
This "Natural Simulations" course is a derivative of "The Nature of Code" by Daniel Shiffman, used under a Creative Commons Attribution-NonCommercial 3.0 Unported License.