#!/usr/bin/env python3
"""
MOUNTAIN-LAYER SWEEP — what gain and octave count let the erosion filter work?
hyper-games #46. The filter is a faithful port of runevision's, and the
reference feeds it ONE near-smooth fBm: freq 3, 3 octaves, gain 0.1, lac 2.0,
amplitude 0.125 (lpmitchell/AdvancedTerrainErosion README, and the same config
in the Shadertoy lineage). dc-terrain adds a mountain layer on top of that at
8 octaves, gain 0.68, lacunarity 1.87 -- gain*lac = 1.27, so the input's slope
GROWS 27% per octave and the finest octave dominates it. The filter then puts
gullies on every bump at every scale.
CRITERION, so this is not eyeballing:
1. gain * lacunarity, which must be <= 1 for the input's slope not to grow
with frequency. Arithmetic, not opinion.
2. the per-octave local power-law exponent beta of the RESULT, measured by
the #46 §4 instrument. A generator with a drainage hierarchy STEEPENS
down the column (terrain-diffusion does: 4.18 -> 4.69 -> 5.10). One that
is texture at every scale stays flat (ours shipped at 3.58/3.44/3.78).
Both baselines are rendered here too, so the column is read against something
rather than in the abstract.
"""
import sys, urllib.parse, urllib.request
import numpy as np
sys.path.insert(0, "/storage/projects/code/godot/terrain-lookdev")
from spectrum_probe import radial_power_spectrum, selftest
LOOKDEV = "http://127.0.0.1:8818"
N, MPP = 512, 30.0
GEOM = dict(n=N, mpp=MPP, ox=120000, oz=40000, steps=256, voxel_size=2 * MPP)
OCTAVE_BANDS = [(64, 128), (128, 256)] # 120-240 m, 60-120 m at this geometry
FIT = (16, 64)
def fetch(kind, **kw):
q = urllib.parse.urlencode(kw)
with urllib.request.urlopen(f"{LOOKDEV}/api/{kind}.bin?{q}", timeout=900) as r:
n = int(r.headers["X-N"]); mpp = float(r.headers["X-Mpp"])
buf = r.read()
return np.frombuffer(buf, dtype="<f4").reshape(n, n).astype(np.float64), mpp
def betas(h, mpp):
"""Local beta per octave, plus the sd of the field. No extrapolation."""
k, p = radial_power_spectrum(h)
out = []
for lo, hi in [FIT] + OCTAVE_BANDS:
m = (k > lo) & (k <= hi) & (p > 0)
if m.sum() < 4:
out.append(float("nan")); continue
s, _ = np.polyfit(np.log10(k[m]), np.log10(p[m]), 1)
out.append(-s)
return out
def row(label, h, mpp, note=""):
b = betas(h, mpp)
trend = b[2] - b[1] # positive = steepens toward fine = hierarchy
print(f" {label:<26} {h.std():7.0f} {b[0]:7.2f} {b[1]:7.2f} {b[2]:7.2f} "
f"{trend:+7.2f} {note}")
return b, trend
def main():
print("INSTRUMENT SELF-TEST")
if not selftest(verbose=False):
print(" FAILED"); return 2
print(" all pass\n")
print(f"{N}x{N} @ {MPP:g} m/px = {N*MPP/1000:.2f} km, voxel {GEOM['voxel_size']:g} m "
f"(band-limited to the sampling)")
print(f"beta measured locally inside each band. 'trend' = beta(60-120m) - beta(120-240m);")
print(f"POSITIVE means it steepens toward fine scales, i.e. large forms dominate.\n")
hdr = f" {'config':<26} {'sd m':>7} {'0.24-1km':>7} {'120-240':>7} {'60-120':>7} {'trend':>7}"
print(hdr); print(" " + "-" * (len(hdr) - 2))
# --- baselines ---------------------------------------------------------
hd, _ = fetch("diffusion", n=N, i1=0, j1=2048)
row("terrain-diffusion", hd, 30.0, "<- the target character")
hr, _ = fetch("erosion", **GEOM, mountain_height=0)
row("erosion, NO mountain", hr, MPP, "<- ~runevision's own config")
hs, _ = fetch("erosion", **GEOM)
row("SHIPPING (gain .68, oct 8)", hs, MPP, "<- gain*lac = 1.27")
print()
# --- gain sweep at 8 octaves ------------------------------------------
print(" gain sweep, mountain_octaves = 8")
best = None
for g in (0.60, 0.5348, 0.45, 0.35, 0.25, 0.15, 0.10):
h, _ = fetch("erosion", **GEOM, mountain_gain=g)
b, t = row(f" gain {g:.4f}", h, MPP, f"gain*lac {g*1.87:.2f}")
if best is None or t > best[1]:
best = (g, t)
print(f"\n best trend at gain {best[0]:.4f} ({best[1]:+.2f})\n")
# --- octave sweep at that gain ----------------------------------------
print(f" octave sweep, mountain_gain = {best[0]:.4f}")
for o in (8, 6, 5, 4, 3):
h, _ = fetch("erosion", **GEOM, mountain_gain=best[0], mountain_octaves=o)
row(f" octaves {o}", h, MPP)
# --- the reference shape, transplanted --------------------------------
print("\n runevision's own input shape on our mountain layer")
for g, o in ((0.10, 3), (0.15, 3), (0.25, 3), (0.25, 4), (0.35, 4)):
h, _ = fetch("erosion", **GEOM, mountain_gain=g, mountain_octaves=o)
row(f" gain {g:.2f} oct {o}", h, MPP, f"gain*lac {g*1.87:.2f}")
return 0
if __name__ == "__main__":
sys.exit(main())