Procedural Fractal Terrains with Python: How does Minecraft generate infinite maps? (hands-on)

miquel
6 min readDec 31, 2023

Full article: Procedural Fractal Terrains: How does Minecraft generate infinite maps? | by miquel | Dec, 2023 | Medium

Minecraft is able to generate infinite random and unique worlds. Perlin noise is at the core of it:

If we sample values from this Noise(x) function, we obtain a continuous function which bears some resemblance to mountains. That is, each point of the function is the height of the mountain at that position.

Fractal Brownian Motion adds many Perlin noises together, each with increasing frequency. We obtain something which resembles mountains much better.

pip install noise
pip install numpy
pip install matplotlib
import noise
import numpy as np
import matplotlib.pyplot as plt
from matplotlib import cm
from matplotlib.colors import ListedColormap, LinearSegmentedColormap


start, stop, step = 200, 300, 0.1

# Perlin parameters
frequency = 0.05
amplitude = 1

# fBM parameters
octaves = 4
lacunarity = 2
h = 0.1

xx = np.arange(start, stop, step)
yy = np.zeros(xx.size)
for i, x in enumerate(xx):
A, f = amplitude, frequency
for o in range(octaves):
A *= lacunarity**(-h*o)
yy[i] += A*noise.pnoise1(f*x)
f *= lacunarity

plt.figure(figsize=(9,3))
plt.plot(xx, yy, color='black')
plt.title(f'fBM (1D) [f={frequency}] [A={amplitude}] [O={octaves}] [L={lacunarity}] [H={h}]')
plt.xlabel('x'), plt.ylabel('fBM(x)')

Now, we can do the same but in 2D. We obtain the height of the mountain at each (x, y) position (white = high, black = low).

import noise
import numpy as np
import matplotlib.pyplot as plt
from matplotlib import cm
from matplotlib.colors import ListedColormap, LinearSegmentedColormap


start, stop, step = 200, 300, 0.1

# Perlin parameters
frequency = 0.05
amplitude = 1

# fBM parameters
octaves = 4
lacunarity = 2
h = 0.1

xx = np.arange(start, stop, step)
shape = (xx.size, xx.size)

yy = np.zeros(shape)
for i in range(shape[0]):
for j in range(shape[1]):
A, f = amplitude, frequency
for o in range(octaves):
A *= lacunarity**(-h*o)
yy[i][j] += A * noise.pnoise2(f*xx[i], f*xx[j])
f *= lacunarity

plt.figure(figsize=(8,7))
plt.pcolor(xx, xx, yy, cmap='gray')
plt.title(f'fBM (2D) [f={frequency}] [A={amplitude}] [O={octaves}] [L={lacunarity}] [H={h}]')
plt.xlabel('$x_1$'), plt.ylabel('$x_2$')
plt.colorbar()

Then, we can assign a color to each height to create a simple texture.

Finally, we can use Blender to create a mesh with the heights and colors we generated above.

import noise
import numpy as np
import matplotlib.pyplot as plt
from matplotlib import cm
from matplotlib.colors import ListedColormap, LinearSegmentedColormap


def fbm(start, stop, step, # general params
frequency, amplitude, # perlin params
octaves, lacunarity, h
): # fBM params
xx = np.arange(start, stop, step)
shape = (xx.size, xx.size)

yy = np.zeros(shape)
for i in range(shape[0]):
for j in range(shape[1]):
A, f = amplitude, frequency
for o in range(octaves):
A *= lacunarity**(-h*o)
yy[i][j] += A * noise.pnoise2(f*xx[i], f*xx[j])
f *= lacunarity

return yy


def normalize(x):
x += -1 * x.min()
x /= x.max()
return x


############## generate heightmap ##############

start=1200
stop=1300
step=0.1
frequency=0.05
amplitude=1
octaves=4
lacunarity=2
h=0.99

heightmap = normalize(fbm(
start=start,
stop=stop,
step=step,
frequency=frequency,
amplitude=amplitude,
octaves=octaves,
lacunarity=lacunarity,
h=h))

plt.figure()
plt.imshow(heightmap, cmap='gray')
plt.axis('off')
plt.savefig(f'heighMap_fBM_f{frequency}_A{amplitude}_O{octaves}_L{lacunarity}_H{h}.png',bbox_inches='tight', pad_inches=0)

########### generate colormap noise #############
# a perfect correspondence between heights
# and colors (snow, rock, ...) is not realistic
# we generate some noise to add a bit of spice and variation

cmap_noise = normalize(fbm(
start=200,
stop=300,
step=0.1,
frequency=0.02,
amplitude=1,
octaves=8,
lacunarity=2,
h=0.99))

plt.figure()
plt.imshow(cmap_noise, cmap='gray')
plt.axis('off')

############### generate colormap ###############
coast = 50
pastures = 70
mountains = 120
top_mountains = 150
snow = 200

original = cm.get_cmap('gray', 256)
cmap = original(np.linspace(-1, 1, 256))
cmap[:coast, :] = np.array([37/256, 159/256, 175/256, 1])
cmap[coast:pastures, :] = np.array([246/256, 215/256, 176/256, 1])
cmap[pastures:mountains, :] = np.array([110/256, 204/256, 126/256, 1])
cmap[mountains:top_mountains, :] = np.array([74/256, 71/256, 63/256, 1])
cmap[top_mountains:snow, :] = np.array([79/256, 78/256, 77/256, 1])
cmap[snow:, :] = np.array([235/256, 231/256, 218/256, 1])

plt.figure()
plt.imshow(normalize(heightmap + cmap_noise / 5), cmap=ListedColormap(cmap))
plt.axis('off')
plt.savefig(f'colorMap_fBM_f{frequency}_A{amplitude}_O{octaves}_L{lacunarity}_H{h}.png',bbox_inches='tight', pad_inches=0)

While the result is very convincing, we can improve it with Multi Fractal Brownian Motion. Concretely, with the Statistics By Altitude model. This is a heterogenous model that forces low altitudes to be flatter than higher altitudes to achieve more realistic results.

fBM (homogeneous)
multi-fBM (homogeneous)
import noise
import numpy as np
import matplotlib.pyplot as plt
from matplotlib import cm
from matplotlib.colors import ListedColormap, LinearSegmentedColormap


start, stop, step = 200, 300, 0.1

# Perlin parameters
frequency = 0.05
amplitude = 1

# fBM parameters
octaves = 4
lacunarity = 2
h = 0.1

# multi-fBM (Statistics By Altitude) parameters
offset = 0.8

xx = np.arange(start, stop, step)
yy = np.zeros(xx.size)
for i, x in enumerate(xx):
f = frequency
for o in range(octaves):
if o == 0:
gain = 1
else:
gain = f**(-h*o)
gain *= yy[i]
yy[i] += gain * (noise.pnoise1(f*x) + offset)
f *= lacunarity

plt.figure(figsize=(9,3))
plt.plot(xx, yy, color='black')
plt.title(f'multi-fBM (1D) [f={frequency}] [A={amplitude}] [O={octaves}] [L={lacunarity}] [H={h}] [P={offset}]')
plt.xlabel('x'), plt.ylabel('fBM(x)')

If we repeat the process above, assigning each color to a height, we obtain a much richer and better looking terrain.

import noise
import numpy as np
import matplotlib.pyplot as plt
from matplotlib import cm
from matplotlib.colors import ListedColormap, LinearSegmentedColormap


def fbm_altitude(start, stop, step, # general params
frequency, amplitude, # perlin params
octaves, lacunarity, h, # fBM params
offset
): # statistics by altitutde params
xx = np.arange(start, stop, step)
shape = (xx.size, xx.size)

yy = np.zeros(shape)
for i in range(shape[0]):
for j in range(shape[1]):
f = frequency
for o in range(octaves):
if o == 0:
gain = 1
else:
gain = f**(-h*o)
gain *= yy[i][j]
yy[i][j] += gain * (noise.pnoise2(f*xx[i], f*xx[j]) + offset)
f *= lacunarity
return yy

def normalize(x):
x += -1 * x.min()
x /= x.max()
return x


############## generate heightmap ##############
start=200
stop=400
step=0.1
frequency=0.005
amplitude=1
octaves=4
lacunarity=2
h=0.99
offset=0.3

heightmap = normalize(fbm_altitude(
start=start,
stop=stop,
step=step,
frequency=frequency,
amplitude=amplitude,
octaves=octaves,
lacunarity=lacunarity,
h=h,
offset=offset))

plt.figure()
plt.imshow(heightmap, cmap='gray')
plt.axis('off')
plt.savefig(f'heighMap_multi-fBM_f{frequency}_A{amplitude}_O{octaves}_L{lacunarity}_H{h}_offset{offset}.png',bbox_inches='tight', pad_inches=0)

########### generate colormap noise #############
# a perfect correspondence between heights
# and colors (snow, rock, ...) is not realistic
# we generate some noise to add a bit of spice and variation

cmap_noise = normalize(fbm(
start=200,
stop=400,
step=0.1,
frequency=0.02,
amplitude=1,
octaves=8,
lacunarity=2,
h=0.99))

plt.figure()
plt.imshow(cmap_noise, cmap='gray')
plt.axis('off')

############### generate colormap ###############
coast = 50
pastures = 80
mountains = 120
top_mountains = 150
snow = 200

original = cm.get_cmap('gray', 256)
cmap = original(np.linspace(-1, 1, 256))
cmap[:coast, :] = np.array([37/256, 159/256, 175/256, 1])
cmap[coast:pastures, :] = np.array([246/256, 215/256, 176/256, 1])
cmap[pastures:mountains, :] = np.array([110/256, 204/256, 126/256, 1])
cmap[mountains:top_mountains, :] = np.array([74/256, 71/256, 63/256, 1])
cmap[top_mountains:snow, :] = np.array([79/256, 78/256, 77/256, 1])
cmap[snow:, :] = np.array([235/256, 231/256, 218/256, 1])

plt.figure()
plt.imshow(normalize(heightmap + cmap_noise / 5), cmap=ListedColormap(cmap))
plt.axis('off')
plt.savefig(f'colorMap_multi-fBM_f{frequency}_A{amplitude}_O{octaves}_L{lacunarity}_H{h}_offset{offset}.png',bbox_inches='tight', pad_inches=0)

You can find all the code in this Python Notebook. This tutorial explains how to import a heighmap and colormap into Blender to generate the 3D renders.

Sign up to discover human stories that deepen your understanding of the world.

Free

Distraction-free reading. No ads.

Organize your knowledge with lists and highlights.

Tell your story. Find your audience.

Membership

Read member-only stories

Support writers you read most

Earn money for your writing

Listen to audio narrations

Read offline with the Medium app

No responses yet

Write a response