Making Noise Predictably
Meeting Ken Perlin
What noise based outcomes will we see today?
We will understand the basics of procedural noise generation: generating random noise-like numbers that allow us to model and create very realistic-looking textures, such as wood, fire, marble, terrain, mountains, and clouds.
Inspiration
What is Perlin Noise?
Ok, this is going to be a long explanation!!!
A. Inner Product Computation
Let us start by dividing up 2D space ( for now!!) into square-shaped cells. At each vertex we randomly place a unit gradient vector labelled \(r_{i}\) that points in a random direction. See the figure below:
A perhaps more evocative picture may be this representation of an event from the Mahabharata:
We wish to calculate the Perlin Noise amount at any point of interest inside the cell.
- We draw difference vectors to the point from each of the 4 vertices.
- We compute the vector dot product with each of the \(r_{i}\) and the above difference vectors. ( 4 dot products )
- These are shown in the text print at the side of the figure.
Note how the 4 dot products change as you move the mouse/touchpad. This changes the 4 gradient vectors and hence the scalar dot products change in amplitude and polarity.
In a typical Perlin Noise implementation, the gradient vectors are fixed after an initial setup. So each gradient vector generates a range of dot-product values as the point of interest moves within the cell.
B. Interpolation of Dot Product values
With the 4 scalar dot products, we are now ready to compute the Perlin Noise value at the point of interest. There are several ways of doing this:
- Simply take the average
- Take a weighted average, with fixed weights.
- Use a weighting/interpolating function: The closer a point of interest is to one or other of the cell vertices, the higher is the contribution of the corresponding dot-product.
The third approach is the one embedded within (all?) Perlin Noise implementations. The interpolating function is: \[ f(t) = 6t^5-15t^4+10t^3 \] and looks like this:
Both \(\frac{df(t)}{dt}\) and \(\frac{d^2f(t)}{dt}\) are continuous at the ends of the range of the function (t = 0 and t = 1).
\[ \begin{array}{lcl}f'(t) & = & \ \frac{d}{dt}[6*t^5 - 15*t^4 + 10*t^3]\\ & = & 30 * (t^4 - 2 * t^3 + t^2)\\ & = & 0 \ \text{@ t = 0 and t = 1} \end{array} \]
\[ \begin{array}{lcl}f''(t) & = & \ \frac{d^2}{dt^2}[6*t^5 - 15*t^4 + 10*t^3]\\ & = &60 * (2 * t^3 - 3 * t^2 + t)\\ & = & 0 \ \text{@ t = 0 and t = 1} \end{array} \] This ensures that there are not sudden changes in the noise function near about the vertices.
D. Fractal Overlay and Combining
Now that we have one grid full of a layer of noise generated by weighted dot-products, we can appreciate one more thing: we can overlay the space with several layers of such noise values. Why would this be a good idea?
This multiple layer overlay creates a very natural-looking fractal-ness in the resulting noise function. Most natural looking shapes like landscapes, mountains, vegetables, flames…all have this self-similar structure where when one zooms in, the magnified function looks pretty much like the un-zoomed version!!
So how we create and merge overlays? We create several more-closely-spaced grids overlaid on the first one, and generate noise in the same way. These layers of noise-s are scaled by a factor (Usually \(\Large{\frac{1}{2^n}}\)), where \(n\) is the “order” of the layer. Each new finely-spaced layer generates similar-looking noise functions, which are combined with smaller and smaller weights to achieve that final polished fractal look of Perlin Noise.
We will explore this fractality with code. For now, here is Ken Perlin’s own explanation from 1999:
“The outline of my algorithm to create noise is very simple. Given an input point P, look at each of the surrounding grid points. In two dimensions there will be four surrounding grid points; in three dimensions there will be eight. In n dimensions, a point will have 2n surrounding grid points.
For each surrounding grid point Q, choose a pseudo-random gradient vector G. It is very important that for any particular grid point you always choose the same gradient vector.
Compute the inner product G . (P-Q). This will give the value at P of the linear function with gradient G which is zero at grid point Q.
Now you have 2n of these values. Interpolate between them down to your point, using an S-shaped cross-fade curve (eg: 3t2-2t3) to weight the interpolant in each dimension. This step will require computing n S-curves, followed by 2n-1 linear interpolations.”
— Ken Perlin
Creating Textures and Waveforms with Perlin Noise
Here is a landscape generated using Perlin Noise:
Duis ornare ex ac iaculis pretium. Maecenas sagittis odio id erat pharetra, sit amet consectetur quam sollicitudin. Vivamus pharetra quam purus, nec sagittis risus pretium at. Nullam feugiat, turpis ac accumsan interdum, sem tellus blandit neque, id vulputate diam quam semper nisl. Donec sit amet enim at neque porttitor aliquet. Phasellus facilisis nulla eget placerat eleifend. Vestibulum non egestas eros, eget lobortis ipsum. Nulla rutrum massa eget enim aliquam, id porttitor erat luctus. Nunc sagittis quis eros eu sagittis. Pellentesque dictum, erat at pellentesque sollicitudin, justo augue pulvinar metus, quis rutrum est mi nec felis. Vestibulum efficitur mi lorem, at elementum purus tincidunt a. Aliquam finibus enim magna, vitae pellentesque erat faucibus at. Nulla mauris tellus, imperdiet id lobortis et, dignissim condimentum ipsum. Morbi nulla orci, varius at aliquet sed, facilisis id tortor. Donec ut urna nisi.
Add explanation of salient part of the code.
The ambient
package allows us to create a variety of noise patterns, including Perlin Noise. The commands are: gen_perlin()
and noise_perlin()
, whose arguments are:
-
dim
: The dimensions (height, width, (and depth)) of the noise to be generated. The length determines the dimensionality of the noise. -
frequency
: Determines the granularity of the features in the noise. -
interpolator
:How should values between sampled points be calculated? Either ‘linear’, ‘hermite’, or ‘quintic’ (default), ranging from lowest to highest quality. -
fractal
: The fractal type to use. Either ‘none’, ‘fbm’ (default), ‘billow’, or ‘rigid-multi’. It is suggested that you experiment with the different types to get a feel for how they behaves. -
octaves
: The number of noise layers used to create the fractal noise. Ignored if fractal = ‘none’. Defaults to 3. -
lacunarity
: The frequency multiplier between successive noise layers when building fractal noise. Ignored if fractal = ‘none’. Defaults to 2. -
gain
: The relative strength between successive noise layers when building fractal noise. Ignored if fractal = ‘none’. Defaults to 0.5. -
pertubation
: The perturbation to use. Either ‘none’ (default), ‘normal’, or ‘fractal’. Defines the displacement (warping) of the noise, with ‘normal’ giving a smooth warping and ‘fractal’ giving a more erratic warping. -
pertubation_amplitude
: The maximal perturbation distance from the origin. Ignored ifpertubation
= ‘none’. Defaults to 1. -
x, y, z
: Coordinates to get noise value from -
seed
: The seed to use for the noise. If NULL a random seed will be used:
noise2 <- noise_perlin(
dim = c(400, 400), # height/width
frequency = 0.01, # Lower = less granular, more organic
interpolator = "quintic", #' linear', 'hermite', or 'quintic'
fractal = "fbm", # Try "billow" , "rigid-multi"
octaves = 5,
lacunarity = 2,
gain = 0.8, # Default = 0.5 giving 1/2^n scaling
pertubation = "none", # Note the incorrect spelling
pertubation_amplitude = 1 # Note the incorrect spelling
)
## generates a matrix
noise2 %>% as_tibble()
# Plot the matrix
plot(as.raster(normalise(noise2)))
# Using the generator
grid <- long_grid(seq(1, 10, length.out = 1000), seq(1, 10, length.out = 1000))
grid$noise <- gen_perlin(grid$x, grid$y,
octaves = 5,
frequency = 1.2
)
plot(grid, noise)
##
grid %>%
gf_point(y ~ x,
colour = ~noise,
size = 0.01, show.legend = F
) %>%
gf_refine(
scale_color_gradient(
low = "orangered",
high = "black"
),
coord_fixed()
) %>%
gf_theme(theme_void())
It seems the ambient
package cannot generate 1D-Perlin noise, so we cannot generate say time waveforms based on this idea. p5.js
of course can do 1D.
Wait, But Why?
- Generating random waveforms and textures is an important part of Creative Computational projects.
- These ideas are used in landscape, texture and sound generation.
- “Normal” random noise is too jagged to have the natural look that we would like
- Perlin Noise creates both a smoothness and a fine-grain random structure in an arbitrary number of dimensions.
References
Ken Perlin.(1999). Making Noise. https://web.archive.org/web/20151221035155/http://www.noisemachine.com/talk1/index.html. Based on a talk presented at GDCHardCore on Dec 9, 1999.
https://betterexplained.com/articles/vector-calculus-understanding-the-dot-product/
Gorilla Sun Blog. Perlin Noise. https://www.gorillasun.de/tag/perlin-noise/
The
ambient
package in R: https://ambient.data-imaginist.com/
Textbooks
- Patricio Gonzalez Vivo & Jen Lowe. The Book of Shaders. https://thebookofshaders.com/11/
R Package Citations
Citation
@online{2024,
author = {},
title = {\textless Iconify-Icon Icon=“arcticons:noise-Reducer”
Width=“1.2em”
Height=“1.2em”\textgreater\textless/Iconify-Icon\textgreater{}
{Making} {Noise} {Predictably}},
date = {2024-11-07},
url = {https://av-quarto.netlify.app/content/courses/MathModelsDesign/Modules/3-Waves/20-MakingNoise/},
langid = {en},
abstract = {Can you make the same random noise again?}
}