worldgen

git clone https://git.godosa.eu/worldgen

master

raw ยท 3904 bytes

"""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