Noise. Random Noise. Perlin Noise. With Jupyter Notebook.

Noise.

Random Noise. Perlin Noise.

Perlin Noise Algorithm

% matplotlib inline
import math 
import random 
import numpy as np 
import matplotlib.pyplot as plt

Introduction

This is an article about content generation through algorithms.
It sounds fancy, but the idea is a simple one – to generate textures or materials automatically, from numbers. Without real drawing in “Paint” or in another PC program. With other words, if we have the following binary np.array in python:

example = [[1, 0], [0, 1]]

we would be able to generate the following image with it in python easily:
Ok, it is quite visible, that somehow we have plotted the 0 as a black square and the 1 as white squire.
Is this useful somehow? Probably, if you feel like building online chess boards and you can find someone to pay you for that. Anyway, the idea is to make it a bit more complicated from there, as between 1 and 0 we have enough values, and we will be able to generate something more interesting, based on the numbers there.

So, if the numbers are a bit random, we will get some random noise, that will look really not following any pattern, like this:

The idea of this article is to provide a tool, to build noise, that looks like it is not-random (but it is random!):

This, non-random looking noise that we will be building is called “Perlin Noise” and is an extremely powerful algorithm that is used often in procedural content generation. It is especially useful for games and other visual media such as movies.[1] Named after Ken Perlin, this technique earned him an “Oscar” for its originality. In game development, Perlin Noise can be used for any sort of wave-like, undulating material or texture. For example, it could be used for procedural terrain (Minecraft-like terrain can be created with Perlin Noise, for example), fire effects, water, and clouds.[1] Generally, Perlin Noise has a more organic appearance because it produces a naturally ordered (“smooth”) sequence of pseudo-random numbers [2].

Research Questions:

A good research question can be defined as How to build Perlin Noise in python?
A subquestion can be How to distinguish random noise from Perlin Noise?

A few lines about random noise (with examples)

Before going into Perlin Noise, it will be useful to get a few examples of non-Perlin Noise. Let’s plot a 6x6 non-random np.array(), just to see how it works:

def plot_noise(noise, plt_title, cmap_given = "gray"):
    plt.imshow(noise, cmap=cmap_given, interpolation='nearest')
    #plt.colorbar()
    plt.title(plt_title)
    plt.show()
non_random_noise = [
    [1,   0,  1,  0,  1,  0],
    [.1, .9, .1, .9, .1, .9],
    [.8, .2, .8, .2, .8, .2],
    [.3, .7, .3, .7, .3, .7],
    [1,   0,  1,  0,  1,  0],
    [.4, .6, .4, .6, .4, .6]
]

plot_noise(non_random_noise,'Looks like a strange 6x6 chess')

Well, the picture above looks like strange 6×6 chess, with some squares in white and some in grey or being black completely. As discussed in the introduction, this is due to the fact that all the numbers between 0 and 1 can be plotted in the grey colors schema between black and white.

Let’s continue with random, non-Perlin noises. First we will plot some 8x8 random noises and then we will plot 10000x10000.

def generate_random_noise(width, height):
    noise = np.random.rand(width, height)
    return noise
def make_some_noise(size, label=""):
    width = size
    height = size
    random_noise = generate_random_noise(width, height)

    # Plot the noise
    plot_noise(random_noise, label)
# Generate tiny random noise
size = 8
title = "8x8 random noise"
make_some_noise(size, title)
# Generate huge random noise
size = 10000
title = "10000x10000 random noise"
make_some_noise(size, title)

Well, it is obvious that the noises are random. But are these Perlin Noises? In general, the answer is simple no, but proving so might require a bit of an effort. The easiest way to prove it, is to open the noise plot in paint and to cut something:
If we can paste it anywhere in the picture without having the picture “distroyed” and without seeing where exactly did we paste it, then it is not a Perlin noise:

Perlin noise methodology

In this part of the article, an explanation for the “Perlin noise” is to be written.
And then the code. At the end the nice random pictures, that are nice, because they look non-random.

  1. Create an empty noise array
  2. Generate random gradient vectors
  3. Iterate over each pixel in the noise array and
    • Calculate the grid cell coordinates for the current pixel
    • Calculate the position within the cell as fractional offsets
    • Calculate the dot products between gradients and offsets
    • Interpolate the dot products using smoothstep function
    • Store the interpolated value in the noise array
  4. Normalize the noise values within the range of 0 to 1
def generate_perlin_noise(width, height, scale):
    """
    Generate Perlin noise using the given parameters.
    
    Parameters:
    - width (int): Width of the noise array.
    - height (int): Height of the noise array.
    - scale (int): Scale factor for generating the noise.
    
    Returns:
    - noise (n-dimensional array): Perlin noise array of shape (height, width).
    """

    
    # Create an empty noise array
    noise = np.zeros((height, width))
    
    # Generate random gradient vectors
    gradients = np.random.randn(height // scale + 2, width // scale + 2, 2)

    # Iterate over each pixel in the noise array
    for y in range(height):
        for x in range(width):
            # Calculate the grid cell coordinates for the current pixel
            cell_x = x // scale
            cell_y = y // scale

            # Calculate the position within the cell as fractional offsets
            cell_offset_x = x / scale - cell_x
            cell_offset_y = y / scale - cell_y

            # Calculate the dot products between gradients and offsets
            dot_product_tl = np.dot([cell_offset_x, cell_offset_y], gradients[cell_y, cell_x])
            dot_product_tr = np.dot([cell_offset_x - 1, cell_offset_y], gradients[cell_y, cell_x + 1])
            dot_product_bl = np.dot([cell_offset_x, cell_offset_y - 1], gradients[cell_y + 1, cell_x])
            dot_product_br = np.dot([cell_offset_x - 1, cell_offset_y - 1], gradients[cell_y + 1, cell_x + 1])
          
            # Interpolate the dot products using smoothstep function
            weight_x = smoothstep(cell_offset_x)
            weight_y =  smoothstep(cell_offset_y)
            interpolated_top = lerp(dot_product_tl, dot_product_tr, weight_x)
            interpolated_bottom = lerp(dot_product_bl, dot_product_br, weight_x)
            interpolated_value = lerp(interpolated_top, interpolated_bottom, weight_y)

            # Store the interpolated value in the noise array
            noise[y, x] = interpolated_value
            
    # Normalize the noise values within the range of 0 to 1
    noise = (noise - np.min(noise)) / (np.max(noise) - np.min(noise))

    return noise

def smoothstep(t):
    """
    Smoothstep function for interpolation.
    
    Parameters:
    - t (float): Interpolation value between 0.0 and 1.0.
    
    Returns:
    - result (float): Smoothstep interpolated value.
    """
    return t * t * (3 - 2 * t)

def lerp(a, b, t):
    """
    Linear interpolation between two values.
    
    Parameters:
    - a (float): Start value.
    - b (float): End value.
    - t (float): Interpolation factor between 0.0 and 1.0.
    
    Returns:
    - result (float): Interpolated value between a and b.
    """
    return a + t * (b - a)
Now, let’s run the code, with the provided functions.
# Set the width, height, and scale parameters
width = 256
height = 256
scale = 10

# Generate the Perlin noise
noise = generate_perlin_noise(width, height, scale)

# Plot the Perlin noise
plot_noise(noise, "Perlin noise example", cmap_given = "twilight")

Looks ok-ish, but we may try all the supported_cmap values, built-in matplotlib. In the supported_cmap list, add the commented values to the list, to see all of the possible cases. It might take up to 1 minute to generate all, that is why I have commented most of these.
supported_cmap = ['Accent', 'Accent_r', 'Blues', 'Blues_r', ]#'BrBG', 'BrBG_r', 'BuGn', 'BuGn_r', 'BuPu', 'BuPu_r', 'CMRmap'

 

for cmap in supported_cmap:
    plot_noise(noise, f'Perlin Noise with {cmap}', cmap_given = cmap)

Conclusion

In this article, we explored Perlin Noise and how it can be used to create interesting patterns.

The Perlin Noise algorithm works in a step-by-step manner to generate natural-looking random patterns (Or with other words – non-random looking random numbers). The algorithm starts by creating an empty grid where the noise values are stored. Then, random gradients are generated to serve as reference vectors for calculations.

The algorithm goes through each pixel in the grid and determines its position within the grid. By using fractional offsets, it calculates the dot products between the gradients and the offsets. These dot products are then blended together using a smooth function to create smooth transitions between the grid points, hence the borders between these are made a bit “invisble”.
The resulting value is stored in the grid as the noise value for that pixel. To ensure that the noise values are in a desired range, a normalization step is performed. This step scales the noise values to a range between 0 and 1, making them easier to work with.

By following this methodology, we can generate random patterns that express natural phenomena like fire, clouds, textures.

In conclusion, Perlin Noise is a powerful technique enabling the quick creation of visually interesting and realistic random patterns. As next steps one can concentrate on additional examples and applications of Perlin Noise – for instance, it can explore how Perlin Noise can be used to generate realistic terrain heightmaps for games or create textures for 3D modeling.

Attachments

Tests

def test_perlin_noise():
    # Set test parameters
    width = 256
    height = 256
    scale = 10

    # Generate Perlin noise
    noise = generate_perlin_noise(width, height, scale)

    # Ensure the generated noise array has the correct shape
    assert noise.shape == (height, width), "Incorrect shape"

    # Ensure all values in the noise array are within the expected range
    assert np.min(noise) >= 0, "Min noise value is less than 0"
    assert np.max(noise) <= 1, "Max noise value is more than 1"

    # Plot the Perlin noise
    plot_noise(noise, "Perlin Noise Test", cmap_given = "gray")
    
# Run the test
test_perlin_noise()

Sources: