DEV Community

Cover image for Create useful noise patterns with Academy award winning Ken Perlin 🏆🎲
Pascal Thormeier
Pascal Thormeier

Posted on

Create useful noise patterns with Academy award winning Ken Perlin 🏆🎲

Randomness is weird. Apart from the fact that true randomness on computers can only be achieved with either really complex devices or fish tanks, it just doesn't look appealing, either.

Consider this piece of code:

<canvas id="c" width="640" height="360" />

<script>
const canvas = document.querySelector('#c')

const paintGreyPixel = (canvas, x, y, grayScale) => {
  const context = canvas.getContext('2d')
  context.fillStyle = `rgba(0, 0, 0, ${grayScale})`
  context.fillRect(x, y, 1, 1)
}

for (let x = 0; x < 640; x++) {
  for (let y = 0; y < 360; y++) {
    paintGreyPixel(canvas, x, y, Math.random())
  }
}
</script>
Enter fullscreen mode Exit fullscreen mode

What it does, is to paint every single pixel in a random grey scale. Math.random() combined with rgba and a black color results in a random shade of grey. And that's the result:

Random noise

Meh.

Now imagine you want to do something with a randomly generated pattern like this. For example, using it as a height map for a game you're developing, or a simulate a wave pattern. Pretty useless for that, isn't it? You would want something that generates gradients instead.

Luckily, there are random noise algorithms that generate something useful. Ken Perlin has developed an algorithm that is aptly termed the Perlin noise generation. Perlin has actually received an Academy award for that. Yes, the movie trophy. The algorithm was originally developed for the movie Tron.

It generates something like this:

Smooth random grey scale noise

Much better, isn't it? And that noise could actually be used for height maps, water simulation and whatnot. Now, how do we generate that noise, though?

How the algorithm works

Let's have a deeper look at how the thing works that got Ken Perlin their golden statue. The Perlin noise algorithm works by dividing the plane into a grid of squares. The above example is 640 wide and 360 tall and was divided into a 60 by 60 grid. So, we have roughly 10.7 grid cells of width and 6 grid cells of height.

A rectangle that's divided into cells

We then assign a vector to each grid intersection point. These vectors are usually chosen by random and out of a set of four, pointing to top-left, top-right, bottom-left and bottom-right. For smoother results, we can also generate these entirely random. We only need to make sure their length is uniform

These gradient vectors never change. Whenever we color a pixel, we use the four vectors of that grid cell to calculate the color. This keeps the number of random numbers to a minimum and uses what computers are actually good at: Calculations.

A grid with vectors assigned.

Now, to figure out what color a single pixel has, we first determine its grid cell and then the four gradient vectors of that grid cell, as well as the pixel's relative position within that grid cell as numbers between 0 and 1. We then calculate the vectors pointing from the edges of that grid cell to the pixel.

A grid cell with a marked pixel and vectors pointing to it

We now have 8 vectors, two per grid cell corner. We now calculate the dot products of the two vectors per grid corner to determine a single number per grid corner.

The dot product of the gradient vectors and the vectors pointing the edges, gives us four numbers. Essentially, the dot product tells us "what amount of one vector goes in the direction of another".

We can then "fade" the relative coordinates of the pixel within the grid cell to get smooth values. Perlin has defined a function for that, which they found yielded the best reults.

fade(t)=6t515t4+10t3 fade(t) = 6t^5 - 15t^4 + 10t^3

We now have a total of 6 numbers, 4 dot products and 2 faded coordinates.

All numbers that result in a grid cell

With these numbers and the faded coordinates, we can then do what's called "lerping". We can think of the faded values between 0 and 1 as percentages. So, if the faded X coordinate is 0.25, that's 25%. If we, for example, want to lerp the numbers 12 and 18 with 25%, that's 25% of the way from 12 to 18. In maths, that would be:

lerp(max,min,t)=(maxmin)t+min lerp(max, min, t) = (max - min) * t + min

We do that three times:

  • Lerp the top two dot products (top left and top right) with the X coordinate
  • Lerp the bottom two dot products (bottom left and bottom right) with the X coordinate
  • Lerp the result of the two previous lerps with the Y coordinate

If done correctly, this will yield a single number between 0 and 1 - the noise value of that one pixel. Rinse and repeat.

The lerping process visualized

That was a lot, right? Perlin got that award for a reason.

Off to the implementation!

Let's get coding. We start with the lerp and the fade function. We'll need these eventually and they are the mathematically more complex functions:

const fade = t => 6 * (t ** 5) - 15 * (t **4) + 10 * (t ** 3)

const lerp = (v0, v1, t) => {
  const max = Math.max(v0, v1)
  const min = Math.min(v0, v1)

  const before = `${v0}/${v1}`
  const after = `${min}/${max}`

  // The values don't always come in
  // v0 < v1, so we might need to invert t
  // if v0 > v1, to keep the formula 
  // satisfied.
  if (before !== after) {
    t = 1 - t
  }

  return (max - min) * t + min
}
Enter fullscreen mode Exit fullscreen mode

Notice how the lerp function takes into account that v0 is not always the smaller value. This might very well happen, depending on the graident vectors and the position of the pixel. The fade function, on the other hand, is relatively straight forward.

Next, we introduce a Vector class. We use a class, so we don't have to juggle X and Y coordinates as arrays all the time and can have the rest of our code be more descriptive.

In that Vector class, we already implement the dot product and any other operation we might need.

class Vector {
  constructor(x, y) {
    this.x = x
    this.y = y
  }

  timesScalar(s) {
    return new Vector(
      this.x * s,
      this.y * s
    )
  }

  plus(other) {
    return new Vector(
      this.x + other.x,
      this.y + other.y
    )
  }

  minus(other) {
    return new Vector(
      this.x - other.x,
      this.y - other.y
    )
  }

  scalarProduct(other) {
    return (this.x * other.x) + (this.y * other.y)
  }
}
Enter fullscreen mode Exit fullscreen mode

Now we need to generate the gradient vectors. We'll create an array of arrays of vectors with the number of grid cells in both X and Y direction. For that we need the desired grid size and the actual width and height of the canvas we'll paint.

const getRandomGradientVectors = (width, height, gridSize) => {
  const possibleGradientVectors = [
    new Vector(1, 1),
    new Vector(-1, 1),
    new Vector(1, -1),
    new Vector(-1, -1),
  ]

  // + 1 because we need two vectors per grid cell.
  const gridCellsX = Math.ceil(width / gridSize) + 1
  const gridCellsY = Math.ceil(height / gridSize) + 1

  const gridCells = []

  for (let y = 0; y < gridCellsY; y++) {
    gridCells[y] = []
    for (let x = 0; x < gridCellsX; x++) {
      const gradientVector = possibleGradientVectors[
        Math.floor(Math.random() * 3)
      ]

      gridCells[y].push(gradientVector)
    }
  }

  return gridCells
}
Enter fullscreen mode Exit fullscreen mode

Now, we implement the actual perlin noise algorithm. We first figure out the grid cell and its gradient vectors, then calculate the relative position of the pixel and the distance vectors to that pixel within the grid, calculate the dot products, fade and lerp the entire thing.

const getPerlinValue = (x, y, gridSize, gradientVectors) => {
  // Determine grid cell
  const gridCellX = Math.floor(x / gridSize)
  const gridCellY = Math.floor(y / gridSize)

  // Figure out gradient vectors of that grid cell
  const usedGradientVectors = {
    topLeft: gradientVectors[gridCellY][gridCellX],
    topRight: gradientVectors[gridCellY][gridCellX + 1],
    bottomLeft: gradientVectors[gridCellY + 1][gridCellX],
    bottomRight: gradientVectors[gridCellY + 1][gridCellX + 1],
  }

  // Vectors for the corners - we need these to determine
  // the distance vectors to the pixel from the corners.
  const unitCornerVectors = {
    topLeft: new Vector(0, 0),
    topRight: new Vector(1, 0),
    bottomLeft: new Vector(0, 1),
    bottomRight: new Vector(1, 1)
  }

  // The relative position of the pixel within the grid cell
  const relativePos = new Vector(
    (x % gridSize) / gridSize,
    (y % gridSize) / gridSize,
  )

  // The distances of the corners to the pixel
  const distanceVectors = {
    topLeft: relativePos.minus(unitCornerVectors.topLeft),
    topRight: relativePos.minus(unitCornerVectors.topRight),
    bottomLeft: relativePos.minus(unitCornerVectors.bottomLeft),
    bottomRight: relativePos.minus(unitCornerVectors.bottomRight),
  }

  // The influence values we can later on lerp
  const influenceValues = {
    topLeft: usedGradientVectors.topLeft.scalarProduct(distanceVectors.topLeft),
    topRight: usedGradientVectors.topRight.scalarProduct(distanceVectors.topRight),
    bottomLeft: usedGradientVectors.bottomLeft.scalarProduct(distanceVectors.bottomLeft),
    bottomRight: usedGradientVectors.bottomRight.scalarProduct(distanceVectors.bottomRight),
  }

  // Fade and lerp
  const fadedX = fade(relativePos.x)
  const fadedY = fade(relativePos.y)

  const lerpedValue = lerp(
    lerp(influenceValues.topLeft, influenceValues.topRight, fadedX),
    lerp(influenceValues.bottomLeft, influenceValues.bottomRight, fadedX),
    fadedY
  )

  // Normalize the value
  return (1 + lerpedValue) / 2
}
Enter fullscreen mode Exit fullscreen mode

And that's more or less it. We now need to hook this up to our canvas, and we can have a look at the result:

const width = 640
const height = 340
const gridSize = 60

const canvas = document.querySelector('#c')

const paintGreyPixel = (canvas, x, y, grayScale) => {
  const context = canvas.getContext('2d')
  context.fillStyle = `rgba(0, 0, 0, ${grayScale})`

  context.fillRect(x, y, 1, 1)
}

const gradientVectors = getRandomGradientVectors(width, height, gridSize)

console.log(gradientVectors)

for (let x = 0; x < width; x++) {
  for (let y = 0; y < height; y++) {
    paintGreyPixel(canvas, x, y, getPerlinValue(x, y, gridSize, gradientVectors))
  }
}
Enter fullscreen mode Exit fullscreen mode

That was quick, though, wasn't it?

The result

And here's the result:

Let's be honest: Winning an Academy award with 162 lines of code? I'd be in!


I hope you enjoyed reading this article as much as I enjoyed writing it! If so, leave a ❤️ or a 🦄! I write tech articles in my free time and like to drink coffee every once in a while.

If you want to support my efforts, you can offer me a coffee or follow me on Twitter 🐦! You can also support me directly via Paypal!

Buy me a coffee button

Top comments (2)

Collapse
 
felicemcc profile image
Felice Candilio

Great and interesting article Pascal! Thanks

Collapse
 
thormeier profile image
Pascal Thormeier

Thank you, glad you liked it!