Source code for shades.noise_fields

"""
noise_fields

Functions and classes relating to Shades' NoiseField
"""
import random
import math

from typing import List, Tuple, Union

import numpy as np

from numpy.typing import ArrayLike



[docs]class NoiseField(): """ An object to calculate and store perlin noise data. Initialisation takes float (recommend very low number < 0.1) and random seed """ def __init__(self, scale: float = 0.002, seed: int = None) -> None: if seed is None: self.seed = random.randint(0,9999) else: self.seed = seed self.scale = scale size = 10 self.x_lin = np.linspace(0, (size*self.scale), size, endpoint=False) self.y_lin = np.linspace(0, (size*self.scale), size, endpoint=False) self.field = self.perlin_field(self.x_lin, self.y_lin) self.x_negative_buffer = 0 self.y_negative_buffer = 0 self.buffer_chunks = 500 def _roundup(self, to_round: float, nearest_n: float) -> float: """ Internal function to round up number to_round to nearest_n """ return int(math.ceil(to_round / nearest_n)) * nearest_n
[docs] def buffer_field_right(self, to_extend: int) -> None: """ Extends object's noise field right """ # y is just gonna stay the same, but x needs to be picking up max_lin = self.x_lin[-1] additional_x_lin = np.linspace( max_lin+self.scale, max_lin+(to_extend*self.scale), to_extend, endpoint=False, ) self.field = np.concatenate( [self.field, self.perlin_field(additional_x_lin, self.y_lin)], axis=1, ) self.x_lin = np.concatenate([self.x_lin, additional_x_lin])
[docs] def buffer_field_bottom(self, to_extend: int) -> None: """ Extends object's noise field downwards """ max_lin = self.y_lin[-1] additional_y_lin = np.linspace( max_lin+self.scale, max_lin+(to_extend*self.scale), to_extend, endpoint=False, ) self.field = np.concatenate( [self.field, self.perlin_field(self.x_lin, additional_y_lin)], axis=0, ) self.y_lin = np.concatenate([self.y_lin, additional_y_lin])
[docs] def buffer_field_left(self, to_extend: int) -> None: """ Extends object's noise field left """ min_lin = self.x_lin[0] additional_x_lin = np.linspace( min_lin-(to_extend*self.scale), min_lin, to_extend, endpoint=False, ) self.field = np.concatenate( [self.perlin_field(additional_x_lin, self.y_lin), self.field], axis=1, ) self.x_lin = np.concatenate([additional_x_lin, self.x_lin]) self.x_negative_buffer += to_extend
[docs] def buffer_field_top(self, to_extend: int) -> None: """ Extends object's noise field upwards """ min_lin = self.y_lin[0] additional_y_lin = np.linspace( min_lin-(to_extend*self.scale), min_lin, to_extend, endpoint=False, ) self.field = np.concatenate( [self.perlin_field(self.x_lin, additional_y_lin), self.field], axis=0, ) self.y_lin = np.concatenate([additional_y_lin, self.y_lin]) self.y_negative_buffer += to_extend
[docs] def perlin_field(self, x_lin: List[float], y_lin: List[float]) -> ArrayLike: """ generate field from x and y linear points credit to tgirod for stack overflow on numpy perlin noise (most of this code from answer) https://stackoverflow.com/questions/42147776/producing-2d-perlin-noise-with-numpy """ # remembering the random state (so we can put it back after) initial_random_state = np.random.get_state() x_grid, y_grid = np.meshgrid(x_lin, y_lin) x_grid %= 512 y_grid %= 512 # permutation table np.random.seed(self.seed) field_256 = np.arange(256, dtype=int) np.random.shuffle(field_256) field_256 = np.stack([field_256, field_256]).flatten() # coordinates of the top-left x_i, y_i = x_grid.astype(int), y_grid.astype(int) # internal coordinates x_f, y_f = x_grid - x_i, y_grid - y_i # fade factors u_array, v_array = self.fade(x_f), self.fade(y_f) # noise components n00 = self.gradient(field_256[(field_256[x_i%512] + y_i)%512], x_f, y_f) n01 = self.gradient(field_256[(field_256[x_i%512] + y_i + 1)%512], x_f, y_f - 1) n11 = self.gradient( field_256[(field_256[((x_i%512)+1)%512] + y_i + 1)%512], x_f - 1, y_f - 1) n10 = self.gradient(field_256[(field_256[((x_i%512)+1)%512] + y_i)%512], x_f - 1, y_f) # combine noises x_1 = self.lerp(n00, n10, u_array) x_2 = self.lerp(n01, n11, u_array) # putting the random state back in place np.random.set_state(initial_random_state) field = self.lerp(x_1, x_2, v_array) field += 0.5 return field
[docs] def lerp(self, a_array: ArrayLike, b_array: ArrayLike, x_array: ArrayLike) -> ArrayLike: "linear interpolation" return a_array + x_array * (b_array - a_array)
[docs] def fade(self, t_array: ArrayLike) -> ArrayLike: "6t^5 - 15t^4 + 10t^3" return 6 * t_array**5 - 15 * t_array**4 + 10 * t_array**3
[docs] def gradient(self, h_array: ArrayLike, x_array: ArrayLike, y_array: ArrayLike) -> ArrayLike: "grad converts h to the right gradient vector and return the dot product with (x,y)" vectors = np.array([[0, 1], [0, -1], [1, 0], [-1, 0]]) g_array = vectors[h_array % 4] return g_array[:, :, 0] * x_array + g_array[:, :, 1] * y_array
[docs] def noise(self, xy_coords: Tuple[int, int]): """ Returns noise of xy coords Also manages noise_field (will dynamically recalcuate as needed) """ if self.scale == 0: return 0 x_coord, y_coord = xy_coords x_coord += self.x_negative_buffer y_coord += self.y_negative_buffer if x_coord < 0: # x negative buffer needs to be increased x_to_backfill = self._roundup(abs(x_coord), self.buffer_chunks) self.buffer_field_left(x_to_backfill) x_coord, y_coord = xy_coords x_coord += self.x_negative_buffer y_coord += self.y_negative_buffer if y_coord < 0: # y negative buffer needs to be increased y_to_backfill = self._roundup(abs(y_coord), self.buffer_chunks) self.buffer_field_top(y_to_backfill) x_coord, y_coord = xy_coords x_coord += self.x_negative_buffer y_coord += self.y_negative_buffer try: return self.field[int(y_coord)][int(x_coord)] except IndexError: # ran out of generated noise, so need to extend the field height, width = self.field.shape x_to_extend = x_coord - width + 1 y_to_extend = y_coord - height + 1 if x_to_extend > 0: x_to_extend = self._roundup(x_to_extend, self.buffer_chunks) self.buffer_field_right(x_to_extend) if y_to_extend > 0: y_to_extend = self._roundup(y_to_extend, self.buffer_chunks) self.buffer_field_bottom(y_to_extend) return self.noise(xy_coords)
[docs] def recursive_noise( self, xy_coords: Tuple[int, int], depth: int = 1, feedback: float = 0.7, ) -> float: """Returns domain warped recursive perlin noise (number between 0 and 1 from xy coordinates) Recomended feedback (which determines affect of recursive call) around 0-2 """ # base case if depth <= 0: return self.noise(xy_coords) # else recur with noise of noise noise_from_xy_coords = int(self.noise(xy_coords) * feedback * 100) return self.recursive_noise( (noise_from_xy_coords, noise_from_xy_coords), depth-1, feedback, )
[docs]def noise_fields( scale: Union[List[float], float] = 0.002, seed: Union[List[int], int] = None, channels: int = 3, ) -> List[NoiseField]: """ Create multiple NoiseField objects in one go. This is a quality of life function, rather than adding new behaviour shades.noise_fields(scale=0.2, channels=3) rather than [shades.NoiseField(scale=0.2) for i in range(3)] """ if not isinstance(scale, list): scale = [scale for i in range(channels)] if not isinstance(seed, list): seed = [seed for i in range(channels)] return [NoiseField(scale=scale[i], seed=seed[i]) for i in range(channels)]