feat(simulation): relief_q — a local relief signal the deep rungs can resolve (T-1213)
The District and Quarter rungs rendered as flat colour, and the cause was not the biome work everyone assumed. Measured on Ferrath through the production canvas builder: at District the mean |elev_q delta| between neighbouring gridunits is 0.02, and NOT ONE PAIR in a 1290x540 frame differs by 2. elev_q spans 0-100 across the body's whole 8 km elevation range, so ONE STEP IS 80 METRES. A District canvas covers 2,048 m of ground, where the rolling relief a walker navigates by is metres to tens of metres -- a fraction of a single step. The sub-district detail IS generated (invent_primitives' scatter and relief bands compute it) and then rounded away. Confirmed by running the diagnostic with the octave cutoff disabled: still 0.02. relief_q carries that same invented fine component against a scale chosen to resolve it: 0-100 about a flat 50, RELIEF_FULL_SCALE_M = 400 m either side, so 8 m per step -- ten times finer than elev_q. elev_q keeps its body-absolute meaning and the Atlas legend stays true. Measured effect, elev_q vs relief_q (distinct values / mean 4-cell delta): Region 49 / 2.38 -> 101 / 21.86 District 10 / 0.08 -> 35 / 0.35 Quarter 8 / 0.02 -> 19 / 0.06 FIXED metre scale, never per-canvas normalization: the value for a piece of ground must not depend on what else is in frame, or the same hillside changes tone as the viewer pans. And it excludes elev_pct deliberately -- this is the departure from the surrounding land, not height above sea level; including the base would re-introduce the body-scale dominance that makes elev_q unusable down here. 50 at the orbital rungs, which skip invent_primitives by design. Nothing is lost: Global and Region still have varied elev_q (101 and 49 distinct values), and the client takes whichever field carries signal via a max, with no rung-name branching. The client's ruggedness driver changes with it. It was an elev_q GRADIENT, which cannot work across rungs -- the same 4-cell delta reads 21.86 at Region and 0.08 at District, so any single full-scale constant either saturates one or vanishes on the other. relief_q states relief outright, so |relief_q - 50| is the answer directly and a fixed metre scale is immune to that by construction. An absent plane reads FLAT, not zero -- 0 on this field means maximum relief BELOW flat, so a payload without it would have stippled the entire map. That is reachable: the field is #[serde(default)] so old-shape payloads decode. Two colorize tests whose fixtures predate the plane caught it. 2004 server tests, 1838 client tests, 0 failed. clippy clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -76,6 +76,12 @@ const RUGGEDNESS_FULL_SCALE_Q: int = 4
|
||||
## has arrived but has not yet flattened into the regional average.
|
||||
const RUGGEDNESS_BASELINE_CELLS: int = 4
|
||||
|
||||
## relief_q deviation from flat (50) at which ground is "fully rugged". Each
|
||||
## step is 8 m, so 25 = 200 m of local relief — a substantial ridge at the
|
||||
## sub-district scale, and comfortably inside the range the field actually
|
||||
## uses (Region spans all 101 values; District 35 of them).
|
||||
const RELIEF_TEXTURE_FULL_SCALE_Q: int = 25
|
||||
|
||||
## One decoded plane set, pre-extracted from the five L8 Images + the two
|
||||
## raw-array fields a caller needs per cell — built once per arrived canvas
|
||||
## (see StepCanvasTerrainLayer.build_texture()), not re-decoded per pixel.
|
||||
@@ -88,6 +94,7 @@ class CellPlanes:
|
||||
var moisture_q: Image
|
||||
var vegetation: Image
|
||||
var lake_margin_q: Image
|
||||
var relief_q: Image
|
||||
var glaciation: Image
|
||||
var temp_dc: Array
|
||||
var width: int
|
||||
@@ -172,6 +179,32 @@ static func _texture(planes: CellPlanes, col: int, row: int, base: Color) -> Col
|
||||
## Clamped at the edges by sampling the centre, so a canvas border neither
|
||||
## invents relief nor loses it.
|
||||
static func _ruggedness(planes: CellPlanes, col: int, row: int) -> float:
|
||||
# PRIMARY: relief_q states local relief outright (T-1213) — 50 is flat and
|
||||
# each step is 8 m — so the ruggedness is just how far from flat this ground
|
||||
# is. No neighbour comparison: the field already IS the answer, measured on
|
||||
# a fixed metre scale rather than against whatever happens to be in frame.
|
||||
#
|
||||
# This replaced an elev_q GRADIENT, which cannot work across rungs: the same
|
||||
# 4-cell delta reads 21.86 at Region and 0.08 at District, so any single
|
||||
# full-scale constant either saturates the one or vanishes on the other. A
|
||||
# fixed metre scale is immune to that by construction.
|
||||
# Absent plane reads FLAT, not zero. `_l8_value` zero-fills a missing image,
|
||||
# and on this field 0 does not mean "no data" — it means maximum relief
|
||||
# below flat, so a payload without the plane would render every cell fully
|
||||
# rugged and stipple the entire map. That is reachable in practice: the
|
||||
# field is `#[serde(default)]` server-side precisely so an old-shape payload
|
||||
# decodes rather than hard-failing. Caught by two colorize tests whose
|
||||
# fixtures predate the plane.
|
||||
var from_relief: float = 0.0
|
||||
if planes.relief_q != null:
|
||||
var rel: int = _l8_value(planes.relief_q, col, row)
|
||||
from_relief = absf(float(rel) - 50.0) / float(RELIEF_TEXTURE_FULL_SCALE_Q)
|
||||
|
||||
# FALLBACK: the orbital rungs skip the sub-district derive entirely, so
|
||||
# relief_q is a flat 50 there and would texture nothing. elev_q is still
|
||||
# richly varied at that scale (101 distinct values on Ferrath at Global), so
|
||||
# Global keeps the gradient read. Taking the max means each rung is driven
|
||||
# by whichever field actually carries signal, with no rung-name branching.
|
||||
var here: int = _l8_value(planes.elev_q, col, row)
|
||||
var worst: int = 0
|
||||
var b: int = RUGGEDNESS_BASELINE_CELLS
|
||||
@@ -179,7 +212,9 @@ 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))
|
||||
return clampf(float(worst) / float(RUGGEDNESS_FULL_SCALE_Q), 0.0, 1.0)
|
||||
var from_elev: float = float(worst) / float(RUGGEDNESS_FULL_SCALE_Q)
|
||||
|
||||
return clampf(maxf(from_relief, from_elev), 0.0, 1.0)
|
||||
|
||||
|
||||
## Deterministic [0,1) dither for a cell, salted per mark type so the relief and
|
||||
|
||||
@@ -112,6 +112,7 @@ func _decode_planes(canvas: Dictionary, width: int, height: int) -> StepCanvasCo
|
||||
planes.moisture_q = _decode_l8_plane(canvas.get("moisture_q"))
|
||||
planes.vegetation = _decode_l8_plane(canvas.get("vegetation"))
|
||||
planes.lake_margin_q = _decode_l8_plane(canvas.get("lake_margin_q"))
|
||||
planes.relief_q = _decode_l8_plane(canvas.get("relief_q"))
|
||||
planes.glaciation = _decode_l8_plane(canvas.get("glaciation"))
|
||||
planes.temp_dc = canvas.get("temp_dc", [])
|
||||
return planes
|
||||
|
||||
Reference in New Issue
Block a user