Introduction to Perlin Noise

Perlin Noise was developed by Ken Perlin in 1983 for the film Tron as a smooth pseudo-random noise algorithm1. It can generate random patterns with natural-looking texture and is widely used in computer graphics to simulate natural phenomena such as clouds, terrain, fire, wood grain, and water flow2.

Unlike plain white noise, Perlin Noise has spatial correlation: values at neighboring sample points vary smoothly without abrupt jumps. This smoothness makes the generated textures resemble the continuous variations found in nature.

Minecraft, for example, uses Perlin Noise as a foundational algorithm to generate its effectively infinite world maps.

Types of Noise

1D Uniform White Noise

1D uniform white noise is a one-dimensional noise signal where values follow a uniform distribution.

import numpy as np
import matplotlib.pyplot as plt

def sample_random_points(num_points):
    # Sampler function: return num_points random numbers in the range [0, 1)
    return np.random.rand(num_points)

def draw_random_strip_image_from_samples(rand_1d, width, height, filename, thickness=1):
    num_points = len(rand_1d)
    cols_per_point = width // num_points

    rand_pos = (rand_1d * (height - 1)).astype(int)
    img = np.zeros((height, width))
    for i, row in enumerate(rand_pos):
        start_row = max(row - thickness, 0)
        end_row = min(row + thickness + 1, height)
        start_col = i * cols_per_point
        end_col = start_col + cols_per_point
        img[start_row:end_row, start_col:end_col] = 1

    plt.imshow(img, cmap='gray', origin='lower')
    plt.axis('off')
    plt.show()
    plt.imsave(filename, img, cmap='gray')

if __name__ == "__main__":
    rand_noise = sample_random_points(512)
    draw_random_strip_image_from_samples(rand_noise, 1024, 512, "random_noise_1d.png")

For one-dimensional random noise, the generated points have no correlation between them.

random_noise_1d

If we use Perlin Noise instead, we get a continuous and smooth curve.

perlin_noise_1d

2D Uniform White Noise

If we randomly generate a noise image with a uniform distribution, the result is uniform white noise: each point has no relation to its neighbors.

import numpy as np
import matplotlib.pyplot as plt

# Generate a 512x512 random floating-point grayscale image with values in [0, 1)
noise = np.random.rand(512, 512)

plt.imshow(noise, cmap='gray')
plt.axis('off')
plt.imsave('random_noise.png', noise, cmap='gray')

Completely random white noise:

random_noise

Perlin Noise, by contrast, has correlations between neighboring points — adjacent values are required to be close and smooth.

perlin_noise_2d

Gaussian White Noise

import numpy as np
import matplotlib.pyplot as plt

# Generate 512x512 Gaussian white noise with mean 0.5 and std deviation 0.1
noise = np.random.normal(loc=0.5, scale=0.1, size=(512, 512))

# Clip values to [0, 1] to avoid display artifacts
noise = np.clip(noise, 0, 1)

plt.imshow(noise, cmap='gray')
plt.axis('off')
plt.imsave('gaussian_white_noise_2d.png', noise, cmap='gray')
plt.show()

Because it is not uniformly distributed, Gaussian noise often looks a bit softer than uniform white noise.

gaussian_white_noise_2d

The Gaussian distribution is even more apparent in one dimension, where a clear central band concentrates most of the points.

gaussian_white_noise_1d

1D Perlin Noise

To understand the fractal-related concepts in Perlin Noise, we first need to understand the ideas of fade functions, periodicity, and frequency multipliers (octaves)3.

Fade

Given a series of known point values, to estimate values between them we can use linear interpolation to produce straight-line segments or cosine interpolation to produce smooth curves.

lerp

Cosine interpolation can be written as:

$$ x = x1 + (x2-x1) * \frac{1 - \cos(\pi \alpha)}{2} $$

The goal of different interpolation methods is to produce a smooth transition rather than a sharp corner. Besides cosine, cubic, quintic, and other polynomials are commonly used.

Converting linear interpolation into a smooth interpolation like this is called the Fade operation.

In the improved version of his noise algorithm, Perlin used the following Fade function:

$$ t = 6 t^5 - 15 t^4 + 10 t^3 $$

Octaves

If you repeatedly double the sampling frequency, you obtain functions with halved periods. Because doubling frequency corresponds to musical octaves, these levels are called octaves.

octaves

Perlin Noise is composed by mixing noises of different periods with certain weights.

Typically, periods are doubled while weights are halved, which introduces a fractal-like concept.

persistence

2D Perlin Noise Generator

Algorithm

Ken Perlin published an implementation of his improved Perlin Noise on his website4.

Perlin Noise is a grid-based gradient noise function commonly used to generate natural-looking textures and terrain. The two-dimensional implementation proceeds as follows:

1. Grid partitioning

Divide the 2D space into a regular integer grid. Each grid corner (lattice point) is associated with a random gradient vector (often unit vectors uniformly distributed in direction).

2. Compute the sample point’s local position

Given a sample point $(x, y)$, determine its containing grid cell by computing the integer coordinates $(X, Y)$ of the cell’s lower-left corner:

$$ X = \lfloor x \rfloor, \quad Y = \lfloor y \rfloor $$

Compute the sample point’s local coordinates relative to the lower-left corner:

$$ x_{\text{rel}} = x - X, \quad y_{\text{rel}} = y - Y $$

3. Compute dot products between corner gradients and displacement vectors

For the four corners of the cell:

$$ (X, Y),\ (X+1, Y),\ (X, Y+1),\ (X+1, Y+1) $$

retrieve each corner’s gradient vector $\vec{g}$ and compute the dot product between $\vec{g}$ and the displacement vector $\vec{d}$ from the corner to the sample point:

$$ \text{dot} = \vec{g} \cdot \vec{d} $$

4. Smooth interpolation

Apply a smooth interpolation function (such as Perlin’s fade) along the $x$ and $y$ axes to bilinearly interpolate the four dot products:

$$ u = \text{fade}(x_{\text{rel}}), \quad v = \text{fade}(y_{\text{rel}}) $$

5. Output noise value

The interpolated result is the noise value at the sample point $(x,y)$, typically in the range approximately $[-1, 1]$.

Python Code

Below is an implementation of 2D Perlin Noise in Python.

import numpy as np
import matplotlib.pyplot as plt

permutation = np.array([ 151,160,137,91,90,15,
131,13,201,95,96,53,194,233,7,225,140,36,103,30,69,142,8,99,37,240,21,10,23,
190, 6,148,247,120,234,75,0,26,197,62,94,252,219,203,117,35,11,32,57,177,33,
88,237,149,56,87,174,20,125,136,171,168, 68,175,74,165,71,134,139,48,27,166,
77,146,158,231,83,111,229,122,60,211,133,230,220,105,92,41,55,46,245,40,244,
102,143,54, 65,25,63,161, 1,216,80,73,209,76,132,187,208, 89,18,169,200,196,
135,130,116,188,159,86,164,100,109,198,173,186, 3,64,52,217,226,250,124,123,
5,202,38,147,118,126,255,82,85,212,207,206,59,227,47,16,58,17,182,189,28,42,
223,183,170,213,119,248,152, 2,44,154,163, 70,221,153,101,155,167, 43,172,9,
129,22,39,253, 19,98,108,110,79,113,224,232,178,185, 112,104,218,246,97,228,
251,34,242,193,238,210,144,12,191,179,162,241, 81,51,145,235,249,14,239,107,
49,192,214, 31,181,199,106,157,184, 84,204,176,115,121,50,45,127, 4,150,254,
138,236,205,93,222,114,67,29,24,72,243,141,128,195,78,66,215,61,156,180
], dtype=np.uint16)

p = np.tile(permutation, 2)

def grad(hash:int, x:float, y:float)->float:
    grad_vectors = np.array([
        [1, 1], [-1, 1], [1, -1], [-1, -1],
        [1, 0], [-1, 0], [0, 1], [0, -1]
    ])
    # Normalize gradient vectors
    grad_vectors = grad_vectors / np.linalg.norm(grad_vectors, axis=1)[:, None]

    g = grad_vectors[hash % 8]

    return np.dot(g, [x, y])

def lerp(t: float, a: float, b: float)->float:
    return a + t * (b - a)

def fade(t: float)->float:
    return t * t * t * (t * (t * 6 - 15) + 10)

def get_noise(x:float, y:float):
    X = int(np.floor(x)) & 255
    Y = int(np.floor(y)) & 255
    x = x % 1
    y = y % 1
    u = fade(x)
    v = fade(y)
    ## Here we only need to ensure the hash values are unique
    x1 = p[p[X] + Y]
    x2 = p[p[X+1] + Y]
    x3 = p[p[X] + Y + 1]
    x4 = p[p[X+1] + Y + 1]
    ## x3 x4
    ## x1 x2
    ## The direction of each vector is from the integer lattice point towards the noise sample point
    return lerp(v, lerp(u, grad(x1, x, y), grad(x2, x - 1, y)), 
                   lerp(u, grad(x3, x, y - 1), grad(x4, x - 1, y - 1)))

def get_perlin_noise(width=256, height=256, scale=8.0, octaves=1, persistence=0.5, lacunarity=2.0):
    noise_arr = np.zeros((height, width), dtype=np.float32)
    for i in range(height):
        for j in range(width):
            for k in range(octaves):
                x = (j / width) * scale * (lacunarity ** k)
                y = (i / height) * scale * (lacunarity ** k)
                noise_arr[i, j] += (persistence ** k) * get_noise(x, y)
    return noise_arr


fig, axs = plt.subplots(2, 2, figsize=(10, 10))

for octave_count, ax in zip(range(1, 5), axs.flatten()):
    noise_arr = get_perlin_noise(width=256, height=256, scale=8.0, octaves=octave_count)
    # Optionally normalize to [0, 1]
    noise_arr = (noise_arr - noise_arr.min()) / (noise_arr.max() - noise_arr.min())
    ax.imshow(noise_arr, cmap='gray')
    ax.set_title(f'octaves = {octave_count}', fontsize=14)
    ax.axis('off')

plt.tight_layout()
plt.savefig('perlin_noise_octaves_1_to_4.png')

perlin_noise_octaves_1_to_4

How to improve gradient values

Using fixed gradient directions instead of gradients produced by a fully uniform random hash helps avoid directional bias that degrades the natural appearance5.

In the original approach, gradient directions were uniformly distributed over the sphere. However, a cube is not a sphere: projections along coordinate axes are shorter while diagonal directions are longer.

This asymmetry in direction can cause a sparse clustering effect: when several nearby gradients that are nearly axis-aligned happen to point the same way, those regions can show anomalously high values.

anomalously high values

In his improved version, Perlin suggested choosing gradients like:

$$ (1,1,0),(-1,1,0),(1,-1,0),(-1,-1,0), (1,0,1),(-1,0,1),(1,0,-1),(-1,0,-1), (0,1,1),(0,-1,1),(0,1,-1),(0,-1,-1) $$

To avoid the cost of dividing by 12, he expanded the set to 16 directions by adding $(1,1,0),(-1,1,0),(0,-1,1),(0,-1,-1)$.

This article follows that idea: it uses eight planar directions for gradients, and each gradient is normalized to unit length.

For one-dimensional Perlin Noise, you can omit normalization to increase variation. A 1D Perlin Noise can be seen as sampling a 2D Perlin Noise along a line through the lattice; lattice points off the line have zero weight and the result is equivalent to the 1D case. In that situation, the projection of a diagonal gradient onto the horizontal axis is not 1 but $\frac{\sqrt{2}}{2}$.

Improving the Fade function

Perlin5 proposed replacing the fade function with a smoother polynomial (the quintic above).

The reason is: if the second derivative is not smooth, when used to displace surfaces you can see obvious blocky artifacts.

Noise-displaced

So it’s best to follow these principles:

  • At 0 and 1, derivatives should be 0 so transitions are as smooth as possible. Higher-order derivatives can also be made zero.
  • $ f(0) = 0 $
  • $ f(1) = 1 $

Examples / Applications

Python Noise Library

In practice, you rarely need to implement Perlin Noise from scratch — the Python noise library can generate Perlin Noise directly.

import numpy as np
import matplotlib.pyplot as plt
from noise import pnoise2

width, height = 512, 512
scale = 64.0  # Controls the "stretch" of the noise; larger values make changes occur more slowly
octaves = 6
persistence = 0.5
lacunarity = 2.0

noise = np.zeros((height, width))
for y in range(height):
    for x in range(width):
        nx = x / scale
        ny = y / scale
        noise[y][x] = pnoise2(nx, ny, octaves=octaves, persistence=persistence, lacunarity=lacunarity, repeatx=1024, repeaty=1024, base=0)

# Perlin noise is typically in approximately [-1, 1]; normalize to [0, 1]
noise = (noise - noise.min()) / (noise.max() - noise.min())

plt.imshow(noise, cmap='gray')
plt.axis('off')
plt.imsave('perlin_noise_2d_lib.png', noise, cmap='gray')
plt.show()

Because it is optimized, generation is very fast:

perlin_noise_2d_lib

Clouds

Use Perlin Noise to simulate simple cloud textures.

  • Simulate the dissipating edges of clouds.
  • Simulate central highlights; above a threshold, values remain unchanged, and within a certain range apply the following operation:

$$ \text{Intensity} = \max(\text{Intensity} - 40, 0) $$

import numpy as np
from PIL import Image

# Read grayscale image, keep integer values in 0~255
img = Image.open('perlin_noise_2d_lib.png').convert('L')
arr = np.array(img).astype(np.int32)  # 0~255

height, width = arr.shape

# Create an RGB image, pure blue background (R,G,B) = (0,0,255)
rgb_img = np.zeros((height, width, 3), dtype=np.uint8)
for y in range(height):
    for x in range(width):
        r, g, b = 0, 0, 255
        
        cloud_intensity = arr[y, x]  # 0~255

        cloud_intensity = cloud_intensity - 30

        if cloud_intensity < 100:
            cloud_intensity = max(cloud_intensity - 40, 0)

        # Linear blend: cloud_intensity represents white cloud strength (0~255)
        # Compute weight: cloud_intensity / 255
        w = cloud_intensity / 255
        
        r = int(r * (1 - w) + w * 255)
        g = int(g * (1 - w) + w * 255)
        b = int(b * (1 - w) + w * 255)
        
        rgb_img[y, x, 0] = r
        rgb_img[y, x, 1] = g
        rgb_img[y, x, 2] = b

# Save the uint8 array directly
im = Image.fromarray(rgb_img)
im.save('perlin_cloud.png')

Cloud result:

perlin_cloud


  1. Perlin, K. (1985). An image synthesizer. ACM Siggraph Computer Graphics, 19(3), 287-296. ↩︎

  2. Perlin Noise wiki, https://en.wikipedia.org/wiki/Perlin_noise ↩︎

  3. Roger Eastman. (2019). CMSC425.01 Spring 2019 Lecture 20: Perlin noise I. University of Maryland. https://www.cs.umd.edu/class/spring2019/cmsc425/handouts/CMSC425Day20.pdf ↩︎

  4. Ken Perlin. (2022). Improved Noise reference implementation. https://mrl.cs.nyu.edu/~perlin/noise/ ↩︎

  5. Perlin, K. (2002, July). Improving noise. In Proceedings of the 29th annual conference on Computer graphics and interactive techniques (pp. 681-682). ↩︎ ↩︎