// Define an object to handle Perlin generation
let perlin = {
// Gets a random vector - not seeded - however results will be saved into a map
rand_vect: function(){
let theta = Math.random() * 2 * Math.PI;
return {x: Math.cos(theta), y: Math.sin(theta)};
},
// Takes two points - the candidate point (x,y), and a grid corner (vx, vy)
// Interpolates between the vector of the corner (grid_vector)
// and the vector represented by corner->candidate point (candidate_vector)
dot_prod_grid: function(x, y, vx, vy, octave){
// Calculate the candidate vector from the co-ordinate
let candidate_vector = {x: x - vx, y: y - vy};
// Generate the unit-length Grid Vector, or fetch from cache if it exists
let key = [vx, vy, octave].join(",");
let grid_vector;
if (this.vector_map.has(key)){
grid_vector = this.vector_map.get(key);
} else {
grid_vector = this.rand_vect();
this.vector_map.set(key, grid_vector);
}
// Return the dot-product
return candidate_vector.x * grid_vector.x + candidate_vector.y * grid_vector.y;
},
// Weight Interpolation using various functions
smootherstep: function(x) {
// Smoother step interpolation (version from Ken Perlin)
return (x**3 * (x * ((x*6) - 15) + 10));
},
smoothstep: function(x) {
// Smoothstep interpolation (3x^2 - 2x^2)
return x**2 * (3 - (2*x));
},
linear: function(x) {
// Linear interpolation
return x
},
// Value interpolation utilising various weight interpolation functions
interpolate: function(weight, a, b) {
return a + this.smootherstep(weight) * (b-a);
},
// Initialises the object and clears any saved gradients or vectors
// vector_map are saved vectors to use when calculating dot products
// perlin_cache are saved perlin values so if the same coordinates are called, the same output is fetched
seed: function(){
this.vector_map = new Map();
this.perlin_cache = new Map();
},
// Main function to get perlin noise
get: function(x, y, octave = 1) {
// TotalPerlin is the accumulated value of multiple layers of perlin noise added to a point
let TotalPerlin = 0;
// Loop over every octave. At each octave a new value of perlin noise is generated and added to TotalPerlin
for (let currentOctave = 1; currentOctave <= octave; currentOctave++) {
// Checks if the cache contains a value for this coordinate
let key = [x,y,currentOctave].join(",");
if (this.perlin_cache.has(key))
return this.perlin_cache.get(key);
// Get the corners of the coordinate
let xFloor = Math.floor(x);
let yFloor = Math.floor(y);
// Calculate Grid Corner values from the two vectors at every point
let TopLeft = this.dot_prod_grid(x, y, xFloor, yFloor, currentOctave);
let TopRight = this.dot_prod_grid(x, y, xFloor+1, yFloor, currentOctave);
let BottomLeft = this.dot_prod_grid(x, y, xFloor, yFloor+1, currentOctave);
let BottomRight = this.dot_prod_grid(x, y, xFloor+1, yFloor+1, currentOctave);
// Interpolate between values at Grid Corners
let xTop = this.interpolate(x-xFloor, TopLeft, TopRight);
let xBottom = this.interpolate(x-xFloor, BottomLeft, BottomRight);
let Value = this.interpolate(y-yFloor, xTop, xBottom);
// Adjust amplitude based on octave
Value /= (Math.pow(2, currentOctave-1));
// Saves to cache in case the same coord is called in the future
this.perlin_cache.set(key, Value);
TotalPerlin += Value;
// Adjust x and y to octaves for future iterations
x *= 2;
y *= 2;
}
return TotalPerlin;
}
}
// Initialises the caches as soon as the object is defined
perlin.seed();