feat(ui): the stipple says WHERE the ground is broken, not just that it is (T-1194)
RimWorld's technique 4, the reference this ticket names, works by CONTRAST: the
Rockies and Appalachians carry dense hatching and the Great Plains carry none.
Ferrath's Global carried an even wash of dots over every landmass instead —
texture present, information absent.
The cause was a constant measured on the wrong rung. RUGGEDNESS_FULL_SCALE_Q = 4
comes from Region ("d4 2.38"), but the elev_q path it gates only ever RUNS at the
orbital rungs, where relief_q is flat and the stipple falls back to it. Ferrath
Global measures a mean 1-cell elev_q gradient of 0.99, so a coherent 4-cell
baseline reaches ~4 and saturates the constant exactly: every land cell read as
fully rugged.
It now normalizes against the canvas's own measured gradient — the same
self-calibrating shape T-1240 gave the hillshade, and for the same reason: one
constant cannot serve rungs whose sample spacing differs by four orders of
magnitude. A contrast curve rides on top, because even unsaturated the linear
reading puts ordinary ground mid-range and paints grain everywhere.
Measured on Global, before -> after: 1,679 -> 1,900 distinct colours, 145.90 ->
147.39 lum spread, and the pale uplands now stipple visibly denser than the
lowlands beside them.
Recorded because it cost two wrong turns: I first guessed saturation, then
talked myself out of it after measuring a 1-CELL gradient (0.99) against a full
scale meant for the 4-CELL baseline, and shipped a contrast curve alone — which
measurably did nothing (1,679 -> 1,698), because a curve cannot separate values
already clamped to 1.0. The gamma is kept; it does its job now that there is a
range to curve. The elev_q gradient joins relief_q's in the capture readout, so
the next person tuning this can read the number instead of guessing at it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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"
|
||||
|
||||
@@ -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)),
|
||||
]
|
||||
)
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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),
|
||||
|
||||
+3
-1
@@ -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
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user