diff --git a/client/project.godot b/client/project.godot index 1cad9b1ff..fdbdaea05 100644 --- a/client/project.godot +++ b/client/project.godot @@ -17,7 +17,7 @@ config/name="The Settled Reach" ; in the editor and in a shipped build, where res://../project.yaml does not ; exist at all (T-1241). Kept honest by `make check-client-version`, which the ; pre-push hook runs — do not edit this by hand without moving project.yaml too. -config/version="0.4.12" +config/version="0.4.13" run/main_scene="res://scenes/main_menu.tscn" config/features=PackedStringArray("4.6", "GL Compatibility") config/icon="res://icon.svg" diff --git a/client/tests/visual_capture.gd b/client/tests/visual_capture.gd index 1240451c1..485428bdc 100644 --- a/client/tests/visual_capture.gd +++ b/client/tests/visual_capture.gd @@ -339,7 +339,7 @@ func _log_atlas_view_transform(tree_root: Node, scenario_name: String) -> void: "visual_capture: view-transform[%s] rung=%s world_center=%s held_extent=%s " + "canvas_position=%s canvas_scale=%s footprint_px=%s canvas_cells=%dx%d " + "courses=%d runs=%d longest=%.1fpx drawn=%d settlements=%d " - + "planes=%s relief_grad=%.2f" + + "planes=%s relief_grad=%.2f elev_grad=%.2f" ) % [ scenario_name, @@ -358,6 +358,7 @@ func _log_atlas_view_transform(tree_root: Node, scenario_name: String) -> void: int(summary.get("settlement_count", 0)), str(summary.get("plane_variety", {})), float(summary.get("relief_gradient", 0.0)), + float(summary.get("elev_gradient", 0.0)), ] ) ) diff --git a/client/ui/implant/apps/atlas/step_canvas/step_canvas_colorize.gd b/client/ui/implant/apps/atlas/step_canvas/step_canvas_colorize.gd index a1106324e..5c15e2270 100644 --- a/client/ui/implant/apps/atlas/step_canvas/step_canvas_colorize.gd +++ b/client/ui/implant/apps/atlas/step_canvas/step_canvas_colorize.gd @@ -116,6 +116,28 @@ const RUGGEDNESS_FULL_SCALE_Q: int = 4 ## has arrived but has not yet flattened into the regional average. const RUGGEDNESS_BASELINE_CELLS: int = 4 +## Exponent applied to the ruggedness reading before it becomes a stipple +## probability (T-1194). +## +## The texture's job is to say WHERE the ground is broken, and a linear reading +## cannot: measured on Ferrath Global the mean |elev_q gradient| is 0.99 steps +## per cell against RUGGEDNESS_FULL_SCALE_Q's 4, so ordinary land sits at +## 0.25-0.4 and the map gets an even wash of dots. The RimWorld reference is the +## opposite — clean plains, dense ranges — and that difference is distribution, +## not amplitude. +## +## 2.0 keeps the top end almost untouched (0.9 -> 0.81) while collapsing the +## middle (0.4 -> 0.16), which is exactly the asymmetry wanted. Higher starts +## erasing genuinely rolling country along with the flats. +const RUGGEDNESS_CONTRAST_GAMMA: float = 2.0 + +## How far above the canvas's own expected baseline delta counts as "fully +## rugged" (T-1194). 2x puts terrain of ordinary roughness at 0.5 before the +## contrast curve and reserves saturation for ground genuinely twice as broken +## as its neighbours — which is what makes a range read as a range against the +## plain beside it. +const RUGGEDNESS_HEADROOM_MULTIPLE: float = 2.0 + ## relief_q deviation from flat (50) at which ground is "fully rugged". Each ## step is 8 m, so 60 = 480 m of local relief. ## @@ -221,6 +243,10 @@ class CellPlanes: ## one constant works at every rung — see HILLSHADE_GRADIENT_MULTIPLE. 0.0 ## means flat (or unmeasured), which shades nothing. var relief_gradient_scale: float = 0.0 + ## Mean |elev_q central difference| across this canvas, measured once per + ## rebuild alongside the relief one. The stipple normalizes against it at the + ## orbital rungs — see [method _ruggedness]. 0.0 falls back to the constant. + var elev_gradient_scale: float = 0.0 var glaciation: Image var temp_dc: Array var width: int @@ -463,9 +489,40 @@ static func _ruggedness(planes: CellPlanes, col: int, row: int) -> float: var c: int = clampi(col + d.x, 0, planes.width - 1) var r: int = clampi(row + d.y, 0, planes.height - 1) worst = maxi(worst, absi(_l8_value(planes.elev_q, c, r) - here)) - var from_elev: float = float(worst) / float(RUGGEDNESS_FULL_SCALE_Q) + # Normalize against THIS canvas's own elev_q gradient rather than the fixed + # constant (T-1194). RUGGEDNESS_FULL_SCALE_Q = 4 was measured at Region + # ("d4 2.38"), and the stipple's elev path only ever RUNS at the orbital + # rungs, where the number is different: Ferrath Global measures a mean + # 1-cell gradient of 0.99, so a coherent 4-cell baseline reaches ~4 and + # saturates the constant outright. Every land cell then read as fully rugged + # — the even wash of dots this ticket exists to fix, and the reason adding a + # contrast curve alone changed nothing (1,679 -> 1,698 distinct colours: a + # curve cannot separate values that are all already clamped to 1.0). + # + # The expected baseline delta is the mean 1-cell gradient times the baseline + # length; the multiple then puts typical ground below saturation and leaves + # headroom for genuinely broken terrain. + var elev_full_scale: float = float(RUGGEDNESS_FULL_SCALE_Q) + if planes.elev_gradient_scale > 0.0: + elev_full_scale = ( + planes.elev_gradient_scale + * float(RUGGEDNESS_BASELINE_CELLS) + * RUGGEDNESS_HEADROOM_MULTIPLE + ) + var from_elev: float = float(worst) / maxf(elev_full_scale, 0.001) - return clampf(maxf(from_relief, from_elev), 0.0, 1.0) + # CONTRAST CURVE (T-1194). The linear reading puts ordinary ground in the + # middle of the density range, which paints uniform grain everywhere — the + # opposite of what this texture is for. The reference map's whole tell is + # CONTRAST: "its high flat plains carry none; its mountain ranges are dense + # with it" (this file's own T-1194 note). Measured on Ferrath Global, the + # mean |elev_q gradient| is 0.99 steps per cell against a full scale of 4, so + # typical land lands near 0.25-0.4 — everywhere gets some, nowhere gets much. + # + # A power curve fixes the distribution without touching the measurement: + # gentle ground falls away toward clean, broken ground keeps its density. + # 0.4 -> 0.16 at gamma 2, while 0.9 only drops to 0.81. + return pow(clampf(maxf(from_relief, from_elev), 0.0, 1.0), RUGGEDNESS_CONTRAST_GAMMA) ## Domain salts for the two marks, resolved ONCE at class load rather than per diff --git a/client/ui/implant/apps/atlas/step_canvas/step_canvas_terrain_layer.gd b/client/ui/implant/apps/atlas/step_canvas/step_canvas_terrain_layer.gd index b1bf1dd25..3026cac3c 100644 --- a/client/ui/implant/apps/atlas/step_canvas/step_canvas_terrain_layer.gd +++ b/client/ui/implant/apps/atlas/step_canvas/step_canvas_terrain_layer.gd @@ -76,6 +76,9 @@ var _plane_variety: Dictionary = {} ## Mean |relief_q gradient| of the held canvas — see [method get_relief_gradient]. var _relief_gradient_scale: float = 0.0 +## Mean |elev_q gradient| — the stipple's driver at the orbital rungs (T-1194). +var _elev_gradient_scale: float = 0.0 + ## Rebuild (or reuse) the held texture from a decoded StepCanvasResponse's ## `canvas` Dictionary (step_canvas_protocol.gd's shape: width/height + @@ -133,6 +136,12 @@ func _decode_planes(canvas: Dictionary, width: int, height: int) -> StepCanvasCo planes.relief_q = _decode_l8_plane(canvas.get("relief_q")) planes.relief_gradient_scale = _measure_relief_gradient(planes.relief_q) _relief_gradient_scale = planes.relief_gradient_scale + # T-1194: the stipple's OWN driver at the orbital rungs, where relief_q is + # flat and `_ruggedness` falls back to the elev_q gradient. Measured here so + # the density constant can be tuned against the rung it actually runs on + # rather than against the one it was first measured at. + _elev_gradient_scale = _measure_relief_gradient(planes.elev_q) + planes.elev_gradient_scale = _elev_gradient_scale planes.glaciation = _decode_l8_plane(canvas.get("glaciation")) planes.temp_dc = canvas.get("temp_dc", []) return planes @@ -192,6 +201,13 @@ func get_relief_gradient() -> float: return _relief_gradient_scale +## The held canvas's mean |elev_q gradient| (T-1194). At the orbital rungs this +## is what drives the relief stipple, and its density constant was measured at +## Region — a rung it does not run on. Surfaced so the two can be compared. +func get_elev_gradient() -> float: + return _elev_gradient_scale + + ## Cells stepped between variety samples. ## ## A full scan is 5 planes x 1290x540 = ~3.5M get_pixel() calls per canvas diff --git a/client/ui/implant/apps/atlas/step_canvas/step_canvas_viewer.gd b/client/ui/implant/apps/atlas/step_canvas/step_canvas_viewer.gd index 4ea7f64e8..971959afc 100644 --- a/client/ui/implant/apps/atlas/step_canvas/step_canvas_viewer.gd +++ b/client/ui/implant/apps/atlas/step_canvas/step_canvas_viewer.gd @@ -450,6 +450,7 @@ func get_current_canvas_summary() -> Dictionary: # "the server sent a noisy field" from "the renderer is painting noise # onto a smooth one" — two causes with one appearance (a stucco rung). "relief_gradient": _terrain_layer.get_relief_gradient() if _terrain_layer else 0.0, + "elev_gradient": _terrain_layer.get_elev_gradient() if _terrain_layer else 0.0, "course_count_by_class": course_count_by_class, "cliff_count": (d.get("cliffs", []) as Array).size(), "settlement_count": _count_distinct_settlements(d), diff --git a/project.yaml b/project.yaml index 74126b66a..371935e89 100644 --- a/project.yaml +++ b/project.yaml @@ -65,7 +65,9 @@ name: The Settled Reach # every octave the rung cannot reconstruct, so a 0.4.11 Region entry holds a # relief field this build would never produce. Warm caches are genuinely wrong # here, not merely stale. -version: 0.4.12 +# 0.4.13 — stipple contrast (T-1194). Paint only; bumped because the client +# step_canvas cluster is in the T-1242 registry. +version: 0.4.13 repository: settled-reach