Python Noise Generation Perlin — Deep Dive
The Perlin Noise Algorithm Step by Step
1. Permutation Table
Perlin noise uses a fixed permutation table — a shuffled array of integers 0-255, doubled to avoid index wrapping:
import numpy as np
def generate_permutation(seed=0):
rng = np.random.default_rng(seed)
p = np.arange(256, dtype=int)
rng.shuffle(p)
return np.tile(p, 2) # double to avoid modular indexing
perm = generate_permutation(seed=42)
2. Gradient Vectors
In 2D, gradients are unit vectors at 45° intervals:
GRADIENTS_2D = np.array([
(1, 1), (-1, 1), (1, -1), (-1, -1),
(1, 0), (-1, 0), (0, 1), (0, -1)
], dtype=float)
def gradient(hash_val, x, y):
g = GRADIENTS_2D[hash_val % 8]
return g[0] * x + g[1] * y # dot product
3. Fade Function
The fade curve ensures smooth interpolation at grid boundaries. Perlin’s improved version uses a quintic polynomial:
def fade(t):
# 6t^5 - 15t^4 + 10t^3
return t * t * t * (t * (t * 6 - 15) + 10)
4. Core Algorithm
def perlin_2d(x, y, perm):
# Grid cell coordinates
xi = int(np.floor(x)) & 255
yi = int(np.floor(y)) & 255
# Relative position within cell
xf = x - np.floor(x)
yf = y - np.floor(y)
# Fade curves
u = fade(xf)
v = fade(yf)
# Hash corners
aa = perm[perm[xi] + yi]
ab = perm[perm[xi] + yi + 1]
ba = perm[perm[xi + 1] + yi]
bb = perm[perm[xi + 1] + yi + 1]
# Gradient dot products
g_aa = gradient(aa, xf, yf)
g_ba = gradient(ba, xf - 1, yf)
g_ab = gradient(ab, xf, yf - 1)
g_bb = gradient(bb, xf - 1, yf - 1)
# Bilinear interpolation
x1 = g_aa + u * (g_ba - g_aa)
x2 = g_ab + u * (g_bb - g_ab)
return x1 + v * (x2 - x1)
Vectorized Implementation with NumPy
The scalar version above is educational but slow for generating large heightmaps. A vectorized version processes entire arrays at once:
def perlin_2d_vectorized(x_arr, y_arr, perm):
xi = np.floor(x_arr).astype(int) & 255
yi = np.floor(y_arr).astype(int) & 255
xf = x_arr - np.floor(x_arr)
yf = y_arr - np.floor(y_arr)
u = fade(xf)
v = fade(yf)
aa = perm[perm[xi] + yi]
ab = perm[perm[xi] + yi + 1]
ba = perm[perm[xi + 1] + yi]
bb = perm[perm[xi + 1] + yi + 1]
def grad_dot(h, dx, dy):
g = GRADIENTS_2D[h % 8]
return g[:, 0] * dx + g[:, 1] * dy if dx.ndim > 0 else g[0] * dx + g[1] * dy
g_aa = grad_dot(aa, xf, yf)
g_ba = grad_dot(ba, xf - 1, yf)
g_ab = grad_dot(ab, xf, yf - 1)
g_bb = grad_dot(bb, xf - 1, yf - 1)
x1 = g_aa + u * (g_ba - g_aa)
x2 = g_ab + u * (g_bb - g_ab)
return x1 + v * (x2 - x1)
# Generate a 512x512 heightmap
size = 512
scale = 50.0
x = np.linspace(0, size / scale, size)
y = np.linspace(0, size / scale, size)
xx, yy = np.meshgrid(x, y)
heightmap = perlin_2d_vectorized(xx.flatten(), yy.flatten(), perm).reshape(size, size)
Performance Comparison
| Method | 512×512 Heightmap | 2048×2048 Heightmap |
|---|---|---|
| Scalar Python (loop) | ~8 seconds | ~130 seconds |
| NumPy vectorized | ~0.05 seconds | ~0.8 seconds |
C-backed noise library | ~0.02 seconds | ~0.3 seconds |
| FastNoise Lite (SIMD) | ~0.005 seconds | ~0.06 seconds |
For production use, prefer C-backed libraries. Use the pure Python version for learning and customization.
Fractal Brownian Motion (fBm)
Layering octaves is formally called fractal Brownian motion:
def fbm(x, y, perm, octaves=6, lacunarity=2.0, persistence=0.5):
total = 0.0
amplitude = 1.0
frequency = 1.0
max_value = 0.0 # for normalization
for _ in range(octaves):
total += perlin_2d(x * frequency, y * frequency, perm) * amplitude
max_value += amplitude
amplitude *= persistence
frequency *= lacunarity
return total / max_value # normalize to [-1, 1]
Domain Warping
Domain warping feeds noise output back as input coordinates, creating organic, swirling patterns:
def domain_warp(x, y, perm, warp_strength=4.0):
# First noise layer warps the coordinates
qx = fbm(x, y, perm)
qy = fbm(x + 5.2, y + 1.3, perm)
# Second layer uses warped coordinates
return fbm(
x + warp_strength * qx,
y + warp_strength * qy,
perm
)
This technique produces terrain that looks weathered and eroded — far more realistic than plain fBm.
Multi-Level Domain Warping
def double_warp(x, y, perm):
# Level 1
q_x = fbm(x + 0.0, y + 0.0, perm)
q_y = fbm(x + 5.2, y + 1.3, perm)
# Level 2
r_x = fbm(x + 4.0 * q_x + 1.7, y + 4.0 * q_y + 9.2, perm)
r_y = fbm(x + 4.0 * q_x + 8.3, y + 4.0 * q_y + 2.8, perm)
return fbm(x + 4.0 * r_x, y + 4.0 * r_y, perm)
This creates intricate, painterly landscapes used in AAA game terrain.
Noise Types Beyond Perlin
Worley (Cellular) Noise
Distributes random feature points and colors each pixel by distance to the nearest point. Creates cell-like patterns — cracked earth, stone walls, biological tissue.
def worley_2d(x, y, seed=0, num_cells=16):
rng = np.random.default_rng(seed)
points = rng.random((num_cells * num_cells, 2)) * num_cells
min_dist = float('inf')
for px, py in points:
dist = (x - px) ** 2 + (y - py) ** 2
min_dist = min(min_dist, dist)
return np.sqrt(min_dist)
Value Noise
Assigns random values to grid points and interpolates (without gradients). Simpler than Perlin but produces more “blocky” artifacts.
Ridged Noise
Takes the absolute value of noise and inverts it, creating sharp ridges:
def ridged_fbm(x, y, perm, octaves=6):
total = 0.0
amplitude = 1.0
frequency = 1.0
for _ in range(octaves):
n = perlin_2d(x * frequency, y * frequency, perm)
n = 1.0 - abs(n) # invert absolute value → ridges
n = n * n # sharpen
total += n * amplitude
amplitude *= 0.5
frequency *= 2.0
return total
This produces mountain-ridge-like terrain and river-valley patterns.
Practical Applications
Terrain Generation with Biomes
def generate_world(size, seed):
perm = generate_permutation(seed)
elevation = np.zeros((size, size))
moisture = np.zeros((size, size))
for y in range(size):
for x in range(size):
nx, ny = x / size - 0.5, y / size - 0.5
elevation[y][x] = fbm(nx * 6, ny * 6, perm, octaves=8)
moisture[y][x] = fbm(nx * 4 + 100, ny * 4 + 100, perm, octaves=6)
# Assign biomes
biomes = np.empty((size, size), dtype='U20')
biomes[(elevation > 0.6)] = "mountain"
biomes[(elevation > 0.3) & (elevation <= 0.6) & (moisture > 0.3)] = "forest"
biomes[(elevation > 0.3) & (elevation <= 0.6) & (moisture <= 0.3)] = "grassland"
biomes[(elevation > 0.0) & (elevation <= 0.3)] = "beach"
biomes[(elevation <= 0.0)] = "ocean"
return elevation, moisture, biomes
Animated Effects
Use time as a third dimension for evolving patterns:
import time as time_mod
def animated_fire(x, y, t):
# Offset y by time to create upward flow
return fbm(x * 3, y * 3 - t * 2, perm, octaves=4, persistence=0.6)
Texture Synthesis
def marble_texture(x, y, perm):
n = fbm(x * 2, y * 2, perm, octaves=6)
return np.sin(x * 0.05 + n * 5) # sine wave distorted by noise
def wood_texture(x, y, perm):
n = fbm(x, y, perm, octaves=4)
grain = 20 * n
return grain - int(grain) # fractional part creates rings
3D Noise for Caves and Volumetric Effects
Extending to 3D allows carving cave systems:
def generate_cave(size_x, size_y, size_z, threshold=0.0):
cave = np.zeros((size_x, size_y, size_z), dtype=bool)
for x in range(size_x):
for y in range(size_y):
for z in range(size_z):
val = noise.pnoise3(x / 20, y / 20, z / 20, octaves=4)
cave[x][y][z] = val > threshold # True = solid
return cave
Voxels where noise is below the threshold become air, creating interconnected tunnel systems.
Performance Best Practices
- Use C-backed libraries for production —
noise,pyfastnoiselite, oropensimplexwith Cython. - Vectorize with NumPy — generate entire arrays instead of calling noise point-by-point.
- Cache generated chunks — store generated terrain to disk rather than regenerating.
- Generate on background threads — Python’s GIL releases during C library calls, enabling true parallelism.
- Use lower octaves for previews — 2 octaves renders 4× faster than 8 and is sufficient for mini-maps or LOD.
The one thing to remember: Perlin noise is a deterministic, smooth gradient function that, combined with octave layering and domain warping, produces endlessly varied natural patterns — and Python’s NumPy and C-backed libraries make it fast enough for real-time generation.
See Also
- Python Arcade Library Think of a magical art table that draws your game characters, listens when you press buttons, and cleans up the mess — that's Python Arcade.
- Python Audio Fingerprinting Ever wonder how Shazam identifies a song from just a few seconds of noisy audio? Audio fingerprinting is the magic behind it, and Python can do it too.
- Python Barcode Generation Picture the stripy labels on grocery items to understand how Python can create those machine-readable barcodes from numbers.
- Python Cellular Automata Imagine a checkerboard where each square follows simple rules to turn on or off — and suddenly complex patterns emerge like magic.
- Python Godot Gdscript Bridge Imagine speaking English to a friend who speaks French, with a translator in the middle — that's how Python talks to the Godot game engine.