0x- Playing with Perlin Noise

I haven’t seen a ton of great examples of making maps with perlin noise in python. So here we go!

The dancing crocodile in my garden thanks you for sharing.

Share Yvan

Perlin noise is a mathematical formula used to generate ‘realistic’ structures. It’s noise but unlike regular noise it has some coherent structure. You can think of perlin noise as multiple samples of a noisy field sampled at different resolutions and layered on top of each other. Here is regular noise vs. Perlin noise:

regular static and perlin noise

In the python noise module there are a few parameters that affect what you see when you generate your perlin noise:

  1. scale: number that determines at what distance to view the noisemap.

  2. octaves: the number of levels of detail you want you perlin noise to have.

  3. lacunarity: number that determines how much detail is added or removed at each octave (adjusts frequency).

  4. persistence: number that determines how much each octave contributes to the overall shape (adjusts amplitude).

We won’t worry about scale too much, you can use it to zoom out (bigger scale) or in (smaller scale).

Perlin noise combines multiple functions called ‘octaves’ to produce natural looking surfaces. Each octave adds a layer of detail to the surface. For example: octave 1 could be mountains, octave 2 could be boulders, octave 3 could be the rocks.

Lacunarity of more than 1 means that each octave will increase its level of fine grained detail (increased frqeuency). Lacunarity of 1 means that each octave will have the sam level of detail. Lacunarity of less than one means that each octave will get smoother. The last two are usually undesirable so a lacunarity of 2 works quite well.

Persistence determines how much each octave contributes to the overall structure of the noise map. If your persistence is 1 all octaves contribute equally. If you persistence is more than 1 successive octaves contribute more and you get something closer to regular noise (the regular noise image above is actually a perlin noise with a persistence of 5.0). A more default setting would be a persistence of less than 1.0 which will decrease the effect of later octaves.

Enough chatting though! Let’s run some experiments. First let’s start with default perlin noise, and its accompanying image:

import noise
import numpy as np
from scipy.misc import toimage
shape = (1024,1024)
scale = 100.0
octaves = 6
persistence = 0.5
lacunarity = 2.0
world = np.zeros(shape)
for i in range(shape[0]):
for j in range(shape[1]):
world[i][j] = noise.pnoise2(i/scale,
j/scale,
octaves=octaves,
persistence=persistence,
lacunarity=lacunarity,
repeatx=1024,
repeaty=1024,
base=0)
toimage(world).show()
view raw default_noise.py hosted with ❤ by GitHub

The way this perlin noise looks in our script is a 2D array of values between -1 and 1. The values that are darker on the map are lower values, the values that are close to 1 are lighter. What I want to try next is assigning two colors to different ranges of values in this map to produce some terrain:

blue = [65,105,225]
green = [34,139,34]
beach = [238, 214, 175]
def add_color(world):
color_world = np.zeros(world.shape+(3,))
for i in range(shape[0]):
for j in range(shape[1]):
if world[i][j] < -0.05:
color_world[i][j] = blue
elif world[i][j] < 0:
color_world[i][j] = beach
elif world[i][j] < 1.0:
color_world[i][j] = green
return color_world
color_world = add_color(world)
toimage(color_world).show()
view raw first_terrain.py hosted with ❤ by GitHub

This terrain map is pretty neat; it has jagged coasts, beaches, and lots of water. while I have never observed natural terrain that looks like this if we look at any one part of the map it seems ‘realistic.’ Let’s take it a step further and add mountains and snow:

blue = [65,105,225]
green = [34,139,34]
beach = [238, 214, 175]
snow = [255, 250, 250]
mountain = [139, 137, 137]
def add_color(world):
color_world = np.zeros(world.shape+(3,))
for i in range(shape[0]):
for j in range(shape[1]):
if world[i][j] < -0.05:
color_world[i][j] = blue
elif world[i][j] < 0:
color_world[i][j] = beach
elif world[i][j] < 1.0:
color_world[i][j] = green
elif world[i][j] < 0.35:
color_world[i][j] = mountain
elif world[i][j] < 1.0:
color_world[i][j] = snow
return color_world
color_world = add_color(world)
toimage(color_world).show()
view raw second_terrain.py hosted with ❤ by GitHub

This is cool but this terrain pattern is clearly not natural. To make it more natural we will use a circular filter to get rid of all the preipheral perlin noise:

a,b = shape[0]/2, shape[1]/2
n = 1024
r = 125
y,x = np.ogrid[-a:n-a, -b:n-b]
# creates a mask with True False values
# at indices
mask = x**2+y**2 <= r**2
black = [0, 0, 0]
island_world = np.zeros_like(color_world)
for i in range(shape[0]):
for j in range(shape[1]):
if mask[i][j]:
island_world[i][j] = color_world[i][j]
else:
island_world[i][j] = black
toimage(island_world).show()
view raw planet_terrain.py hosted with ❤ by GitHub
We’ll call it Perlin’s World!

Here I was trying to create an island so I made a circular filter and then applied it to the color_world perlin noise array. What I ended up was a planet floating in an ocean. I changed the ocean color to black and it looks pretty cool! That said what I wanted was an island so let’s try again. This time we’re going to calculate a circular gradient and then apply that over the perlin noise as a filter.

Circular Gradient:

import math
center_x, center_y = shape[1] // 2, shape[0] // 2
circle_grad = np.zeros_like(world)
for y in range(world.shape[0]):
for x in range(world.shape[1]):
distx = abs(x - center_x)
disty = abs(y - center_y)
dist = math.sqrt(distx*distx + disty*disty)
circle_grad[y][x] = dist
# get it between -1 and 1
max_grad = np.max(circle_grad)
circle_grad = circle_grad / max_grad
circle_grad -= 0.5
circle_grad *= 2.0
circle_grad = -circle_grad
# shrink gradient
for y in range(world.shape[0]):
for x in range(world.shape[1]):
if circle_grad[y][x] > 0:
circle_grad[y][x] *= 20
# get it between 0 and 1
max_grad = np.max(circle_grad)
circle_grad = circle_grad / max_grad
toimage(circle_grad).show()
view raw circular_gradient.py hosted with ❤ by GitHub

I struggled a lot with this part. I’m sure there is a more efficient way to get the gradient like this but the above was what I came up with. I calculated a distance metric from the center of the map and then normalized, shrunk, and renomalized those distances to produce this spherical gradient. Again lighter means the value is closer to 1, darker colors are closer to 0. Next I apply this circular gradient to the perlin noise we created before.

Circular Gradient + Noise:

world_noise = np.zeros_like(world)
for i in range(shape[0]):
for j in range(shape[1]):
world_noise[i][j] = (world[i][j] * circle_grad[i][j])
if world_noise[i][j] > 0:
world_noise[i][j] *= 20
# get it between 0 and 1
max_grad = np.max(world_noise)
world_noise = world_noise / max_grad
toimage(world_noise).show()

This part was less tricky but still a pain. I multiply the perlin noise by the circle gradient and then I increase the contrast by multiplying positive (lighter values) by 20. Then I renormalize to make it 0–1 again.

Colorized Circular Gradient + Noise:

lightblue = [0,191,255]
blue = [65,105,225]
green = [34,139,34]
darkgreen = [0,100,0]
sandy = [210,180,140]
beach = [238, 214, 175]
snow = [255, 250, 250]
mountain = [139, 137, 137]
threshold = 0
def add_color2(world):
color_world = np.zeros(world.shape+(3,))
for i in range(shape[0]):
for j in range(shape[1]):
if world[i][j] < threshold + 0.05:
color_world[i][j] = blue
elif world[i][j] < threshold + 0.055:
color_world[i][j] = sandy
elif world[i][j] < threshold + 0.1:
color_world[i][j] = beach
elif world[i][j] < threshold + 0.25:
color_world[i][j] = green
elif world[i][j] < threshold + 0.6:
color_world[i][j] = darkgreen
elif world[i][j] < threshold + 0.7:
color_world[i][j] = mountain
elif world[i][j] < threshold + 1.0:
color_world[i][j] = snow
return color_world
island_world_grad = add_color2(world_noise)
toimage(island_world_grad).show()

This is really cool and it looks like a much more natural archipelago. I encourage you to try different shading methods and maybe randomly removing some sections. I’m going to change the threshold value and set it as threshold = 0.2. That will produce a smaller but more realistic archipelago as so:

There we are! We have a natural looking island archipelago! So now that we have our islands you may notice that no matter how often you rerun this script perlin noise will produce the same islands. To get new islands you can set the base parameter of the pnoise2 function to a random integer number, let’s try base=5, base=100:

Archipelagos generated from base 5 and base 100.

Conclusion

So we started with some simple noise and ended up with a way to generate a realtively unlimited number of unique and natural looking archipelagos! I hope you enjoyed this post!

If you liked this share it with a like minded friend!

Share