Noise.
Random Noise. Perlin Noise.
1 |
% matplotlib inline |
1 2 3 4 |
import math import random import numpy as np import matplotlib.pyplot as plt |
Introduction
This is an article about content generation through algorithms.
It sounds fancy, but the idea is a simple one – to generate textures or materials automatically, from numbers. Without real drawing in “Paint” or in another PC program. With other words, if we have the following binary np.array
in python
:
1 |
example = [[1, 0], [0, 1]] |
we would be able to generate the following image with it in python easily:
Ok, it is quite visible, that somehow we have plotted the 0
as a black square and the 1
as white squire.
Is this useful somehow? Probably, if you feel like building online chess boards and you can find someone to pay you for that. Anyway, the idea is to make it a bit more complicated from there, as between 1 and 0 we have enough values, and we will be able to generate something more interesting, based on the numbers there.
So, if the numbers are a bit random, we will get some random noise, that will look really not following any pattern, like this:
The idea of this article is to provide a tool, to build noise, that looks like it is not-random (but it is random!):
This, non-random looking noise that we will be building is called “Perlin Noise” and is an extremely powerful algorithm that is used often in procedural content generation. It is especially useful for games and other visual media such as movies.[1] Named after Ken Perlin, this technique earned him an “Oscar” for its originality. In game development, Perlin Noise can be used for any sort of wave-like, undulating material or texture. For example, it could be used for procedural terrain (Minecraft-like terrain can be created with Perlin Noise, for example), fire effects, water, and clouds.[1] Generally, Perlin Noise has a more organic appearance because it produces a naturally ordered (“smooth”) sequence of pseudo-random numbers [2].
Research Questions:
A good research question can be defined as How to build Perlin Noise in python?
A subquestion can be How to distinguish random noise from Perlin Noise?
A few lines about random noise (with examples)
Before going into Perlin Noise, it will be useful to get a few examples of non-Perlin Noise. Let’s plot a 6x6
non-random np.array()
, just to see how it works:
1 2 3 4 5 |
def plot_noise(noise, plt_title, cmap_given = "gray"): plt.imshow(noise, cmap=cmap_given, interpolation='nearest') #plt.colorbar() plt.title(plt_title) plt.show() |
1 2 3 4 5 6 7 8 9 10 |
non_random_noise = [ [1, 0, 1, 0, 1, 0], [.1, .9, .1, .9, .1, .9], [.8, .2, .8, .2, .8, .2], [.3, .7, .3, .7, .3, .7], [1, 0, 1, 0, 1, 0], [.4, .6, .4, .6, .4, .6] ] plot_noise(non_random_noise,'Looks like a strange 6x6 chess') |
Well, the picture above looks like strange 6×6 chess, with some squares in white and some in grey or being black completely. As discussed in the introduction, this is due to the fact that all the numbers between 0
and 1
can be plotted in the grey colors schema between black and white.
Let’s continue with random, non-Perlin noises. First we will plot some 8x8
random noises and then we will plot 10000x10000
.
1 2 3 |
def generate_random_noise(width, height): noise = np.random.rand(width, height) return noise |
1 2 3 4 5 6 7 |
def make_some_noise(size, label=""): width = size height = size random_noise = generate_random_noise(width, height) # Plot the noise plot_noise(random_noise, label) |
1 2 3 4 |
# Generate tiny random noise size = 8 title = "8x8 random noise" make_some_noise(size, title) |
1 2 3 4 |
# Generate huge random noise size = 10000 title = "10000x10000 random noise" make_some_noise(size, title) |
Well, it is obvious that the noises are random. But are these Perlin Noises? In general, the answer is simple no, but proving so might require a bit of an effort. The easiest way to prove it, is to open the noise plot in paint and to cut something:
If we can paste it anywhere in the picture without having the picture “distroyed” and without seeing where exactly did we paste it, then it is not a Perlin noise:
Perlin noise methodology
In this part of the article, an explanation for the “Perlin noise” is to be written.
And then the code. At the end the nice random pictures, that are nice, because they look non-random.
- Create an empty noise array
- Generate random gradient vectors
- Iterate over each pixel in the noise array and
- Calculate the grid cell coordinates for the current pixel
- Calculate the position within the cell as fractional offsets
- Calculate the dot products between gradients and offsets
- Interpolate the dot products using smoothstep function
- Store the interpolated value in the noise array
- Normalize the noise values within the range of 0 to 1
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 |
def generate_perlin_noise(width, height, scale): """ Generate Perlin noise using the given parameters. Parameters: - width (int): Width of the noise array. - height (int): Height of the noise array. - scale (int): Scale factor for generating the noise. Returns: - noise (n-dimensional array): Perlin noise array of shape (height, width). """ # Create an empty noise array noise = np.zeros((height, width)) # Generate random gradient vectors gradients = np.random.randn(height // scale + 2, width // scale + 2, 2) # Iterate over each pixel in the noise array for y in range(height): for x in range(width): # Calculate the grid cell coordinates for the current pixel cell_x = x // scale cell_y = y // scale # Calculate the position within the cell as fractional offsets cell_offset_x = x / scale - cell_x cell_offset_y = y / scale - cell_y # Calculate the dot products between gradients and offsets dot_product_tl = np.dot([cell_offset_x, cell_offset_y], gradients[cell_y, cell_x]) dot_product_tr = np.dot([cell_offset_x - 1, cell_offset_y], gradients[cell_y, cell_x + 1]) dot_product_bl = np.dot([cell_offset_x, cell_offset_y - 1], gradients[cell_y + 1, cell_x]) dot_product_br = np.dot([cell_offset_x - 1, cell_offset_y - 1], gradients[cell_y + 1, cell_x + 1]) # Interpolate the dot products using smoothstep function weight_x = smoothstep(cell_offset_x) weight_y = smoothstep(cell_offset_y) interpolated_top = lerp(dot_product_tl, dot_product_tr, weight_x) interpolated_bottom = lerp(dot_product_bl, dot_product_br, weight_x) interpolated_value = lerp(interpolated_top, interpolated_bottom, weight_y) # Store the interpolated value in the noise array noise[y, x] = interpolated_value # Normalize the noise values within the range of 0 to 1 noise = (noise - np.min(noise)) / (np.max(noise) - np.min(noise)) return noise def smoothstep(t): """ Smoothstep function for interpolation. Parameters: - t (float): Interpolation value between 0.0 and 1.0. Returns: - result (float): Smoothstep interpolated value. """ return t * t * (3 - 2 * t) def lerp(a, b, t): """ Linear interpolation between two values. Parameters: - a (float): Start value. - b (float): End value. - t (float): Interpolation factor between 0.0 and 1.0. Returns: - result (float): Interpolated value between a and b. """ return a + t * (b - a) |
1 2 3 4 5 6 7 8 9 10 |
# Set the width, height, and scale parameters width = 256 height = 256 scale = 10 # Generate the Perlin noise noise = generate_perlin_noise(width, height, scale) # Plot the Perlin noise plot_noise(noise, "Perlin noise example", cmap_given = "twilight") |
supported_cmap
values, built-in matplotlib. In the supported_cmap
list, add the commented values to the list, to see all of the possible cases. It might take up to 1 minute to generate all, that is why I have commented most of these.
1 |
supported_cmap = ['Accent', 'Accent_r', 'Blues', 'Blues_r', ]#'BrBG', 'BrBG_r', 'BuGn', 'BuGn_r', 'BuPu', 'BuPu_r', 'CMRmap' |
1 2 |
for cmap in supported_cmap: plot_noise(noise, f'Perlin Noise with {cmap}', cmap_given = cmap) |