Procedural Terrain Generation using Perlin Noise
- Published on

Procedural terrain generation allows developers to create vast, randomized terrains programmatically. In this article, we'll explore how to generate realistic terrains in Python using Perlin Noise. Let's embark on this exciting journey into the world of procedural generation!
Introduction
Procedural generation is a method of creating data algorithmically rather than manually. It's widely used in game development to create expansive and varied environments without the need for handcrafted assets. Perlin Noise, developed by Ken Perlin in 1983, is a type of gradient noise often used in procedural generation to create natural-looking textures and terrains.
Prerequisites
- Python 3.x installed on your system.
- NumPy library for numerical operations:
pip install numpy
- Matplotlib for plotting the terrain:
pip install matplotlib
- Basic understanding of Python programming.
What Is Perlin Noise?
Perlin Noise is a gradient noise function used to generate smooth, natural patterns. It's commonly used to simulate textures like wood grain, marble, clouds, and terrains. The noise function produces a pseudo-random sequence of values that are smoothly interpolated, resulting in a coherent and natural appearance.
Implementing Perlin Noise in Python
We'll start by implementing a simple version of Perlin Noise and then use it to generate a terrain height map.
1. Importing Necessary Libraries
import numpy as np
import matplotlib.pyplot as plt
2. Defining the Perlin Noise Function
First, we'll create functions to generate Perlin Noise.
def lerp(a, b, x):
"""Linear interpolation between a and b with x."""
return a + x * (b - a)
def fade(t):
"""Fade function as defined by Ken Perlin. This eases coordinate values
so that they will ease towards integral values. This ends up smoothing
the final output."""
return 6 * t**5 - 15 * t**4 + 10 * t**3
def gradient(h, x, y):
"""Calculates the dot product of a pseudorandom gradient vector and the
vector from the input coordinate to the grid node."""
vectors = np.array([[0,1], [0,-1], [1,0], [-1,0]])
g = vectors[h % 4]
return g[:, :, 0] * x + g[:, :, 1] * y
def perlin(x, y, seed=0):
"""Generate Perlin noise based on the input x and y coordinates."""
np.random.seed(seed)
# Determine grid cell coordinates
x0 = x.astype(int)
x1 = x0 + 1
y0 = y.astype(int)
y1 = y0 + 1
# Interpolation weights
sx = fade(x - x0)
sy = fade(y - y0)
# Random hash
h00 = np.random.randint(0, 4, size=(x.shape[0], x.shape[1]))
h01 = np.random.randint(0, 4, size=(x.shape[0], x.shape[1]))
h10 = np.random.randint(0, 4, size=(x.shape[0], x.shape[1]))
h11 = np.random.randint(0, 4, size=(x.shape[0], x.shape[1]))
# Gradients
n00 = gradient(h00, x - x0, y - y0)
n10 = gradient(h10, x - x1, y - y0)
n01 = gradient(h01, x - x0, y - y1)
n11 = gradient(h11, x - x1, y - y1)
# Interpolate
ix0 = lerp(n00, n10, sx)
ix1 = lerp(n01, n11, sx)
value = lerp(ix0, ix1, sy)
return value
3. Generating the Terrain Height Map
We'll create a grid of coordinates and compute the Perlin Noise values.
def generate_perlin_noise_2d(shape, res, seed=0):
"""Generate a 2D numpy array of perlin noise."""
def f(t):
return 6 * t**5 - 15 * t**4 + 10 * t**3
delta = (res[0] / shape[0], res[1] / shape[1])
d = (shape[0] // res[0], shape[1] // res[1])
grid = np.mgrid[0:res[0]:complex(0, shape[0]), 0:res[1]:complex(0, shape[1])].transpose(1, 2, 0) % 1
# Gradients
np.random.seed(seed)
gradients = np.random.randn(res[0]+1, res[1]+1, 2)
gradients /= np.linalg.norm(gradients, axis=2, keepdims=True)
# Ramps
g00 = gradients[0:-1, 0:-1].repeat(d[0], 0).repeat(d[1], 1)
g10 = gradients[1:, 0:-1].repeat(d[0], 0).repeat(d[1], 1)
g01 = gradients[0:-1, 1:].repeat(d[0], 0).repeat(d[1], 1)
g11 = gradients[1:, 1:].repeat(d[0], 0).repeat(d[1], 1)
n00 = np.sum(grid * g00, 2)
n10 = np.sum(np.stack([grid[:,:,0]-1, grid[:,:,1]], axis=2) * g10, 2)
n01 = np.sum(np.stack([grid[:,:,0], grid[:,:,1]-1], axis=2) * g01, 2)
n11 = np.sum(np.stack([grid[:,:,0]-1, grid[:,:,1]-1], axis=2) * g11, 2)
t = f(grid)
n0 = n00*(1-t[:,:,0]) + t[:,:,0]*n10
n1 = n01*(1-t[:,:,0]) + t[:,:,0]*n11
return np.sqrt(2)*((1 - t[:,:,1])*n0 + t[:,:,1]*n1)
4. Visualizing the Terrain
Now, let's generate the noise and visualize it using Matplotlib.
shape = (500, 500)
res = (8, 8)
noise = generate_perlin_noise_2d(shape, res, seed=1)
plt.figure(figsize=(8, 8))
plt.imshow(noise, cmap='terrain')
plt.colorbar()
plt.title('Procedurally Generated Terrain')
plt.show()
Explanation:
shape
: The dimensions of the generated terrain.res
: The number of periods of noise to generate along each axis.seed
: A seed value for the random number generator to produce repeatable results.cmap='terrain'
: A colormap that gives the image a terrain-like appearance.
5. Enhancing Realism with Octaves
To create more realistic terrains, we can sum multiple layers of Perlin Noise at different frequencies and amplitudes, known as octaves.
def generate_fractal_noise_2d(shape, res, octaves=1, persistence=0.5, seed=0):
noise = np.zeros(shape)
frequency = 1
amplitude = 1
max_amplitude = 0
for _ in range(octaves):
noise += amplitude * generate_perlin_noise_2d(shape, (frequency * res[0], frequency * res[1]), seed=seed)
max_amplitude += amplitude
amplitude *= persistence
frequency *= 2
return noise / max_amplitude
Let's generate terrain with multiple octaves:
octaves = 6
persistence = 0.5
noise = generate_fractal_noise_2d(shape, res, octaves, persistence, seed=2)
plt.figure(figsize=(8, 8))
plt.imshow(noise, cmap='terrain')
plt.colorbar()
plt.title('Terrain with Multiple Octaves')
plt.show()
Explanation:
octaves
: The number of noise layers.persistence
: Controls the amplitude of each octave. Lower values result in smoother terrains.
Full Source Code
Combining all the parts, here's the complete code:
import numpy as np
import matplotlib.pyplot as plt
def generate_perlin_noise_2d(shape, res, seed=0):
def f(t):
return 6 * t**5 - 15 * t**4 + 10 * t**3
delta = (res[0] / shape[0], res[1] / shape[1])
d = (shape[0] // res[0], shape[1] // res[1])
grid = np.mgrid[0:res[0]:complex(0, shape[0]), 0:res[1]:complex(0, shape[1])].transpose(1, 2, 0) % 1
# Gradients
np.random.seed(seed)
gradients = np.random.randn(res[0]+1, res[1]+1, 2)
gradients /= np.linalg.norm(gradients, axis=2, keepdims=True)
# Ramps
g00 = gradients[0:-1, 0:-1].repeat(d[0], 0).repeat(d[1], 1)
g10 = gradients[1:, 0:-1].repeat(d[0], 0).repeat(d[1], 1)
g01 = gradients[0:-1, 1:].repeat(d[0], 0).repeat(d[1], 1)
g11 = gradients[1:, 1:].repeat(d[0], 0).repeat(d[1], 1)
n00 = np.sum(grid * g00, 2)
n10 = np.sum(np.stack([grid[:,:,0]-1, grid[:,:,1]], axis=2) * g10, 2)
n01 = np.sum(np.stack([grid[:,:,0], grid[:,:,1]-1], axis=2) * g01, 2)
n11 = np.sum(np.stack([grid[:,:,0]-1, grid[:,:,1]-1], axis=2) * g11, 2)
t = f(grid)
n0 = n00*(1 - t[:,:,0]) + t[:,:,0]*n10
n1 = n01*(1 - t[:,:,0]) + t[:,:,0]*n11
return np.sqrt(2)*((1 - t[:,:,1])*n0 + t[:,:,1]*n1)
def generate_fractal_noise_2d(shape, res, octaves=1, persistence=0.5, seed=0):
noise = np.zeros(shape)
frequency = 1
amplitude = 1
max_amplitude = 0
for _ in range(octaves):
noise += amplitude * generate_perlin_noise_2d(
shape, (frequency * res[0], frequency * res[1]), seed=seed)
max_amplitude += amplitude
amplitude *= persistence
frequency *= 2
return noise / max_amplitude
# Parameters
shape = (500, 500)
res = (4, 4)
octaves = 6
persistence = 0.5
# Generate noise
noise = generate_fractal_noise_2d(shape, res, octaves, persistence, seed=2)
# Plotting
plt.figure(figsize=(8, 8))
plt.imshow(noise, cmap='terrain')
plt.colorbar()
plt.title('Procedurally Generated Terrain')
plt.show()
Customizing the Terrain
You can adjust the terrain's appearance by tweaking parameters:
- Resolution (
res
): Lower values create larger features; higher values create finer details. - Octaves: Increasing octaves adds more layers of detail.
- Persistence: Controls how quickly the amplitudes decrease for higher octaves.
Adding Features: Rivers and Lakes
To simulate water bodies, we can set height thresholds:
water_level = -0.1
terrain = noise.copy()
terrain[terrain < water_level] = water_level
plt.figure(figsize=(8, 8))
plt.imshow(terrain, cmap='terrain')
plt.colorbar()
plt.title('Terrain with Water Bodies')
plt.show()
Conclusion
Procedural terrain generation using Perlin Noise opens up endless possibilities for creating dynamic and natural-looking environments. By adjusting a few parameters, you can generate a variety of terrains for games, simulations, or visualizations.
Next Steps
- 3D Terrain Visualization: Use libraries like
matplotlib
'splot_surface
ormayavi
to visualize the terrain in 3D. - Texture Mapping: Apply textures to different height ranges to simulate grass, rocks, or snow.
- Terrain Export: Save the generated terrain data for use in game engines or other applications.
- Erosion Simulation: Implement algorithms to simulate natural erosion for more realistic terrains.
Conclusion
Procedural generation is a powerful tool in a developer's arsenal, enabling the creation of vast and varied environments with minimal manual effort. Python's simplicity and the availability of powerful libraries make it an excellent choice for experimenting with these concepts. So go ahead, tweak the code, and create your own unique worlds!