"""Seeded, vectorized 3D value noise and fractal sums on points (N, 3).""" from __future__ import annotations import zlib import numpy as np _MASK = (1 << 64) - 1 _K1 = np.uint64(0x9E3779B185EBCA87) _K2 = np.uint64(0xC2B2AE3D27D4EB4F) _K3 = np.uint64(0x165667B19E3779F9) _M1 = np.uint64(0x94D049BB133111EB) def _hash(ix, iy, iz, seed: int): s = np.uint64((seed * 0x27D4EB2F165667C5 + 0x632BE59BD9B4E019) & _MASK) h = ix.astype(np.uint64) * _K1 ^ iy.astype(np.uint64) * _K2 ^ iz.astype(np.uint64) * _K3 ^ s h ^= h >> np.uint64(31) h *= _M1 h ^= h >> np.uint64(29) return (h >> np.uint64(11)).astype(np.float64) / float(1 << 53) * 2.0 - 1.0 def value_noise(p, seed: int): if _noise_jit is not None: p = np.asarray(p) if p.dtype == np.float64 and p.ndim == 2 and p.shape[1] == 3: s = np.uint64((seed * 0x27D4EB2F165667C5 + 0x632BE59BD9B4E019) & _MASK) return _noise_jit(np.ascontiguousarray(p), s) return _value_noise(p, seed) def _value_noise(p, seed: int): f = np.floor(p) i = f.astype(np.int64) t = p - f t = t * t * (3.0 - 2.0 * t) out = np.zeros(len(p)) for dx in (0, 1): wx = t[:, 0] if dx else 1.0 - t[:, 0] for dy in (0, 1): wy = t[:, 1] if dy else 1.0 - t[:, 1] for dz in (0, 1): wz = t[:, 2] if dz else 1.0 - t[:, 2] out += wx * wy * wz * _hash(i[:, 0] + dx, i[:, 1] + dy, i[:, 2] + dz, seed) return out def _make_noise_jit(): """value_noise compiled (numba optional): per point the same integer hash and the same float steps in the same order, so the same values; one call costs microseconds instead of a dozen numpy passes (river meanders call it on a few points at a time). WORLDGEN_NO_JIT=1 turns it off.""" import os if os.environ.get("WORLDGEN_NO_JIT"): return None try: import numba except ImportError: return None K1, K2, K3, M1 = _K1, _K2, _K3, _M1 @numba.njit(cache=True) def noise(p, s): n = p.shape[0] out = np.empty(n) for k in range(n): fx, fy, fz = np.floor(p[k, 0]), np.floor(p[k, 1]), np.floor(p[k, 2]) ix, iy, iz = np.int64(fx), np.int64(fy), np.int64(fz) tx, ty, tz = p[k, 0] - fx, p[k, 1] - fy, p[k, 2] - fz tx = tx * tx * (3.0 - 2.0 * tx) ty = ty * ty * (3.0 - 2.0 * ty) tz = tz * tz * (3.0 - 2.0 * tz) acc = 0.0 for dx in range(2): wx = tx if dx else 1.0 - tx for dy in range(2): wy = ty if dy else 1.0 - ty for dz in range(2): wz = tz if dz else 1.0 - tz h = (np.uint64(ix + dx) * K1) ^ (np.uint64(iy + dy) * K2) ^ (np.uint64(iz + dz) * K3) ^ s h ^= h >> np.uint64(31) h *= M1 h ^= h >> np.uint64(29) v = np.float64(h >> np.uint64(11)) / 9007199254740992.0 * 2.0 - 1.0 acc += wx * wy * wz * v out[k] = acc return out return noise _noise_jit = _make_noise_jit() def fbm(xyz, seed: int, octaves: int = 5, freq: float = 2.0, lacunarity: float = 2.0, gain: float = 0.5): total = np.zeros(len(xyz)) amp, norm, f = 1.0, 0.0, freq for o in range(octaves): total += amp * value_noise(xyz * f, seed + 1013 * o) norm += amp amp *= gain f *= lacunarity return total / norm def ridged(xyz, seed: int, octaves: int = 5, freq: float = 2.0): return 1.0 - np.abs(fbm(xyz, seed, octaves, freq)) def name_seed(seed: int, name: str) -> int: """A per-feature noise seed: the build seed plus a hash of the feature's name (stable across runs).""" return seed + zlib.crc32(name.encode()) % 100000