2D terrain maps (for side-scrollers): how to modulate strata with Perlin Noise

Ken Perlin writes about using his noise function to modulate or perturb a given color on slide 20 of his talk “Making Noise”, e.g., making the color lighter or darker or varying its hue. [EDIT: unfortunately, the article has been removed from the web and the following link is dead. http://www.noisemachine.com/talk1/20.html] When working on a 2D terrain map for a side-scroller, we can take this another step, and use the noise to modulate a strata set.

By strata, I am referring to a series of layers of various air and ground types. The basic layering function might be something like the following pseudo code function (for a board with a Y dimension of 24 blocks):

   getLayerType(int y) {
        if y < 4 then return layer1
        if y < 8 then return layer2
        if y < 12 then return layer3
        if y < 16 then return layer4 
        if y < 20 then return layer5
        else return layer6
    }

Note that here, the strata depend entirely on the Y value. The X value does not matter.

The way in which you assign layer types can be calculated either bottom or top or the reverse, and at any level of granularity. In the following graphic, the dims are 500 x 300, and the layering uses the following logic:

blue > 240
green > 180
grey > 120
brown > 60
black > 0

The layers do not have to be of even thickness. For example, a thinner green layer might be a good way to suggest the ground’s surface. The main point is that the basic layering, which you will use a starting point, should be something that is encapsulated in a function.

Now, you can modulate or perturb this function by adding a random component. Perlin Noise comes in various forms. I happen to like the Simplex Noise variant, but the “Improved Perlin Noise” is also a good choice. There are open source choices that can be found via search.

A 2D noise function usually takes the following form:

    noiseValue = getNoise( f(x), f(y) );

I use the notation f(x) and f(y) instead of (x, y) for two reasons:

(1) we want the noise value to relate to the position in the board array or the graphic. In this, I’m taking x to range from 0 to the width of the board array or graphic, and y to range from 0 to the height.

(2) the exact int values used for the [x, y] inevitably have to be modified to give a desired periodicity to the perturbations. The most common modification is a simple scaling function, e.g., f(x) = x * k, but more complex functions are possible, such as equations to suggest the illusion of perspective.

When iterating through the grid of the game screen, or through the pixels of the graphic, the function that returns the layer type consists of your basic layer function, with the Y value being altered by the noise value. Following is a pseudo code example:

    for each y
        for each x
            noiseVal = noiseFunction(x * k, y * k);
            layerType = getLayerType( y + (noiseVal * modulationFactor) );

The sharpness of the contours depends on how you scale and proportion things. Noise functions typically return values ranging from -1 to 1. This has to be scaled by some factor in order to give a return value range that works for your desired level of smoothness (modulationFactor). Too little can be too flat or boring, too much can create sharp edges or lead to pockets of layers appearing out of sequence, e.g., a cave of air, or worse, clouds of green. Unfortunately, there’s no guarantee that a good middle ground exists via this method. Sometimes to get an acceptable result, another iteration is needed (e.g., make a second pass where you impose a rule that for any vertically adjacent points, the delta Y is always positive and always less than a certain maximum).

In the above, the k value is used to convert the series of integers that identify pixel or grid location into a range that relates to the ‘period’ of the random curve lengths. If the noise function returns a maximum of one full wave curve per one unit (this is usually the case, or something close), and you want up to 2 waves per screen (where the screen is 50 units wide), then the scaling factor should be 2 / 50, or, k = 0.04. Similarly, if you wanted an average of 3 complete waves over the course of 500 pixels, the scaling factor would be 3 / 500, or k = 0.006. Note: the k value does not have to be the same for the X and Y directions.

In the following graphic, I modulated the strata function with a moderate amount of noise. I don’t have the exact amount because the tool I used to generate the graphic only gives relatives value relating to its sliders. By eye-balling, I’m going to estimate that the modulationFactor gives us a variability approximately equal to the width of a single layer (60 pixels), So, it should be equal to 30 in this case, giving us +/-30 when multiplied against the [-1, 1] range of the noise function output.

I hope the above explanation helps you to understand better understand how to use Perlin noise, and the concept of how to calculate the factors needed to scale both the periodicity and the strength of the noisiness.

One other note: you aren’t restricted to making a single call to Perlin noise. Often, periodicities are combined by making multiple Noise function calls, each with a different K, with the results combined at various proportions. This technique too much to cover in this tutorial, but you can [Edit: no longer, article has been removed] read Ken Perlin’s description of a fractal example at the following location, slide 21 of his “Making Noise” presentation. http://www.noisemachine.com/talk1/21.html

The diagrams were made with SiVi (Simplex Visualizer) http://github.com/philfrei/SiVi

How does one edit a tutorial post?

At the bottom of your pist you see the edit-icon.