| … | … | @@ -23,6 +23,12 @@ label{display:grid;grid-template-columns:1fr auto;gap:2px 8px;margin:9px 0; |
| 23 | 23 | label .v{font-family:"IBM Plex Mono",ui-monospace,monospace;color:var(--accent); |
| 24 | 24 | font-size:11px;font-variant-numeric:tabular-nums} |
| 25 | 25 | input[type=range]{grid-column:1/-1;width:100%;accent-color:var(--accent);height:16px} |
| 26 | .d{grid-column:1/-1;font-family:"IBM Plex Mono",ui-monospace,monospace;font-size:10px; |
| 27 | color:var(--dim);line-height:1.35;margin-top:-2px} |
| 28 | .d.warn{color:var(--warn)} |
| 29 | #bands{font-family:"IBM Plex Mono",ui-monospace,monospace;font-size:10.5px; |
| 30 | color:var(--dim);white-space:pre;line-height:1.5} |
| 31 | #bands .warn{color:var(--warn)} |
| 26 | 32 | input[type=number],select{background:#0e1216;color:var(--ink);border:1px solid var(--edge2); |
| 27 | 33 | border-radius:3px;padding:3px 5px;font:inherit;font-size:11.5px;width:100%} |
| 28 | 34 | button{background:#22303a;color:var(--ink);border:1px solid var(--edge2);border-radius:3px; |
| … | … | @@ -93,10 +99,21 @@ kbd{font-family:"IBM Plex Mono",monospace;background:#0e1216;border:1px solid va |
| 93 | 99 | <label>Erosion origin Z (m)<input type="number" id="oz" value="0" step="1000"></label> |
| 94 | 100 | <label>Diffusion i1 (px)<input type="number" id="i1" value="0" step="256"></label> |
| 95 | 101 | <label>Diffusion j1 (px)<input type="number" id="j1" value="2048" step="256"></label> |
| 102 | <label>Diffusion seed<input type="number" id="seed" value="1234" step="1"> |
| 103 | <span class="d">a new seed is a whole new world. it rebuilds the pipeline and |
| 104 | clears every cached tile, so it is much slower than moving i1/j1.</span></label> |
| 105 | <div class="row"> |
| 106 | <button id="newloc">New location</button> |
| 107 | <button id="newseed">New seed</button> |
| 108 | </div> |
| 96 | 109 | <label id="exagrow" hidden>3D vertical exaggeration<span class="v" id="v_exag">1</span> |
| 97 | 110 | <input type="range" id="exag" min="0.2" max="6" step="0.1" value="1"></label> |
| 98 | 111 | </fieldset> |
| 99 | 112 | |
| 113 | <fieldset><legend>Bands vs the view</legend> |
| 114 | <div id="bands"></div> |
| 115 | </fieldset> |
| 116 | |
| 100 | 117 | <fieldset><legend>Mountain layer</legend> |
| 101 | 118 | <div id="grp_mountain"></div> |
| 102 | 119 | </fieldset> |
| … | … | @@ -194,6 +211,100 @@ const GROUPS = { |
| 194 | 211 | ["assumed_slope_mix",1.0,0.0,1.0,0.01]] |
| 195 | 212 | }; |
| 196 | 213 | const $ = id => document.getElementById(id); |
| 214 | |
| 215 | // The panel used to show raw engine values, which are close to meaningless on |
| 216 | // their own: terrain_height is a HORIZONTAL scale despite its name, every |
| 217 | // wavelength is terrain_height divided (or multiplied) by another knob, and |
| 218 | // detail_scale is inverse. Each control now carries the metres it actually |
| 219 | // produces. |
| 220 | const LABELS = { |
| 221 | terrain_height: "world scale (horizontal — sets every wavelength)", |
| 222 | mountain_scale: "mountain scale", |
| 223 | detail_scale: "detail scale (inverse)", |
| 224 | gn_scale: "base-noise scale (inverse)", |
| 225 | relief_scale: "relief scale (inverse)", |
| 226 | en_erosion_scale: "erosion scale", |
| 227 | rounding_input: "rounding input (input feature size)", |
| 228 | assumed_slope_mag:"assumed slope magnitude", |
| 229 | assumed_slope_mix:"assumed slope mix", |
| 230 | }; |
| 231 | |
| 232 | const num = k => +$(k).value; |
| 233 | function km(m) { |
| 234 | if (!isFinite(m)) return "—"; |
| 235 | return m >= 1000 ? `${(m/1000).toFixed(m >= 10000 ? 0 : 1)} km` : `${m.toFixed(0)} m`; |
| 236 | } |
| 237 | // A band as "coarsest -> finest over N octaves". |
| 238 | function band(l0, lac, oct) { |
| 239 | const lmin = l0 / Math.pow(lac, Math.max(0, oct - 1)); |
| 240 | return `${km(l0)} \u2192 ${km(lmin)} (${oct} oct, lac ${lac.toFixed(2)})`; |
| 241 | } |
| 242 | // gain*lacunarity > 1 means each octave adds MORE slope than the last, so the |
| 243 | // finest octave dominates and large forms disappear. This is the single number |
| 244 | // that was wrong in the shipping mountain layer (0.68 * 1.87 = 1.27). |
| 245 | function gl(gain, lac) { |
| 246 | const r = gain * lac; |
| 247 | return {txt: `gain \u00d7 lac = ${r.toFixed(2)}` + |
| 248 | (r > 1 ? " \u2014 finest octave dominates" : ""), warn: r > 1}; |
| 249 | } |
| 250 | |
| 251 | function derived(k) { |
| 252 | const TH = num("terrain_height"), LAC = num("noise_lacunarity"); |
| 253 | switch (k) { |
| 254 | case "terrain_height": return {txt: `all wavelengths are ${km(TH)} / (their scale knob)`}; |
| 255 | case "mountain_scale": return {txt: band(TH / num("mountain_scale"), LAC, num("mountain_octaves"))}; |
| 256 | case "mountain_octaves": return {txt: band(TH / num("mountain_scale"), LAC, num("mountain_octaves"))}; |
| 257 | case "mountain_gain": return gl(num("mountain_gain"), LAC); |
| 258 | case "relief_scale": return {txt: num("relief_scale") > 0 |
| 259 | ? `\u03bb ${km(TH / num("relief_scale"))}, 2 oct` |
| 260 | : "off"}; |
| 261 | case "detail_scale": return {txt: num("detail_scale") > 0 |
| 262 | ? band(TH / num("detail_scale"), LAC, num("detail_octaves")) |
| 263 | : "off"}; |
| 264 | case "detail_octaves": return {txt: band(TH / num("detail_scale"), LAC, num("detail_octaves"))}; |
| 265 | case "detail_gain": return gl(num("detail_gain"), LAC); |
| 266 | case "gn_scale": return {txt: band(TH / num("gn_scale"), 2.0, num("gn_detail"))}; |
| 267 | case "gn_detail": return {txt: band(TH / num("gn_scale"), 2.0, num("gn_detail"))}; |
| 268 | case "gn_gain": return gl(num("gn_gain"), 2.0); |
| 269 | case "noise_lacunarity": return {txt: "octave spacing for the mountain and detail layers"}; |
| 270 | case "en_erosion_scale": |
| 271 | case "en_cell_scale": |
| 272 | case "en_octaves": return {txt: band(TH * num("en_erosion_scale") * num("en_cell_scale"), |
| 273 | num("en_erosion_lac"), num("en_octaves"))}; |
| 274 | case "en_gain": return gl(num("en_gain"), num("en_erosion_lac")); |
| 275 | case "voxel_size": return {txt: `band-limit; at ${num("mpp")} m/px use \u2265 ${2*num("mpp")} m`, |
| 276 | warn: num("voxel_size") < 2 * num("mpp")}; |
| 277 | case "rounding_input": return {txt: "runevision's input-feature-size control; pinned at 0.1 in the engine"}; |
| 278 | case "assumed_slope_mix":return {txt: num("assumed_slope_mix") >= 1 |
| 279 | ? "1.0 = input slope MAGNITUDE replaced; direction kept" |
| 280 | : "blending real input slope magnitude back in"}; |
| 281 | default: return null; |
| 282 | } |
| 283 | } |
| 284 | |
| 285 | function updateDerived() { |
| 286 | for (const items of Object.values(GROUPS)) |
| 287 | for (const [key] of items) { |
| 288 | const el = $("d_" + key); if (!el) continue; |
| 289 | const d = derived(key); |
| 290 | el.textContent = d ? d.txt : ""; |
| 291 | el.classList.toggle("warn", !!(d && d.warn)); |
| 292 | } |
| 293 | const TH = num("terrain_height"), LAC = num("noise_lacunarity"); |
| 294 | const view = num("n") * num("mpp"); |
| 295 | const rows = [ |
| 296 | ["mountain", TH / num("mountain_scale"), LAC, num("mountain_octaves")], |
| 297 | ["base gn ", TH / num("gn_scale"), 2.0, num("gn_detail")], |
| 298 | ["erosion ", TH * num("en_erosion_scale") * num("en_cell_scale"), |
| 299 | num("en_erosion_lac"), num("en_octaves")], |
| 300 | ["detail ", TH / num("detail_scale"), LAC, num("detail_octaves")], |
| 301 | ]; |
| 302 | $("bands").innerHTML = rows.map(([nm, l0, lac, oct]) => { |
| 303 | const over = l0 > view; |
| 304 | return `${nm} ${(over ? "<span class=\"warn\">" : "<span>")}` + |
| 305 | `${band(l0, lac, oct)}${over ? " coarsest octave is WIDER than the view" : ""}</span>`; |
| 306 | }).join("\n") + `\nview ${km(view)} across`; |
| 307 | } |
| 197 | 308 | let view = "shaded-height"; |
| 198 | 309 | let mode = "2d"; |
| 199 | 310 | |
| … | … | @@ -201,8 +312,9 @@ for (const [grp, items] of Object.entries(GROUPS)) { |
| 201 | 312 | const host = $("grp_" + grp); |
| 202 | 313 | for (const [key, def, lo, hi, step] of items) { |
| 203 | 314 | const l = document.createElement("label"); |
| 204 | | l.innerHTML = `${key.replace(/_/g," ")}<span class="v" id="v_${key}">${def}</span> |
| 205 | | <input type="range" id="${key}" min="${lo}" max="${hi}" step="${step}" value="${def}">`; |
| 315 | l.innerHTML = `${LABELS[key] || key.replace(/_/g," ")}<span class="v" id="v_${key}">${def}</span> |
| 316 | <input type="range" id="${key}" min="${lo}" max="${hi}" step="${step}" value="${def}"> |
| 317 | <span class="d" id="d_${key}"></span>`; |
| 206 | 318 | host.appendChild(l); |
| 207 | 319 | } |
| 208 | 320 | } |
| … | … | @@ -224,6 +336,7 @@ function syncLabels() { |
| 224 | 336 | `diffusion ${(n*30/1000).toFixed(2)} km @ 30 m/px` + |
| 225 | 337 | (mpp !== 30 ? " · SCALES DIFFER" : ""); |
| 226 | 338 | $("scale").className = "stats" + (mpp !== 30 ? " err" : ""); |
| 339 | updateDerived(); |
| 227 | 340 | } |
| 228 | 341 | |
| 229 | 342 | async function load(which, url, imgId, stId, busyId) { |
| … | … | @@ -404,7 +517,8 @@ function erosionQuery() { |
| 404 | 517 | function render() { |
| 405 | 518 | syncLabels(); |
| 406 | 519 | const p = erosionQuery(); |
| 407 | | const q = new URLSearchParams({n: $("n").value, i1: $("i1").value, j1: $("j1").value}); |
| 520 | const q = new URLSearchParams({n: $("n").value, i1: $("i1").value, |
| 521 | j1: $("j1").value, seed: $("seed").value}); |
| 408 | 522 | |
| 409 | 523 | if (mode === "3d") { |
| 410 | 524 | load3d("/api/erosion.bin?" + p, "cv_e", "st_e", "busy_e"); |
| … | … | @@ -428,6 +542,16 @@ document.addEventListener("input", e => { |
| 428 | 542 | if (e.target.type === "range" || e.target.type === "number") schedule(); |
| 429 | 543 | }); |
| 430 | 544 | $("render").onclick = render; |
| 545 | // Two different ways to get different terrain, and they cost very differently. |
| 546 | $("newloc").onclick = () => { |
| 547 | $("i1").value = Math.floor(Math.random() * 200000); |
| 548 | $("j1").value = Math.floor(Math.random() * 200000); |
| 549 | render(); |
| 550 | }; |
| 551 | $("newseed").onclick = () => { |
| 552 | $("seed").value = Math.floor(Math.random() * 1e9); |
| 553 | render(); |
| 554 | }; |
| 431 | 555 | $("reset").onclick = () => { |
| 432 | 556 | for (const items of Object.values(GROUPS)) |
| 433 | 557 | for (const [key, def] of items) $(key).value = def; |