Day 88: Perlin noise

Tomáš Bouda
100 days of algorithms
3 min readJun 20, 2017

--

It has been 35 years since Ken Perlin has discovered a technique today called Perlin noise to generate a fixed gradient noise to achieve a better looking textures in the famous movie Tron.

perlin noise: different frequencies

How Perlin noise works?

Look at the top left square at the picture. There are four corners, and each has defined a fixed 2D vector representing gradient, G¹, G², G³, G⁴. For any pixel P inside we define four vectors representing the difference between the P and each corner, V¹, V², V³, V⁴. Finally, we project gradients G onto vectors V and use bilinear interpolation to transform four numbers into one.

Each square on the picture contains 4-times more changes in gradient when compared to the previous square. As the number of changes increases, the output is more and more noisy.

When these 8 images with different frequencies are combined together using [for example] a weighted average, we get quite nice procedural textures.

perlin noise: weighted average & blue-white palette

Non-linear smoothing function and other tricks can be used to get more realistically looking textures. Fire, skies, fluids, landscapes, islands, mountains … Perlin noise and its successors like Simplex noise represent the very basic building block of procedurally generated textures.

https://github.com/coells/100days

https://notebooks.azure.com/coells/libraries/100days

algorithm

def generate_gradient(seed=None):
global gradient

seed and np.random.seed(seed)
gradient = np.random.rand(512, 512, 2) * 2 - 1
def perlin_noise(size_x, size_y, frequency):
global gradient

# linear space by frequency
x = np.tile(
np.linspace(0, frequency, size_x, endpoint=False),
size_y
)
y = np.repeat(
np.linspace(0, frequency, size_y, endpoint=False),
size_x
)
# gradient coordinates
x0 = x.astype(int)
y0 = y.astype(int)
# local coordinate
x -= x0
y -= y0
# gradient projections
g00 = gradient[x0, y0]
g10 = gradient[x0 + 1, y0]
g01 = gradient[x0, y0 + 1]
g11 = gradient[x0 + 1, y0 + 1]
# fade
t = (3 - 2 * x) * x * x
# linear interpolation
r = g00[:, 0] * x + g00[:, 1] * y
s = g10[:, 0] * (x - 1) + g10[:, 1] * y
g0 = r + t * (s - r)
# linear interpolation
r = g01[:, 0] * x + g01[:, 1] * (y - 1)
s = g11[:, 0] * (x - 1) + g11[:, 1] * (y - 1)
g1 = r + t * (s - r)
# fade
t = (3 - 2 * y) * y * y
# (bi)linear interpolation
g = g0 + t * (g1 - g0)
# reshape
return g.reshape(size_y, size_x)
def banded_perlin_noise(size_x, size_y, frequencies, amplitudes):
image = np.zeros((size_y, size_x))
for f, a in zip(frequencies, amplitudes):
image += perlin_noise(size_x, size_y, f) * a
image -= image.min()
image /= image.max()
return image

run

For full code including plots check the notebook.

>> generate_gradient(394)
>> perlin_noise(8, 8, 1)
array([[ 0. , -0.03, -0.1 , -0.18, -0.23, -0.23, -0.19, -0.11],
[-0. , -0.04, -0.12, -0.2 , -0.26, -0.27, -0.24, -0.16],
[-0.02, -0.07, -0.15, -0.23, -0.29, -0.31, -0.28, -0.22],
[-0.05, -0.1 , -0.19, -0.27, -0.32, -0.33, -0.31, -0.25],
[-0.06, -0.13, -0.21, -0.28, -0.33, -0.33, -0.31, -0.26],
[-0.07, -0.15, -0.23, -0.28, -0.31, -0.3 , -0.27, -0.23],
[-0.06, -0.15, -0.22, -0.26, -0.27, -0.25, -0.21, -0.16],
[-0.04, -0.13, -0.19, -0.22, -0.21, -0.18, -0.13, -0.08]])

--

--