Source code for tcod.noise

"""Noise map generators are provided by this module.

The :any:`Noise.sample_mgrid` and :any:`Noise.sample_ogrid` methods perform
much better than multiple calls to :any:`Noise.get_point`.

Example::

    import numpy as np
    import tcod

    noise = tcod.noise.Noise(
        dimensions=2,
        algorithm=tcod.NOISE_SIMPLEX,
        implementation=tcod.noise.TURBULENCE,
        hurst=0.5,
        lacunarity=2.0,
        octaves=4,
        seed=None,
        )

    # Create a 5x5 open multi-dimensional mesh-grid.
    ogrid = [np.arange(5, dtype=np.float32),
             np.arange(5, dtype=np.float32)]
    print(ogrid)

    # Scale the grid.
    ogrid[0] *= 0.25
    ogrid[1] *= 0.25

    # Return the sampled noise from this grid of points.
    samples = noise.sample_ogrid(ogrid)
    print(samples)
"""
from typing import Any, Optional

import numpy as np

import tcod.constants
import tcod.random
from tcod._internal import deprecate
from tcod.loader import ffi, lib

"""Noise implementation constants"""
SIMPLE = 0
FBM = 1
TURBULENCE = 2


[docs]class Noise(object): """ The ``hurst`` exponent describes the raggedness of the resultant noise, with a higher value leading to a smoother noise. Not used with tcod.noise.SIMPLE. ``lacunarity`` is a multiplier that determines how fast the noise frequency increases for each successive octave. Not used with tcod.noise.SIMPLE. Args: dimensions (int): Must be from 1 to 4. algorithm (int): Defaults to NOISE_SIMPLEX implementation (int): Defaults to tcod.noise.SIMPLE hurst (float): The hurst exponent. Should be in the 0.0-1.0 range. lacunarity (float): The noise lacunarity. octaves (float): The level of detail on fBm and turbulence implementations. seed (Optional[Random]): A Random instance, or None. Attributes: noise_c (CData): A cffi pointer to a TCOD_noise_t object. """ def __init__( self, dimensions: int, algorithm: int = 2, implementation: int = SIMPLE, hurst: float = 0.5, lacunarity: float = 2.0, octaves: float = 4, seed: Optional[tcod.random.Random] = None, ): if not 0 < dimensions <= 4: raise ValueError( "dimensions must be in range 0 < n <= 4, got %r" % (dimensions,) ) self._random = seed _random_c = seed.random_c if seed else ffi.NULL self._algorithm = algorithm self.noise_c = ffi.gc( ffi.cast( "struct TCOD_Noise*", lib.TCOD_noise_new(dimensions, hurst, lacunarity, _random_c), ), lib.TCOD_noise_delete, ) self._tdl_noise_c = ffi.new( "TDLNoise*", (self.noise_c, dimensions, 0, octaves) ) self.implementation = implementation # sanity check @property def dimensions(self) -> int: return int(self._tdl_noise_c.dimensions) @property # type: ignore @deprecate("This is a misspelling of 'dimensions'.") def dimentions(self) -> int: return self.dimensions @property def algorithm(self) -> int: return int(self.noise_c.noise_type) @algorithm.setter def algorithm(self, value: int) -> None: lib.TCOD_noise_set_type(self.noise_c, value) @property def implementation(self) -> int: return int(self._tdl_noise_c.implementation) @implementation.setter def implementation(self, value: int) -> None: if not 0 <= value < 3: raise ValueError("%r is not a valid implementation. " % (value,)) self._tdl_noise_c.implementation = value @property def hurst(self) -> float: return float(self.noise_c.H) @property def lacunarity(self) -> float: return float(self.noise_c.lacunarity) @property def octaves(self) -> float: return float(self._tdl_noise_c.octaves) @octaves.setter def octaves(self, value: float) -> None: self._tdl_noise_c.octaves = value
[docs] def get_point( self, x: float = 0, y: float = 0, z: float = 0, w: float = 0 ) -> float: """Return the noise value at the (x, y, z, w) point. Args: x (float): The position on the 1st axis. y (float): The position on the 2nd axis. z (float): The position on the 3rd axis. w (float): The position on the 4th axis. """ return float(lib.NoiseGetSample(self._tdl_noise_c, (x, y, z, w)))
[docs] def __getitem__(self, indexes: Any) -> np.ndarray: """Sample a noise map through NumPy indexing. This follows NumPy's advanced indexing rules, but allows for floating point values. .. versionadded:: 11.16 """ if not isinstance(indexes, tuple): indexes = (indexes,) if len(indexes) > self.dimensions: raise IndexError( "This noise generator has %i dimensions, but was indexed with %i." % (self.dimensions, len(indexes)) ) indexes = np.broadcast_arrays(*indexes) c_input = [ffi.NULL, ffi.NULL, ffi.NULL, ffi.NULL] for i, index in enumerate(indexes): if index.dtype.type == np.object_: raise TypeError("Index arrays can not be of dtype np.object_.") indexes[i] = np.ascontiguousarray(index, dtype=np.float32) c_input[i] = ffi.from_buffer("float*", indexes[i]) out = np.empty(indexes[0].shape, dtype=np.float32) if self.implementation == SIMPLE: lib.TCOD_noise_get_vectorized( self.noise_c, self.algorithm, out.size, *c_input, ffi.from_buffer("float*", out), ) elif self.implementation == SIMPLE: lib.TCOD_noise_get_fbm_vectorized( self.noise_c, self.algorithm, self.octaves, out.size, *c_input, ffi.from_buffer("float*", out), ) elif self.implementation == TURBULENCE: lib.TCOD_noise_get_turbulence_vectorized( self.noise_c, self.algorithm, self.octaves, out.size, *c_input, ffi.from_buffer("float*", out), ) else: raise TypeError("Unexpected %r" % self.implementation) return out
[docs] def sample_mgrid(self, mgrid: np.ndarray) -> np.ndarray: """Sample a mesh-grid array and return the result. The :any:`sample_ogrid` method performs better as there is a lot of overhead when working with large mesh-grids. Args: mgrid (numpy.ndarray): A mesh-grid array of points to sample. A contiguous array of type `numpy.float32` is preferred. Returns: numpy.ndarray: An array of sampled points. This array has the shape: ``mgrid.shape[:-1]``. The ``dtype`` is `numpy.float32`. """ mgrid = np.ascontiguousarray(mgrid, np.float32) if mgrid.shape[0] != self.dimensions: raise ValueError( "mgrid.shape[0] must equal self.dimensions, " "%r[0] != %r" % (mgrid.shape, self.dimensions) ) out = np.ndarray(mgrid.shape[1:], np.float32) if mgrid.shape[1:] != out.shape: raise ValueError( "mgrid.shape[1:] must equal out.shape, " "%r[1:] != %r" % (mgrid.shape, out.shape) ) lib.NoiseSampleMeshGrid( self._tdl_noise_c, out.size, ffi.from_buffer("float*", mgrid), ffi.from_buffer("float*", out), ) return out
[docs] def sample_ogrid(self, ogrid: np.ndarray) -> np.ndarray: """Sample an open mesh-grid array and return the result. Args ogrid (Sequence[Sequence[float]]): An open mesh-grid. Returns: numpy.ndarray: An array of sampled points. The ``shape`` is based on the lengths of the open mesh-grid arrays. The ``dtype`` is `numpy.float32`. """ if len(ogrid) != self.dimensions: raise ValueError( "len(ogrid) must equal self.dimensions, " "%r != %r" % (len(ogrid), self.dimensions) ) ogrids = [np.ascontiguousarray(array, np.float32) for array in ogrid] out = np.ndarray([array.size for array in ogrids], np.float32) lib.NoiseSampleOpenMeshGrid( self._tdl_noise_c, len(ogrids), out.shape, [ffi.from_buffer("float*", array) for array in ogrids], ffi.from_buffer("float*", out), ) return out
def __getstate__(self) -> Any: state = self.__dict__.copy() if self.dimensions < 4 and self.noise_c.waveletTileData == ffi.NULL: # Trigger a side effect of wavelet, so that copies will be synced. saved_algo = self.algorithm self.algorithm = tcod.constants.NOISE_WAVELET self.get_point() self.algorithm = saved_algo waveletTileData = None if self.noise_c.waveletTileData != ffi.NULL: waveletTileData = list( self.noise_c.waveletTileData[0 : 32 * 32 * 32] ) state["_waveletTileData"] = waveletTileData state["noise_c"] = { "ndim": self.noise_c.ndim, "map": list(self.noise_c.map), "buffer": [list(sub_buffer) for sub_buffer in self.noise_c.buffer], "H": self.noise_c.H, "lacunarity": self.noise_c.lacunarity, "exponent": list(self.noise_c.exponent), "waveletTileData": waveletTileData, "noise_type": self.noise_c.noise_type, } state["_tdl_noise_c"] = { "dimensions": self._tdl_noise_c.dimensions, "implementation": self._tdl_noise_c.implementation, "octaves": self._tdl_noise_c.octaves, } return state def __setstate__(self, state: Any) -> None: if isinstance(state, tuple): # deprecated format return self._setstate_old(state) # unpack wavelet tile data if it exists if "_waveletTileData" in state: state["_waveletTileData"] = ffi.new( "float[]", state["_waveletTileData"] ) state["noise_c"]["waveletTileData"] = state["_waveletTileData"] else: state["noise_c"]["waveletTileData"] = ffi.NULL # unpack TCOD_Noise and link to Random instance state["noise_c"]["rand"] = state["_random"].random_c state["noise_c"] = ffi.new("struct TCOD_Noise*", state["noise_c"]) # unpack TDLNoise and link to libtcod noise state["_tdl_noise_c"]["noise"] = state["noise_c"] state["_tdl_noise_c"] = ffi.new("TDLNoise*", state["_tdl_noise_c"]) self.__dict__.update(state) def _setstate_old(self, state: Any) -> None: self._random = state[0] self.noise_c = ffi.new("struct TCOD_Noise*") self.noise_c.ndim = state[3] ffi.buffer(self.noise_c.map)[:] = state[4] ffi.buffer(self.noise_c.buffer)[:] = state[5] self.noise_c.H = state[6] self.noise_c.lacunarity = state[7] ffi.buffer(self.noise_c.exponent)[:] = state[8] if state[9]: # high change of this being prematurely garbage collected! self.__waveletTileData = ffi.new("float[]", 32 * 32 * 32) ffi.buffer(self.__waveletTileData)[:] = state[9] self.noise_c.noise_type = state[10] self._tdl_noise_c = ffi.new( "TDLNoise*", (self.noise_c, self.noise_c.ndim, state[1], state[2]) )