worldgen

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

master

raw · 4988 bytes

"""Stage `crust`: continental vs oceanic crust, age classes, oceanic age."""
from __future__ import annotations

import numpy as np

from .config import params
from . import plateaus as PL
from .graph import distance_to, smooth_km
from .noise import fbm, name_seed, ridged
from .plates import CONV, DIV
from .sphere import azimuth_deg, gc_dist_km, latlon_to_xyz

OCEANIC, CRATON, PRE_OROGEN, POST_OROGEN, RIFT, LIP, SCAR, VOLCANO = range(8)
AGE_NAMES = ["oceanic", "craton (3g era)", "pre-Lightening orogen", "post-Lightening orogen",
             "rift", "Lightening basalt province", "collapse scar", "overshoot volcano"]
DEFAULTS = {"continental_threshold": 0.25, "shelf_km": 450.0, "edge_noise": 0.5, "edge_noise_freq": 4.0, "edge_noise_gain": 0.65, "orogen_width_km": 700.0, "rift_width_km": 200.0,
            "pre_orogen_threshold": 0.72, "half_spreading_km_myr": 30.0, "max_ocean_age_myr": 200.0,
            "scar_inner": 0.4, "scar_outer": 1.6, "scar_halfwidth_deg": 25.0}


def center_dist(g, center):
    return gc_dist_km(g.xyz, latlon_to_xyz(*center), g.radius_km)


def lip_profile(g, lip, seed):
    r = lip["radius_km"] * (1.0 + 0.25 * fbm(g.xyz, name_seed(seed, lip["name"]), 3, 6.0))
    x = center_dist(g, lip["center"]) / r
    return np.clip((1.0 - x) / 0.25, 0.0, 1.0)


def _scar_azimuths(v, seed):
    if "scar_azimuths" in v:
        return list(v["scar_azimuths"])
    rng = np.random.default_rng(name_seed(seed, v["name"]))
    return list(rng.uniform(0, 360, size=2))


def patch_field(g, patch: dict, seed: int):
    """[[land_patch]]: land hint (strength) over a disc whose radius varies ± 25 % · edge_noise (fractal edge)."""
    d = center_dist(g, patch["center"])
    r = float(patch["radius_km"])
    out = np.zeros(g.n)
    near = d < 1.3 * r
    if near.any():
        w = np.clip(2.0 * fbm(g.xyz[near], name_seed(seed, patch["name"]), 6, 12.0), -1.0, 1.0)
        out[near] = patch.get("strength", 1.0) * (d[near] < r * (1.0 + 0.25 * patch.get("edge_noise", 0.0) * w))
    return out


def run(ctx) -> dict:
    g = ctx.grid
    P = params(ctx.cfg, "crust", DEFAULTS)
    land, land_hint, btype = ctx.need("sk_land", "m_land_hint", "bnd_type")
    rough = (fbm(g.xyz, ctx.seed + 23, 7, P["edge_noise_freq"], gain=P["edge_noise_gain"])
             if P["edge_noise"] > 0 else None)

    def continental_of(raw):
        field = smooth_km(g, raw, P["shelf_km"])
        if rough is not None:     # fractal margins: bays, peninsulas, offshore continental fragments
            f = np.clip(field, 0.0, 1.0)
            near = 4.0 * f * (1.0 - f)                         # strongest at the margin; none deep inland / far offshore
            field = field + P["edge_noise"] * near * rough / max(float(rough.std()), 1e-12)
        else:
            field = np.maximum(field, (raw > 0.5).astype(float))
        cont = field > P["continental_threshold"]
        for m in ctx.tect.get("microcontinent", []):
            cont |= center_dist(g, m["center"]) < m["radius_km"]
        return cont

    raw = np.clip(land + 0.5 * land_hint, 0.0, 1.0)
    continental_base = continental_of(raw)                     # without land patches: its sea level is the world's
    patches = sum((patch_field(g, p, ctx.seed) for p in ctx.tect.get("land_patch", [])), np.zeros(g.n))
    continental = continental_of(np.clip(raw + patches, 0.0, 1.0)) if patches.any() else continental_base.copy()
    plateau_id = PL.cell_ids(g.xyz, ctx.tect.get("plateau", []), ctx.seed, g.radius_km)
    continental |= plateau_id >= 0                             # sunken plateaus: continental crust that never rose
    d_conv = distance_to(g, btype == CONV)
    d_div = distance_to(g, btype == DIV)
    age = np.full(g.n, CRATON, np.int8)
    age[continental & (ridged(g.xyz, ctx.seed + 21, 4, 4.0) > P["pre_orogen_threshold"])] = PRE_OROGEN
    age[continental & (d_div < P["rift_width_km"])] = RIFT
    age[continental & (d_conv < P["orogen_width_km"])] = POST_OROGEN
    age[~continental] = OCEANIC
    for lip in ctx.tect.get("lip", []):
        age[lip_profile(g, lip, ctx.seed) > 0] = LIP
    for v in ctx.tect.get("volcano", []):
        d = center_dist(g, v["center"])
        r = v["radius_km"]
        age[d < r] = VOLCANO
        az = azimuth_deg(latlon_to_xyz(*v["center"]), g.xyz)
        for a in _scar_azimuths(v, ctx.seed):
            diff = np.abs((az - a + 180.0) % 360.0 - 180.0)
            age[(diff < P["scar_halfwidth_deg"]) & (d > P["scar_inner"] * r) & (d < P["scar_outer"] * r)] = SCAR
    oceanic_age = np.minimum(d_div / P["half_spreading_km_myr"], P["max_ocean_age_myr"])
    ocean_age = np.where(continental & (plateau_id < 0), 0.0, oceanic_age)   # plateaus: the floor around them
    return {"continental": continental, "age_class": age, "ocean_age_myr": ocean_age.astype(np.float32),
            "d_conv_km": d_conv, "d_div_km": d_div, "continental_base": continental_base, "plateau_id": plateau_id,
            "land_patch": patches.astype(np.float32)}