Procedural Fractal Terrains with Python: How does Minecraft generate infinite maps? (hands-on)
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.
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.