diff options
Diffstat (limited to 'mapgen/noise.py')
| -rw-r--r-- | mapgen/noise.py | 111 |
1 files changed, 111 insertions, 0 deletions
diff --git a/mapgen/noise.py b/mapgen/noise.py new file mode 100644 index 0000000..e52b7f8 --- /dev/null +++ b/mapgen/noise.py @@ -0,0 +1,111 @@ +"""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 |
