| … | … | @@ -246,6 +246,24 @@ static void set_param(ErosionWrapperParams& p, const char* k, const char* v) { |
| 246 | 246 | else if (!strcmp(k, "en_gain")) p.en_gain = f; |
| 247 | 247 | else if (!strcmp(k, "en_octaves")) p.en_octaves = i; |
| 248 | 248 | else if (!strcmp(k, "en_erosion_scale")) p.en_erosion_scale = f; |
| 249 | else if (!strcmp(k, "en_fade_div")) p.en_fade_div = f; | |
| 250 | else if (!strcmp(k, "en_height_amp")) p.en_height_amp = f; | |
| 251 | else if (!strcmp(k, "en_erosion_lac")) p.en_erosion_lac = f; | |
| 252 | else if (!strcmp(k, "en_gully_weight")) p.en_gully_weight = f; | |
| 253 | else if (!strcmp(k, "en_detail")) p.en_detail = f; | |
| 254 | else if (!strcmp(k, "en_normalization")) p.en_normalization = f; | |
| 255 | else if (!strcmp(k, "en_height_offset_a")) p.en_height_offset_a = f; | |
| 256 | else if (!strcmp(k, "en_height_offset_b")) p.en_height_offset_b = f; | |
| 257 | else if (!strcmp(k, "rounding_ridges")) p.rounding_ridges = f; | |
| 258 | else if (!strcmp(k, "rounding_creases")) p.rounding_creases = f; | |
| 259 | else if (!strcmp(k, "rounding_input")) p.rounding_input = f; | |
| 260 | else if (!strcmp(k, "rounding_per_oct")) p.rounding_per_oct = f; | |
| 261 | else if (!strcmp(k, "onset_input")) p.onset_input = f; | |
| 262 | else if (!strcmp(k, "onset_per_oct")) p.onset_per_oct = f; | |
| 263 | else if (!strcmp(k, "onset_ridge_input")) p.onset_ridge_input = f; | |
| 264 | else if (!strcmp(k, "onset_ridge_oct")) p.onset_ridge_oct = f; | |
| 265 | else if (!strcmp(k, "assumed_slope_mag")) p.assumed_slope_mag = f; | |
| 266 | else if (!strcmp(k, "assumed_slope_mix")) p.assumed_slope_mix = f; | |
| 249 | 267 | else { fprintf(stderr, "unknown parameter '%s'\n", k); exit(2); } |
| 250 | 268 | } |
| 251 | 269 |
| … | … | @@ -106,6 +106,17 @@ kbd{font-family:"IBM Plex Mono",monospace;background:#0e1216;border:1px solid va |
| 106 | 106 | <fieldset><legend>Detail + base noise</legend> |
| 107 | 107 | <div id="grp_detail"></div> |
| 108 | 108 | </fieldset> |
| 109 | <fieldset><legend>Filter shape · baked in the engine</legend> | |
| 110 | <p class="note" style="margin:0 0 4px;border:0;padding:0">These are constants on | |
| 111 | the engine path — <code>GpuCompiler</code>'s push-constant block is seven floats | |
| 112 | and rounding plus onset need eight. This path emits GLSL text, so it has no such | |
| 113 | cap. <b>rounding input</b> is runevision's input-feature-size control: <i>"the | |
| 114 | erosion function doesn't know about the sizes of the terrain features coming | |
| 115 | from [the input height function], so an input value is provided for tweaking | |
| 116 | the rounding of those."</i> It has been pinned at 0.1 while terrain_height went | |
| 117 | 20 km → 60 km.</p> | |
| 118 | <div id="grp_shape"></div> | |
| 119 | </fieldset> | |
| 109 | 120 | |
| 110 | 121 | <p class="note">Both panels are shaded by the same code, so a difference on |
| 111 | 122 | screen is a difference in the terrain and not in the renderer. |
| … | … | @@ -162,7 +173,25 @@ const GROUPS = { |
| 162 | 173 | ["detail_gain",0.5,0.2,0.9,0.01],["detail_octaves",4,1,10,1], |
| 163 | 174 | ["noise_lacunarity",1.87,1.2,3,0.01],["gn_scale",3,0.1,40,0.1], |
| 164 | 175 | ["gn_gain",0.1,0,0.9,0.01],["gn_detail",3,1,8,1], |
| 165 | ["voxel_size",1,0.25,512,0.25]] | |
| 176 | ["voxel_size",1,0.25,512,0.25]], | |
| 177 | shape: [["en_fade_div",4.8,0.5,40.0,0.1], | |
| 178 | ["en_height_amp",0.25,0.02,2.0,0.01], | |
| 179 | ["en_erosion_lac",2.0,1.2,4.0,0.01], | |
| 180 | ["en_gully_weight",0.5,0.0,2.0,0.01], | |
| 181 | ["en_detail",1.5,0.2,5.0,0.05], | |
| 182 | ["en_normalization",0.5,0.0,1.0,0.01], | |
| 183 | ["en_height_offset_a",-0.65,-2.0,1.0,0.01], | |
| 184 | ["en_height_offset_b",0.0,0.0,1.0,0.01], | |
| 185 | ["rounding_ridges",0.1,0.0,2.0,0.005], | |
| 186 | ["rounding_creases",0.0,0.0,2.0,0.005], | |
| 187 | ["rounding_input",0.1,0.005,4.0,0.005], | |
| 188 | ["rounding_per_oct",2.0,0.25,6.0,0.05], | |
| 189 | ["onset_input",1.25,0.0,6.0,0.05], | |
| 190 | ["onset_per_oct",1.25,0.0,6.0,0.05], | |
| 191 | ["onset_ridge_input",2.8,0.0,8.0,0.05], | |
| 192 | ["onset_ridge_oct",1.5,0.0,6.0,0.05], | |
| 193 | ["assumed_slope_mag",0.7,0.0,3.0,0.01], | |
| 194 | ["assumed_slope_mix",1.0,0.0,1.0,0.01]] | |
| 166 | 195 | }; |
| 167 | 196 | const $ = id => document.getElementById(id); |
| 168 | 197 | let view = "shaded-height"; |
| … | … | @@ -59,6 +59,27 @@ KNOBS = { |
| 59 | 59 | "en_gain": ("f", 0.2, 0.9), |
| 60 | 60 | "en_octaves": ("i", 1, 12), |
| 61 | 61 | "en_erosion_scale": ("f", 0.002, 0.5), |
| 62 | # Baked on the engine path (7-float push-constant cap); reachable here | |
| 63 | # because this path emits GLSL text. rounding_input is runevision's | |
| 64 | # input-feature-size control. | |
| 65 | "en_fade_div": ("f", 0.5, 40.0), | |
| 66 | "en_height_amp": ("f", 0.02, 2.0), | |
| 67 | "en_erosion_lac": ("f", 1.2, 4.0), | |
| 68 | "en_gully_weight": ("f", 0.0, 2.0), | |
| 69 | "en_detail": ("f", 0.2, 5.0), | |
| 70 | "en_normalization": ("f", 0.0, 1.0), | |
| 71 | "en_height_offset_a": ("f", -2.0, 1.0), | |
| 72 | "en_height_offset_b": ("f", 0.0, 1.0), | |
| 73 | "rounding_ridges": ("f", 0.0, 2.0), | |
| 74 | "rounding_creases": ("f", 0.0, 2.0), | |
| 75 | "rounding_input": ("f", 0.005, 4.0), | |
| 76 | "rounding_per_oct": ("f", 0.25, 6.0), | |
| 77 | "onset_input": ("f", 0.0, 6.0), | |
| 78 | "onset_per_oct": ("f", 0.0, 6.0), | |
| 79 | "onset_ridge_input": ("f", 0.0, 8.0), | |
| 80 | "onset_ridge_oct": ("f", 0.0, 6.0), | |
| 81 | "assumed_slope_mag": ("f", 0.0, 3.0), | |
| 82 | "assumed_slope_mix": ("f", 0.0, 1.0), | |
| 62 | 83 | } |
| 63 | 84 | |
| 64 | 85 | NO_SURFACE = -3.0e38 |
| … | … | @@ -0,0 +1,113 @@ |
| 1 | #!/usr/bin/env python3 | |
| 2 | """ | |
| 3 | MOUNTAIN-LAYER SWEEP — what gain and octave count let the erosion filter work? | |
| 4 | ||
| 5 | hyper-games #46. The filter is a faithful port of runevision's, and the | |
| 6 | reference feeds it ONE near-smooth fBm: freq 3, 3 octaves, gain 0.1, lac 2.0, | |
| 7 | amplitude 0.125 (lpmitchell/AdvancedTerrainErosion README, and the same config | |
| 8 | in the Shadertoy lineage). dc-terrain adds a mountain layer on top of that at | |
| 9 | 8 octaves, gain 0.68, lacunarity 1.87 -- gain*lac = 1.27, so the input's slope | |
| 10 | GROWS 27% per octave and the finest octave dominates it. The filter then puts | |
| 11 | gullies on every bump at every scale. | |
| 12 | ||
| 13 | CRITERION, so this is not eyeballing: | |
| 14 | ||
| 15 | 1. gain * lacunarity, which must be <= 1 for the input's slope not to grow | |
| 16 | with frequency. Arithmetic, not opinion. | |
| 17 | 2. the per-octave local power-law exponent beta of the RESULT, measured by | |
| 18 | the #46 §4 instrument. A generator with a drainage hierarchy STEEPENS | |
| 19 | down the column (terrain-diffusion does: 4.18 -> 4.69 -> 5.10). One that | |
| 20 | is texture at every scale stays flat (ours shipped at 3.58/3.44/3.78). | |
| 21 | ||
| 22 | Both baselines are rendered here too, so the column is read against something | |
| 23 | rather than in the abstract. | |
| 24 | """ | |
| 25 | import sys, urllib.parse, urllib.request | |
| 26 | import numpy as np | |
| 27 | sys.path.insert(0, "/storage/projects/code/godot/terrain-lookdev") | |
| 28 | from spectrum_probe import radial_power_spectrum, selftest | |
| 29 | ||
| 30 | LOOKDEV = "http://127.0.0.1:8818" | |
| 31 | N, MPP = 512, 30.0 | |
| 32 | GEOM = dict(n=N, mpp=MPP, ox=120000, oz=40000, steps=256, voxel_size=2 * MPP) | |
| 33 | OCTAVE_BANDS = [(64, 128), (128, 256)] # 120-240 m, 60-120 m at this geometry | |
| 34 | FIT = (16, 64) | |
| 35 | ||
| 36 | ||
| 37 | def fetch(kind, **kw): | |
| 38 | q = urllib.parse.urlencode(kw) | |
| 39 | with urllib.request.urlopen(f"{LOOKDEV}/api/{kind}.bin?{q}", timeout=900) as r: | |
| 40 | n = int(r.headers["X-N"]); mpp = float(r.headers["X-Mpp"]) | |
| 41 | buf = r.read() | |
| 42 | return np.frombuffer(buf, dtype="<f4").reshape(n, n).astype(np.float64), mpp | |
| 43 | ||
| 44 | ||
| 45 | def betas(h, mpp): | |
| 46 | """Local beta per octave, plus the sd of the field. No extrapolation.""" | |
| 47 | k, p = radial_power_spectrum(h) | |
| 48 | out = [] | |
| 49 | for lo, hi in [FIT] + OCTAVE_BANDS: | |
| 50 | m = (k > lo) & (k <= hi) & (p > 0) | |
| 51 | if m.sum() < 4: | |
| 52 | out.append(float("nan")); continue | |
| 53 | s, _ = np.polyfit(np.log10(k[m]), np.log10(p[m]), 1) | |
| 54 | out.append(-s) | |
| 55 | return out | |
| 56 | ||
| 57 | ||
| 58 | def row(label, h, mpp, note=""): | |
| 59 | b = betas(h, mpp) | |
| 60 | trend = b[2] - b[1] # positive = steepens toward fine = hierarchy | |
| 61 | print(f" {label:<26} {h.std():7.0f} {b[0]:7.2f} {b[1]:7.2f} {b[2]:7.2f} " | |
| 62 | f"{trend:+7.2f} {note}") | |
| 63 | return b, trend | |
| 64 | ||
| 65 | ||
| 66 | def main(): | |
| 67 | print("INSTRUMENT SELF-TEST") | |
| 68 | if not selftest(verbose=False): | |
| 69 | print(" FAILED"); return 2 | |
| 70 | print(" all pass\n") | |
| 71 | ||
| 72 | print(f"{N}x{N} @ {MPP:g} m/px = {N*MPP/1000:.2f} km, voxel {GEOM['voxel_size']:g} m " | |
| 73 | f"(band-limited to the sampling)") | |
| 74 | print(f"beta measured locally inside each band. 'trend' = beta(60-120m) - beta(120-240m);") | |
| 75 | print(f"POSITIVE means it steepens toward fine scales, i.e. large forms dominate.\n") | |
| 76 | hdr = f" {'config':<26} {'sd m':>7} {'0.24-1km':>7} {'120-240':>7} {'60-120':>7} {'trend':>7}" | |
| 77 | print(hdr); print(" " + "-" * (len(hdr) - 2)) | |
| 78 | ||
| 79 | # --- baselines --------------------------------------------------------- | |
| 80 | hd, _ = fetch("diffusion", n=N, i1=0, j1=2048) | |
| 81 | row("terrain-diffusion", hd, 30.0, "<- the target character") | |
| 82 | hr, _ = fetch("erosion", **GEOM, mountain_height=0) | |
| 83 | row("erosion, NO mountain", hr, MPP, "<- ~runevision's own config") | |
| 84 | hs, _ = fetch("erosion", **GEOM) | |
| 85 | row("SHIPPING (gain .68, oct 8)", hs, MPP, "<- gain*lac = 1.27") | |
| 86 | print() | |
| 87 | ||
| 88 | # --- gain sweep at 8 octaves ------------------------------------------ | |
| 89 | print(" gain sweep, mountain_octaves = 8") | |
| 90 | best = None | |
| 91 | for g in (0.60, 0.5348, 0.45, 0.35, 0.25, 0.15, 0.10): | |
| 92 | h, _ = fetch("erosion", **GEOM, mountain_gain=g) | |
| 93 | b, t = row(f" gain {g:.4f}", h, MPP, f"gain*lac {g*1.87:.2f}") | |
| 94 | if best is None or t > best[1]: | |
| 95 | best = (g, t) | |
| 96 | print(f"\n best trend at gain {best[0]:.4f} ({best[1]:+.2f})\n") | |
| 97 | ||
| 98 | # --- octave sweep at that gain ---------------------------------------- | |
| 99 | print(f" octave sweep, mountain_gain = {best[0]:.4f}") | |
| 100 | for o in (8, 6, 5, 4, 3): | |
| 101 | h, _ = fetch("erosion", **GEOM, mountain_gain=best[0], mountain_octaves=o) | |
| 102 | row(f" octaves {o}", h, MPP) | |
| 103 | ||
| 104 | # --- the reference shape, transplanted -------------------------------- | |
| 105 | print("\n runevision's own input shape on our mountain layer") | |
| 106 | for g, o in ((0.10, 3), (0.15, 3), (0.25, 3), (0.25, 4), (0.35, 4)): | |
| 107 | h, _ = fetch("erosion", **GEOM, mountain_gain=g, mountain_octaves=o) | |
| 108 | row(f" gain {g:.2f} oct {o}", h, MPP, f"gain*lac {g*1.87:.2f}") | |
| 109 | return 0 | |
| 110 | ||
| 111 | ||
| 112 | if __name__ == "__main__": | |
| 113 | sys.exit(main()) |