Perlin Noise

Prerequisites

Heightmaps Show

Noise Functions Show

Perlin Noise

Introduction

  • Perlin noise is an algorithm for producing multi-dimensional noise
  • Unlike the diamond-square algorithm, it does not suffer from severe axis-alignment

Interpolation

  • Let's with an array of two height values: \begin{align} h_1 &= 0.2\
  • We can plot these on a graph: (0, 0.2), (1, 0.5)
  • However if we want to get the height of a value between 0 and 1, for example: 0.5, then we have to interpolate.
  • A simple linear interpolation would involve drawing a straight line between the two points: y = x(h2 - h1) + h1 interpolation between the two points
  • However this isn't very smooth
    • Smoothness becomes very important when dealing with more than two values
  • Perlin noise uses a function called 'smoother step' to smooth the values
  • Smoother step is defined as: y = 6x^5 - 15x^4 + 10x^3 6x^5−15x^4+10x^3
  • If we combine this with our linear interpolation: y = \text{smoother_step}( smooth interpolation between -0.2 and 0.5

Gradients

For origins of 0
  • If a line has an origin of 0 and a gradient of 0.5 then the equation for this line would be: y = 0.5x
  • At the origin, the values of x and y would be: \begin{align} x &= 0 \\ y
  • But as we move further away from the origin: \begin{align} x &= 100 \\
  • The important note here is that the further away from the origin we are, the more of an effect the gradient has on the height
    • We are moving uphill from our origin
For origins of 1
  • If a line has an origin of 1 and a gradient of 0.5 then the equation for this line would be: y = 0.5(x - 1)
  • At the origin, the values of x and y would be: \begin{align} x &= 1 \\ y
  • But as we move further away from the origin: \begin{align} x &= 0 \\ y
  • Note that we end up with a negative number, as we are downhill from our origin
For origin x0 and gradient g
  • To make the equations more generic, we can the line equation as: y = g(x - x_0)
  • Where:
    • g is the gradient of the line
    • x0 is the origin

Gradient vectors

  • In the above interpolation examples we used height values
    • We defined the height that the line should be at the start and end
  • However, we could have used gradients instead
    • We can define the gradient that the line the should be at the start and end: \begin{align} g_1 &= 0.2 gradient vectors -0.2 and 0.5 at 0 and 1
  • For a value x between 0 and 1 we can calculate its height relative to each gradient: \begin{align} y_1 &= g_1( x = 0.5, \enspace g_1 = 0 \begin{align} y_1 &= 0.2( gradient vectors -0.2 and 0.5 at 0 and 1 with points at 0.5
  • Finally, we can interpolate between these two values to get our final value for y: y = \text{smoother_step}( gradient vectors with interpolation at 0.5
  • If we do this for all x values between 0 and 1, we get: 1d perlin noise
  • This is Perlin noise in 1 dimension

Extending to more than two points

  • Our curve is great if you have a start and end point
  • But we can also define a set of gradient vectors at the integers, and interpolate each segment individually: 1d perlin noise with multiple segments

Python Code

import numpy as np
import random

size = 5
gradients = [random.uniform(-1, 1) for t in range(size + 1)]


def smoother_step(x):
	return x * x * x * (x * (x * 6 - 15) + 10)


def interpolate(x):
	# the largest integer, that is smaller than x
	x1 = x - (x % 1)

	if x == x1:
		return 0

	# the smallest integer, that is larger than x
	x2 = x1 + 1

	# and their corresponding gradients
	g1 = gradients[int(x1)]
	g2 = gradients[int(x2)]

	# calculate the position vectors
	p1 = x - x1
	p2 = x - x2
	
	# calculate the heights
	y1 = g1 * p1
	y2 = g2 * p2

	# interpolate
	return smoother_step(x - x1) * (y2 - y1) + y1

# generate a range of values from 0 to 'size - 1' with a step of 0.01
interpolated_x = np.arange(0, size, 0.01)

interpolated_y = [interpolate(x) for x in interpolated_x]

Perlin noise in 2 dimensions

  • For 2d noise, we specify a gradient vector at each corner, each gradient vector has both an x and a y component: gradient lattice for 1 segment
  • The gradients vectors are labelled as:
    • g00 is the top-left gradient
    • g10 is the top-right gradient
    • g10 is the bottom-left gradient
    • g11 is the bottom-right gradient
    • each gradient has both an x and a y value:
      • e.g. g00x and g00y
    • We can't use y to represent height anymore, so instead we will use z

Interpolation

  • For 1 dimensional noise, our equation for the height was: y = g(x - x_0)
  • Where:
    • g is the gradient of the line
    • x0 is the origin
  • However, for two or more dimensions, we have to use a dot product of the vectors
    • Note that the dot product in one dimension is the same as multiplication
    • So we aren't doing anything new, we've just changed the size of the vectors we are multiplying
  • Our equations for the height values are: \begin{align} z_{00} &= g
  • This simplifies to: \begin{align} z_{00} &= g
  • Finally we interpolate
    • However interpolation only works for two values
    • So we will interpolate between z00 and z01
    • And then between z10 and z11
    • And finally between those two values \begin{align} z_a &= \tex
  • We can also extend it to more than a single square by defining a 2d grid of gradients, and interpolating each segment individually:

Image for a single 2d segment

gradient lattice for multiple segments

Image for a 5x5 heightmap

Python Code

import random
from PIL import Image
import numpy as np

size = 5
gradients = np.zeros((size + 1, size + 1, 2))

for i in range(size + 1):
	for j in range(size + 1):
		gradient = (random.uniform(-1, 1), random.uniform(-1, 1))
		gradient = gradient / np.linalg.norm(gradient)
		gradients[i][j] = gradient


def smoother_step(x):
	return x * x * x * (x * (x * 6 - 15) + 10)


def interpolate(x, y):
	# top left
	x1 = x - (x % 1)
	y1 = y - (y % 1)

	if x == x1 and y == y1:
		return 0

	# bottom right
	x2 = x1 + 1
	y2 = y1 + 1

	# and their corresponding gradient vectors
	g00 = gradients[int(x1), int(y1)]
	g10 = gradients[int(x2), int(y1)]
	g01 = gradients[int(x1), int(y2)]
	g11 = gradients[int(x2), int(y2)]

	# calculate the position vectors
	p00 = (x - x1, y - y1)
	p10 = (x - x2, y - y1)
	p01 = (x - x1, y - y2)
	p11 = (x - x2, y - y2)

	z00 = np.dot(p00, g00)
	z10 = np.dot(p10, g10)
	z01 = np.dot(p01, g01)
	z11 = np.dot(p11, g11)

	# interpolate
	za = smoother_step(x - x1) * (z10 - z00) + z00
	zb = smoother_step(x - x1) * (z11 - z01) + z01
	z = smoother_step(y - y1) * (zb - za) + za

	return z


img = Image.new('L', (size * 100, size * 100))

for i in range(size * 100):
	for j in range(size * 100):
		z = interpolate(i / 100, j / 100)
		img.putpixel((i, j), int(128 * z + 128))

img.show()

Choosing gradient vectors

  • In the above example we chose random values for our gradient vectors and stored them in a 2d array
  • Our only constraint was that the values should be in the -1, 1 range
  • This is fine for 1 dimensional noise
  • But for two or more dimensions, the gradient values should be clamped to either -1 or 1
Implementation:
  • A simple approach would be something like:
    gradient = -1 if random.uniform(0, 1) < 0.5 else 1
  • But Ken Perlin created a more elegant and efficient solution
  • Firstly:
    • Instead of using a 2d array of gradients, of size (n, n)
    • We can have an array that is fixed at size (256, 256)
    • If we need a value that is outside of this range then we take its value mod 256
      gradients = np.zeros((256, 256, 2))
      for i in range(256):
          for j in range(256):
              gradients[i, j] = (-1 if random.uniform(0, 1) < 0.5 else 1, -1 if random.uniform(0, 1) < 0.5 else 1)
      
      def get_gradient(x, y):
          return gradients[x % 256, y % 256]
  • But this still requires us to store two values at index
    • an x and a y
  • Instead of this, we can store a single random value and use it to generate both the x and y values:
  • Due to the components of the gradient needing to be either -1 or 1, there are only 4 possible gradient vectors:
    • (-1, -1)
    • (-1, 1)
    • (1, -1)
    • (1, 1)
  • Therefore we can check the random value mod 4, and return one of these vectors:
    gradients = np.zeros((256, 256))
    for i in range(256):
        for j in range(256):
            gradients[i, j] = random.randint(0, 256)
    
    
    def get_gradient(x, y):
        h = gradients[x % 256, y % 256] % 4
        if h == 0:
            return (-1, -1)
        elif h == 1:
            return (-1, 1)
        elif h == 2:
            return (1, -1)
        else:
            return (1, 1)
  • But this still requires an array of 256x256 numbers
  • Instead, we can use a single array of 256 numbers, and index the table twice:
    gradients = np.zeros((256))
    for i in range(256):
            gradients[i] = random.randint(0, 256)
    
    
    def get_gradient(x, y):
        h = gradients[int(gradients[x % 256] + y % 256)] % 4
        if h == 0:
            return (-1, -1)
        elif h == 1:
            return (-1, 1)
        elif h == 2:
            return (1, -1)
        else:
            return (1, 1)
  • Finally, we need to ensure that our array of gradients is uniform
  • So that each value between 0 and 255 appears exactly once
  • To do this, we fill it with the numbers in order, and then shuffle it
    gradients = np.arange(256, dtype=int)
    random.shuffle(gradients)
    
    def get_gradient(x, y):
        h = gradients[int(gradients[x % 256] + y % 256)] % 4
        if h == 0:
            return (-1, -1)
        elif h == 1:
            return (-1, 1)
        elif h == 2:
            return (1, -1)
        else:
            return (1, 1)

Adding multiple octaves

  • If we start with our 1 dimensional noise: single octave of 1d noise
  • We can describe its frequency as the number of line segemnts between each integer
  • In the above case, it has a frequency of 1, as we have one line segment between 0 and 1, one line segment between 1 and 2, etc
  • However if we double the frequency, we get: single octave with double frequency
  • To do this we produce noise twice as long, and scale it in the x-dimension
  • This results in a gradient vector not only at the integers, but also at 0.5, 1.5, etc
  • We can plot these two lines together: two octaves with different frequencies
  • A second property we can define is scale
  • In the above lines, the scale is 1, e.g. unscaled
  • However we can multiply the height of the line by a value
  • If we multiply our blue line (with frequency 2) by a scale of 0.5 then we get: two octaves with different frequencies and scales
  • Finally, if we add these two lines together, we get a 3rd line: two octaves combined together
  • When increasing the frequency of a line, it produces more detail
  • But it also creates larger swings in the gradient
  • By scaling the result, it keeps the detail, and reduces the swings in the gradient
  • Adding lines together with different frequencies and scales gives more detailed noise which is still smooth
  • Each individual line is called an 'octave'

Persistence

  • If we start with an octave with frequency=1 and scale=1
  • Then we can add more octaves
  • But how should the frequency and scale change for the additional octaves?
  • This is defined with Persistance
  • A persistance of 2 means:
    • The frequency should double for each new octave
    • The scale should half for each new octave
  • By varying the persistance, and the number of octaves, you can produce noise with different properties