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.Algorithm.SIMPLEX,
    ...     seed=42,
    ... )
    >>> samples = noise[tcod.noise.grid(shape=(5, 4), scale=0.25, origin=(0, 0))]
    >>> samples  # Samples are a grid of floats between -1.0 and 1.0
    array([[ 0.        , -0.55046356, -0.76072866, -0.7088647 , -0.68165785],
           [-0.27523372, -0.7205134 , -0.74057037, -0.43919194, -0.29195625],
           [-0.40398532, -0.57662135, -0.33160293,  0.12860827,  0.2864191 ],
           [-0.50773406, -0.2643614 ,  0.24446318,  0.6390255 ,  0.5922846 ]],
          dtype=float32)
    >>> (samples + 1.0) * 0.5  # You can normalize samples to 0.0 - 1.0
    array([[0.5       , 0.22476822, 0.11963567, 0.14556766, 0.15917107],
           [0.36238313, 0.1397433 , 0.12971482, 0.28040403, 0.35402188],
           [0.29800734, 0.21168932, 0.33419853, 0.5643041 , 0.6432096 ],
           [0.24613297, 0.3678193 , 0.6222316 , 0.8195127 , 0.79614234]],
          dtype=float32)
    >>> ((samples + 1.0) * (256 / 2)).astype(np.uint8)  # Or as 8-bit unsigned bytes.
    array([[128,  57,  30,  37,  40],
           [ 92,  35,  33,  71,  90],
           [ 76,  54,  85, 144, 164],
           [ 63,  94, 159, 209, 203]], dtype=uint8)
"""

from __future__ import annotations

import enum
import warnings
from typing import Any, Sequence

import numpy as np
from numpy.typing import ArrayLike, NDArray
from typing_extensions import Literal

import tcod.constants
import tcod.random
from tcod.cffi import ffi, lib


[docs] class Algorithm(enum.IntEnum): """Libtcod noise algorithms. .. versionadded:: 12.2 """ PERLIN = 1 """Perlin noise.""" SIMPLEX = 2 """Simplex noise.""" WAVELET = 4 """Wavelet noise.""" def __repr__(self) -> str: return f"tcod.noise.Algorithm.{self.name}"
[docs] class Implementation(enum.IntEnum): """Noise implementations. .. versionadded:: 12.2 """ SIMPLE = 0 """Generate plain noise.""" FBM = 1 """Fractional Brownian motion. https://en.wikipedia.org/wiki/Fractional_Brownian_motion """ TURBULENCE = 2 """Turbulence noise implementation.""" def __repr__(self) -> str: return f"tcod.noise.Implementation.{self.name}"
def __getattr__(name: str) -> Implementation: if name in Implementation.__members__: warnings.warn( f"'tcod.noise.{name}' is deprecated," f" use 'tcod.noise.Implementation.{name}' instead.", FutureWarning, stacklevel=2, ) return Implementation[name] msg = f"module {__name__} has no attribute {name}" raise AttributeError(msg)
[docs] class Noise: """A configurable noise sampler. 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: Must be from 1 to 4. algorithm: Defaults to :any:`tcod.noise.Algorithm.SIMPLEX` implementation: Defaults to :any:`tcod.noise.Implementation.SIMPLE` hurst: The hurst exponent. Should be in the 0.0-1.0 range. lacunarity: The noise lacunarity. octaves: The level of detail on fBm and turbulence implementations. seed: A Random instance, or None. Attributes: noise_c (CData): A cffi pointer to a TCOD_noise_t object. """ def __init__( # noqa: PLR0913 self, dimensions: int, algorithm: int = Algorithm.SIMPLEX, implementation: int = Implementation.SIMPLE, hurst: float = 0.5, lacunarity: float = 2.0, octaves: float = 4, seed: int | tcod.random.Random | None = None, ) -> None: """Initialize and seed the noise object.""" if not 0 < dimensions <= 4: # noqa: PLR2004 msg = f"dimensions must be in range 0 < n <= 4, got {dimensions}" raise ValueError(msg) self._seed = seed self._random = self.__rng_from_seed(seed) _random_c = self._random.random_c 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.algorithm = algorithm self.implementation = implementation # sanity check @staticmethod def __rng_from_seed(seed: None | int | tcod.random.Random) -> tcod.random.Random: if seed is None or isinstance(seed, int): return tcod.random.Random(seed=seed, algorithm=tcod.random.MERSENNE_TWISTER) return seed def __repr__(self) -> str: parameters = [ f"dimensions={self.dimensions}", f"algorithm={self.algorithm!r}", f"implementation={Implementation(self.implementation)!r}", ] if self.hurst != 0.5: parameters.append(f"hurst={self.hurst}") if self.lacunarity != 2: parameters.append(f"lacunarity={self.lacunarity}") if self.octaves != 4: parameters.append(f"octaves={self.octaves}") if self._seed is not None: parameters.append(f"seed={self._seed}") return f"tcod.noise.Noise({', '.join(parameters)})" @property def dimensions(self) -> int: return int(self._tdl_noise_c.dimensions) @property def algorithm(self) -> int: noise_type = self.noise_c.noise_type return Algorithm(noise_type) if noise_type else Algorithm.SIMPLEX @algorithm.setter def algorithm(self, value: int) -> None: lib.TCOD_noise_set_type(self.noise_c, value) @property def implementation(self) -> int: return Implementation(self._tdl_noise_c.implementation) @implementation.setter def implementation(self, value: int) -> None: if not 0 <= value < 3: # noqa: PLR2004 msg = f"{value!r} is not a valid implementation. " raise ValueError(msg) 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: The position on the 1st axis. y: The position on the 2nd axis. z: The position on the 3rd axis. w: The position on the 4th axis. """ return float(lib.NoiseGetSample(self._tdl_noise_c, (x, y, z, w)))
[docs] def __getitem__(self, indexes: Any) -> NDArray[np.float32]: """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 = list(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_: msg = "Index arrays can not be of dtype np.object_." raise TypeError(msg) indexes[i] = np.ascontiguousarray(index, dtype=np.float32) c_input[i] = ffi.from_buffer("float*", indexes[i]) out: NDArray[np.float32] = np.empty(indexes[0].shape, dtype=np.float32) if self.implementation == Implementation.SIMPLE: lib.TCOD_noise_get_vectorized( self.noise_c, self.algorithm, out.size, *c_input, ffi.from_buffer("float*", out), ) elif self.implementation == Implementation.FBM: 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 == 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: ArrayLike) -> NDArray[np.float32]: """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: A mesh-grid array of points to sample. A contiguous array of type `numpy.float32` is preferred. Returns: 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: msg = f"mgrid.shape[0] must equal self.dimensions, {mgrid.shape!r}[0] != {self.dimensions!r}" raise ValueError(msg) out: np.ndarray[Any, np.dtype[np.float32]] = np.ndarray(mgrid.shape[1:], np.float32) if mgrid.shape[1:] != out.shape: msg = f"mgrid.shape[1:] must equal out.shape, {mgrid.shape!r}[1:] != {out.shape!r}" raise ValueError(msg) 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: Sequence[ArrayLike]) -> NDArray[np.float32]: """Sample an open mesh-grid array and return the result. Args: ogrid: An open mesh-grid. Returns: 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: msg = f"len(ogrid) must equal self.dimensions, {len(ogrid)!r} != {self.dimensions!r}" raise ValueError(msg) ogrids: list[NDArray[np.float32]] = [np.ascontiguousarray(array, np.float32) for array in ogrid] out: np.ndarray[Any, np.dtype[np.float32]] = 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) -> dict[str, Any]: state = self.__dict__.copy() if self.dimensions < 4 and self.noise_c.waveletTileData == ffi.NULL: # noqa: PLR2004 # 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: dict[str, 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) return None def _setstate_old(self, state: tuple[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]))
[docs] def grid( shape: tuple[int, ...], scale: tuple[float, ...] | float, origin: tuple[int, ...] | None = None, indexing: Literal["ij", "xy"] = "xy", ) -> tuple[NDArray[Any], ...]: """Generate a mesh-grid of sample points to use with noise sampling. Args: shape: The shape of the grid. This can be any number of dimensions, but :class:`Noise` classes only support up to 4. scale: The step size between samples. This can be a single float, or it can be a tuple of floats with one float for each axis in `shape`. A lower scale gives smoother transitions between noise values. origin: The position of the first sample. If `None` then the `origin` will be zero on each axis. `origin` is not scaled by the `scale` parameter. indexing: Passed to :any:`numpy.meshgrid`. Returns: A sparse mesh-grid to be passed into a :class:`Noise` instance. Example:: >>> noise = tcod.noise.Noise(dimensions=2, seed=42) # Common case for ij-indexed arrays. >>> noise[tcod.noise.grid(shape=(3, 5), scale=0.25, indexing="ij")] array([[ 0. , -0.27523372, -0.40398532, -0.50773406, -0.64945626], [-0.55046356, -0.7205134 , -0.57662135, -0.2643614 , -0.12529983], [-0.76072866, -0.74057037, -0.33160293, 0.24446318, 0.5346834 ]], dtype=float32) # Transpose an xy-indexed array to get a standard order="F" result. >>> noise[tcod.noise.grid(shape=(4, 5), scale=(0.5, 0.25), origin=(1.0, 1.0))].T array([[ 0.52655405, 0.25038874, -0.03488023, -0.18455243, -0.16333057], [-0.5037453 , -0.75348294, -0.73630923, -0.35063767, 0.18149695], [-0.81221616, -0.6379566 , -0.12449139, 0.4495706 , 0.7547447 ], [-0.7057655 , -0.5817767 , -0.22774395, 0.02399864, -0.07006818]], dtype=float32) .. versionadded:: 12.2 """ if isinstance(scale, float): scale = (scale,) * len(shape) if origin is None: origin = (0,) * len(shape) if len(shape) != len(scale): msg = "shape must have the same length as scale" raise TypeError(msg) if len(shape) != len(origin): msg = "shape must have the same length as origin" raise TypeError(msg) indexes = (np.arange(i_shape) * i_scale + i_origin for i_shape, i_scale, i_origin in zip(shape, scale, origin)) return tuple(np.meshgrid(*indexes, copy=False, sparse=True, indexing=indexing))