From ab6e1413d94fd613b6e9533e105b9dc1183111c6 Mon Sep 17 00:00:00 2001 From: Jeroen Schweitzer Date: Sun, 28 Jun 2026 20:41:59 +0200 Subject: [PATCH] =?UTF-8?q?feat(simulation):=20voxel-tier=20mid-scale=20re?= =?UTF-8?q?lief=20=E2=80=94=20navigable=20hills=20(T-1081)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The voxel tier read flat (the D-245 "0–3 m, no hills to navigate by" bug): family generators set elevation from elev_q at a compressed scale plus only ±4 m micro-scatter, and detail-scatter's mid-scale relief (D-243 §2) only ever reached the district tier (elev_q), never per-voxel elevation_m. - detail_scatter.rs: extract the enveloped-fBm core; add `voxel_relief` at the 0.25–2 km octave band (vs the district 4–40 km band). - voxel.rs: add the mid-scale relief post-dispatch in derive_voxel_column, body-global SeedDomain::VoxelRelief seed (position-keyed), f64 truncated to integer metres (D-010). Envelope = slope_q*3 + elev_q — the coarse heightmap gives slope_q ≈ 0 even on high ground (Arbour max 13), so elevation must drive ruggedness. Flat families only (Alluvial/Lava/BraidedDelta/Dune/Meander); the dramatic families already carry strong internal relief and WaterBody stays at sea level. - seed.rs: SeedDomain::VoxelRelief (D-224 domain separation). - believability.rs: new `contrast.voxel_relief_m` metric — mean within-district elevation range over a 2 km transect (a single 64 m sample chunk is narrower than the relief band) + a "voxel relief" criterion. Arbour @ yolo: voxel relief 5 m → 59 m, 6/8 → 7/8 criteria (remaining fail = the Q-123 vegetation-denominator item). Edict 59 m. Golden regenerated; 1586 lib tests + believability harness (determinism) pass; clippy -D warnings clean. D-239 amended (T-1081); Q-123 item 4 updated. Deferred to Q-123/follow-up: the provisional span + threshold, and a per-body hypsometric absolute-elevation model. Co-Authored-By: Claude Opus 4.8 (1M context) --- .pql/changelog/ticket_history/2026-06.sql | 53 ++++++++ .pql/changelog/tickets/2026-06.sql | 39 ++++++ governance/decisions/architecture.md | 1 + governance/questions/architecture.md | 2 +- server/src/atlas/believability.rs | 56 +++++++++ server/src/atlas/detail_scatter.rs | 76 +++++++++++- server/src/atlas/voxel.rs | 141 ++++++++++++++++++++++ server/src/seed.rs | 5 + server/tests/golden/believability.json | 6 +- 9 files changed, 375 insertions(+), 4 deletions(-) diff --git a/.pql/changelog/ticket_history/2026-06.sql b/.pql/changelog/ticket_history/2026-06.sql index 4cf70564c..d2121a6c7 100644 --- a/.pql/changelog/ticket_history/2026-06.sql +++ b/.pql/changelog/ticket_history/2026-06.sql @@ -2771,3 +2771,56 @@ Stack-review note (2026-06-28): the believability sampling-bias fix (contiguous- INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FGSJTXTADJRHYEE1WTAENYAG', 'status', 'in_progress', 'done', NULL, '2026-06-28 16:02:46', '2026-06-28 16:02:46.330', '2026-06-28 16:02:46.330', NULL, '66fcb4c2c045444b78cfc270ba3e14b7', 2) ON CONFLICT(hash) DO NOTHING; INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FGSJWWZNWYT3C4T828ZF1QCG', 'status', 'in_progress', 'done', NULL, '2026-06-28 16:02:46', '2026-06-28 16:02:46.337', '2026-06-28 16:02:46.337', NULL, '3d4736181c620fea8037c2e863bb4a89', 2) ON CONFLICT(hash) DO NOTHING; INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FGSJZG3WFPQ3VXBFQFM4YEWW', 'status', 'in_progress', 'done', NULL, '2026-06-28 16:02:46', '2026-06-28 16:02:46.338', '2026-06-28 16:02:46.338', NULL, 'fe177606db9f2ee174a5cc0131ec8b64', 2) ON CONFLICT(hash) DO NOTHING; +INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FGSJVW30PSP8K0XAWV02X7M0', 'status', 'backlog', 'in_progress', NULL, '2026-06-28 16:08:41', '2026-06-28 16:08:41.257', '2026-06-28 16:08:41.257', NULL, '0c31df90c425803dc23a6ac9369cd751', 2) ON CONFLICT(hash) DO NOTHING; +INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FGSJVW30PSP8K0XAWV02X7M0', 'description', 'Probe evidence (Arbour @ yolo): voxel elevation 0-3 m across nearly all sampled districts (only one snow upland reached 42-49 m). Flat terrain => nothing to navigate by, no landmarks, no ''specificity of place''. Either the heightmap relief is being flattened by the 512x256 working downsample + district derivation, or detail-scatter (D-227, derive-don''t-store) is not producing the mid-scale relief it is supposed to invent at the voxel tier. Direction to discuss: the relief envelope taken from the heightmap, detail-scatter amplitude, and whether the elev_q -> elevation_m mapping is collapsing the range. Believability gap.', 'Probe evidence (Arbour @ yolo): voxel elevation 0-3 m across nearly all sampled districts (only one snow upland reached 42-49 m). Flat terrain => nothing to navigate by, no landmarks, no ''specificity of place''. Either the heightmap relief is being flattened by the 512x256 working downsample + district derivation, or detail-scatter (D-227, derive-don''t-store) is not producing the mid-scale relief it is supposed to invent at the voxel tier. Direction to discuss: the relief envelope taken from the heightmap, detail-scatter amplitude, and whether the elev_q -> elevation_m mapping is collapsing the range. Believability gap. + +--- +Refinement (Si pass, 2026-06-28) — READY. Four compounding causes (all confirmed): +1. elev_q→elevation_m scale: generate_alluvial_plain uses `elev_q / 2` → whole-body relief is 0–50 m (pancake). Other flat families similar (/2,/3,/4). The dramatic families (cliff/fjord/gorge) carry real internal relief; the flat ones (alluvial/lava/dune/braided/meander) don''t. +2. detail_scatter (D-227) is applied ONLY at the district tier (derive_district → elev_q); it never reaches per-voxel elevation_m. So within a chunk relief = base ± 4 m micro-scatter = flat. The promised mid-scale (sub-district, ~tens-to-hundreds m) relief is missing at the voxel tier. +3. Envelope collapses to 0 on flat-slope districts (terrain_detail returns 0 when envelope==0) — by design ("no mountain on an authored plain"); consider a small floor so even gentle land rolls. +4. WaterBody correctly keeps elevation_m=0 (sea level) — must stay. + +PLAN (data-driven, tune via probe like moisture): +- Add MID-SCALE rolling relief at the voxel tier: a detail-scatter octave term (sub-district wavelengths ~256–2048 m) added to elevation_m for DRY-LAND columns, enveloped by district ruggedness (slope_q) so plains stay flat and rugged terrain rolls. f64 perturbation truncated to i32 (D-010, same as the warp). Applied centrally in derive_voxel_column (one site, like derive_cover) so all land families benefit. +- Bump the flat-family base scale from the compressed /N toward a believable span if the probe shows mountains still too low (per-body absolute relief = T-1032 follow-up). +- Add a VOXEL-RELIEF metric to believability.rs (mean within-chunk elevation range across sampled non-water districts) — the current "elevation relief" criterion measures DISTRICT elev_q spread (already passes), not voxel relief, so it does not catch T-1081. +- VERIFY: probe per-location elevation range rises from ~0-3 m; the new voxel-relief criterion flips green. Determinism: integer/D-010. +Files: voxel.rs (generators + central relief), detail_scatter.rs (sub-district wavelengths), believability.rs (metric).', NULL, '2026-06-28 16:16:05', '2026-06-28 16:16:05.190', '2026-06-28 16:16:05.190', NULL, 'c8b9b791152f463f455dfc08d15027c6', 2) ON CONFLICT(hash) DO NOTHING; +INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FGSJVW30PSP8K0XAWV02X7M0', 'description', 'Probe evidence (Arbour @ yolo): voxel elevation 0-3 m across nearly all sampled districts (only one snow upland reached 42-49 m). Flat terrain => nothing to navigate by, no landmarks, no ''specificity of place''. Either the heightmap relief is being flattened by the 512x256 working downsample + district derivation, or detail-scatter (D-227, derive-don''t-store) is not producing the mid-scale relief it is supposed to invent at the voxel tier. Direction to discuss: the relief envelope taken from the heightmap, detail-scatter amplitude, and whether the elev_q -> elevation_m mapping is collapsing the range. Believability gap. + +--- +Refinement (Si pass, 2026-06-28) — READY. Four compounding causes (all confirmed): +1. elev_q→elevation_m scale: generate_alluvial_plain uses `elev_q / 2` → whole-body relief is 0–50 m (pancake). Other flat families similar (/2,/3,/4). The dramatic families (cliff/fjord/gorge) carry real internal relief; the flat ones (alluvial/lava/dune/braided/meander) don''t. +2. detail_scatter (D-227) is applied ONLY at the district tier (derive_district → elev_q); it never reaches per-voxel elevation_m. So within a chunk relief = base ± 4 m micro-scatter = flat. The promised mid-scale (sub-district, ~tens-to-hundreds m) relief is missing at the voxel tier. +3. Envelope collapses to 0 on flat-slope districts (terrain_detail returns 0 when envelope==0) — by design ("no mountain on an authored plain"); consider a small floor so even gentle land rolls. +4. WaterBody correctly keeps elevation_m=0 (sea level) — must stay. + +PLAN (data-driven, tune via probe like moisture): +- Add MID-SCALE rolling relief at the voxel tier: a detail-scatter octave term (sub-district wavelengths ~256–2048 m) added to elevation_m for DRY-LAND columns, enveloped by district ruggedness (slope_q) so plains stay flat and rugged terrain rolls. f64 perturbation truncated to i32 (D-010, same as the warp). Applied centrally in derive_voxel_column (one site, like derive_cover) so all land families benefit. +- Bump the flat-family base scale from the compressed /N toward a believable span if the probe shows mountains still too low (per-body absolute relief = T-1032 follow-up). +- Add a VOXEL-RELIEF metric to believability.rs (mean within-chunk elevation range across sampled non-water districts) — the current "elevation relief" criterion measures DISTRICT elev_q spread (already passes), not voxel relief, so it does not catch T-1081. +- VERIFY: probe per-location elevation range rises from ~0-3 m; the new voxel-relief criterion flips green. Determinism: integer/D-010. +Files: voxel.rs (generators + central relief), detail_scatter.rs (sub-district wavelengths), believability.rs (metric).', 'Probe evidence (Arbour @ yolo): voxel elevation 0-3 m across nearly all sampled districts (only one snow upland reached 42-49 m). Flat terrain => nothing to navigate by, no landmarks, no ''specificity of place''. Either the heightmap relief is being flattened by the 512x256 working downsample + district derivation, or detail-scatter (D-227, derive-don''t-store) is not producing the mid-scale relief it is supposed to invent at the voxel tier. Direction to discuss: the relief envelope taken from the heightmap, detail-scatter amplitude, and whether the elev_q -> elevation_m mapping is collapsing the range. Believability gap. + +--- +Refinement (Si pass, 2026-06-28) — READY. Four compounding causes (all confirmed): +1. elev_q→elevation_m scale: generate_alluvial_plain uses `elev_q / 2` → whole-body relief is 0–50 m (pancake). Other flat families similar (/2,/3,/4). The dramatic families (cliff/fjord/gorge) carry real internal relief; the flat ones (alluvial/lava/dune/braided/meander) don''t. +2. detail_scatter (D-227) is applied ONLY at the district tier (derive_district → elev_q); it never reaches per-voxel elevation_m. So within a chunk relief = base ± 4 m micro-scatter = flat. The promised mid-scale (sub-district, ~tens-to-hundreds m) relief is missing at the voxel tier. +3. Envelope collapses to 0 on flat-slope districts (terrain_detail returns 0 when envelope==0) — by design ("no mountain on an authored plain"); consider a small floor so even gentle land rolls. +4. WaterBody correctly keeps elevation_m=0 (sea level) — must stay. + +PLAN (data-driven, tune via probe like moisture): +- Add MID-SCALE rolling relief at the voxel tier: a detail-scatter octave term (sub-district wavelengths ~256–2048 m) added to elevation_m for DRY-LAND columns, enveloped by district ruggedness (slope_q) so plains stay flat and rugged terrain rolls. f64 perturbation truncated to i32 (D-010, same as the warp). Applied centrally in derive_voxel_column (one site, like derive_cover) so all land families benefit. +- Bump the flat-family base scale from the compressed /N toward a believable span if the probe shows mountains still too low (per-body absolute relief = T-1032 follow-up). +- Add a VOXEL-RELIEF metric to believability.rs (mean within-chunk elevation range across sampled non-water districts) — the current "elevation relief" criterion measures DISTRICT elev_q spread (already passes), not voxel relief, so it does not catch T-1081. +- VERIFY: probe per-location elevation range rises from ~0-3 m; the new voxel-relief criterion flips green. Determinism: integer/D-010. +Files: voxel.rs (generators + central relief), detail_scatter.rs (sub-district wavelengths), believability.rs (metric). + +Implemented (2026-06-28, branch voxel-relief). The voxel tier now carries mid-scale relief (D-243 §2): a `voxel_relief` fBm pass at the 0.25–2 km octave band (detail_scatter.rs), body-global SeedDomain::VoxelRelief seed, added post-dispatch in derive_voxel_column, truncated to integer metres (D-010). + +Root cause refined during impl: the coarse heightmap yields slope_q ≈ 0 even on high ground (Arbour max slope_q = 13), so the relief envelope is `slope_q*3 + elev_q` (elevation drives ruggedness), not slope alone. Applied to the FLAT families only (AlluvialPlain/LavaField/BraidedDelta/DuneStrand/MeanderReach) — the dramatic families (Cliff/Fjord/Gorge) already carry strong internal relief and a position-varying field warps their features; WaterBody stays at sea level. + +Verification: new believability metric `contrast.voxel_relief_m` (mean within-district elevation range over a 2 km transect — a single 64 m chunk is narrower than the relief band). Arbour @ yolo: voxel relief 5 m → 59 m, criteria 6/8 → 7/8 (remaining fail = the Q-123 vegetation-denominator item, not relief). Edict also 59 m. 1586 lib tests + believability harness (determinism + golden) green; clippy -D warnings clean. + +Deferred (Q-123 / follow-up): provisional VOXEL_RELIEF_SPAN_M=300 + the ≥8 m threshold need calibration; the ABSOLUTE elevation span is still the compressed elev_q/N base — a per-body hypsometric relief model is the later refinement. D-239 amended (T-1081); Q-123 item 4 updated.', NULL, '2026-06-28 18:41:37', '2026-06-28 18:41:37.651', '2026-06-28 18:41:37.651', NULL, 'ceea6fa43cd99a8a108633e74a1d05f0', 2) ON CONFLICT(hash) DO NOTHING; diff --git a/.pql/changelog/tickets/2026-06.sql b/.pql/changelog/tickets/2026-06.sql index 4d71204b6..f7f4ddd0f 100644 --- a/.pql/changelog/tickets/2026-06.sql +++ b/.pql/changelog/tickets/2026-06.sql @@ -3738,3 +3738,42 @@ DEFERRED (ticket item 3 — explicitly optional): per-layer PNG maps (the T-960 This is the D-245 enforcer; advisory now, ratchets to strict (BELIEVABILITY_STRICT) once Q-123 calibrates thresholds and T-1080/1081/1082 flip the criteria. Stack-review note (2026-06-28): the believability sampling-bias fix (contiguous-prefix -> seeded Fisher-Yates spread in sample_indices) is logically a T-1083 enforcer-correctness fix but landed in the T-1082 commit cdc55032d (found while verifying T-1082). git history records it accurately under the water-body work; flagged so a bisect lands in the expected place. The Fisher-Yates termination fix + drainage-proxy exclusion of water districts (stack review) are in the T-1080 review-fix commit.', 'done', 'high', NULL, 'server', 'D-239', '2026-06-28 05:55:19.712', '2026-06-28 16:02:46.338', NULL, '682f72f4c7d0310247d4cfe20a2f347e', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash); +INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FGSJVW30PSP8K0XAWV02X7M0', 'story', '06FGSJNP8YM537DJCC82ZD6544', 'Near-zero voxel relief (0-3 m) — generated terrain has no hills or landmarks', 'Probe evidence (Arbour @ yolo): voxel elevation 0-3 m across nearly all sampled districts (only one snow upland reached 42-49 m). Flat terrain => nothing to navigate by, no landmarks, no ''specificity of place''. Either the heightmap relief is being flattened by the 512x256 working downsample + district derivation, or detail-scatter (D-227, derive-don''t-store) is not producing the mid-scale relief it is supposed to invent at the voxel tier. Direction to discuss: the relief envelope taken from the heightmap, detail-scatter amplitude, and whether the elev_q -> elevation_m mapping is collapsing the range. Believability gap.', 'in_progress', 'high', NULL, 'server', 'D-227', '2026-06-28 05:54:50.008', '2026-06-28 16:08:41.256', NULL, 'e9823e01ab027322b5f6cd022dfedc1e', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash); +INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FGSJVW30PSP8K0XAWV02X7M0', 'story', '06FGSJNP8YM537DJCC82ZD6544', 'Near-zero voxel relief (0-3 m) — generated terrain has no hills or landmarks', 'Probe evidence (Arbour @ yolo): voxel elevation 0-3 m across nearly all sampled districts (only one snow upland reached 42-49 m). Flat terrain => nothing to navigate by, no landmarks, no ''specificity of place''. Either the heightmap relief is being flattened by the 512x256 working downsample + district derivation, or detail-scatter (D-227, derive-don''t-store) is not producing the mid-scale relief it is supposed to invent at the voxel tier. Direction to discuss: the relief envelope taken from the heightmap, detail-scatter amplitude, and whether the elev_q -> elevation_m mapping is collapsing the range. Believability gap. + +--- +Refinement (Si pass, 2026-06-28) — READY. Four compounding causes (all confirmed): +1. elev_q→elevation_m scale: generate_alluvial_plain uses `elev_q / 2` → whole-body relief is 0–50 m (pancake). Other flat families similar (/2,/3,/4). The dramatic families (cliff/fjord/gorge) carry real internal relief; the flat ones (alluvial/lava/dune/braided/meander) don''t. +2. detail_scatter (D-227) is applied ONLY at the district tier (derive_district → elev_q); it never reaches per-voxel elevation_m. So within a chunk relief = base ± 4 m micro-scatter = flat. The promised mid-scale (sub-district, ~tens-to-hundreds m) relief is missing at the voxel tier. +3. Envelope collapses to 0 on flat-slope districts (terrain_detail returns 0 when envelope==0) — by design ("no mountain on an authored plain"); consider a small floor so even gentle land rolls. +4. WaterBody correctly keeps elevation_m=0 (sea level) — must stay. + +PLAN (data-driven, tune via probe like moisture): +- Add MID-SCALE rolling relief at the voxel tier: a detail-scatter octave term (sub-district wavelengths ~256–2048 m) added to elevation_m for DRY-LAND columns, enveloped by district ruggedness (slope_q) so plains stay flat and rugged terrain rolls. f64 perturbation truncated to i32 (D-010, same as the warp). Applied centrally in derive_voxel_column (one site, like derive_cover) so all land families benefit. +- Bump the flat-family base scale from the compressed /N toward a believable span if the probe shows mountains still too low (per-body absolute relief = T-1032 follow-up). +- Add a VOXEL-RELIEF metric to believability.rs (mean within-chunk elevation range across sampled non-water districts) — the current "elevation relief" criterion measures DISTRICT elev_q spread (already passes), not voxel relief, so it does not catch T-1081. +- VERIFY: probe per-location elevation range rises from ~0-3 m; the new voxel-relief criterion flips green. Determinism: integer/D-010. +Files: voxel.rs (generators + central relief), detail_scatter.rs (sub-district wavelengths), believability.rs (metric).', 'in_progress', 'high', NULL, 'server', 'D-227', '2026-06-28 05:54:50.008', '2026-06-28 16:16:05.190', NULL, '1450d38cb522267b239186b96d8ad0a2', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash); +INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FGSJVW30PSP8K0XAWV02X7M0', 'story', '06FGSJNP8YM537DJCC82ZD6544', 'Near-zero voxel relief (0-3 m) — generated terrain has no hills or landmarks', 'Probe evidence (Arbour @ yolo): voxel elevation 0-3 m across nearly all sampled districts (only one snow upland reached 42-49 m). Flat terrain => nothing to navigate by, no landmarks, no ''specificity of place''. Either the heightmap relief is being flattened by the 512x256 working downsample + district derivation, or detail-scatter (D-227, derive-don''t-store) is not producing the mid-scale relief it is supposed to invent at the voxel tier. Direction to discuss: the relief envelope taken from the heightmap, detail-scatter amplitude, and whether the elev_q -> elevation_m mapping is collapsing the range. Believability gap. + +--- +Refinement (Si pass, 2026-06-28) — READY. Four compounding causes (all confirmed): +1. elev_q→elevation_m scale: generate_alluvial_plain uses `elev_q / 2` → whole-body relief is 0–50 m (pancake). Other flat families similar (/2,/3,/4). The dramatic families (cliff/fjord/gorge) carry real internal relief; the flat ones (alluvial/lava/dune/braided/meander) don''t. +2. detail_scatter (D-227) is applied ONLY at the district tier (derive_district → elev_q); it never reaches per-voxel elevation_m. So within a chunk relief = base ± 4 m micro-scatter = flat. The promised mid-scale (sub-district, ~tens-to-hundreds m) relief is missing at the voxel tier. +3. Envelope collapses to 0 on flat-slope districts (terrain_detail returns 0 when envelope==0) — by design ("no mountain on an authored plain"); consider a small floor so even gentle land rolls. +4. WaterBody correctly keeps elevation_m=0 (sea level) — must stay. + +PLAN (data-driven, tune via probe like moisture): +- Add MID-SCALE rolling relief at the voxel tier: a detail-scatter octave term (sub-district wavelengths ~256–2048 m) added to elevation_m for DRY-LAND columns, enveloped by district ruggedness (slope_q) so plains stay flat and rugged terrain rolls. f64 perturbation truncated to i32 (D-010, same as the warp). Applied centrally in derive_voxel_column (one site, like derive_cover) so all land families benefit. +- Bump the flat-family base scale from the compressed /N toward a believable span if the probe shows mountains still too low (per-body absolute relief = T-1032 follow-up). +- Add a VOXEL-RELIEF metric to believability.rs (mean within-chunk elevation range across sampled non-water districts) — the current "elevation relief" criterion measures DISTRICT elev_q spread (already passes), not voxel relief, so it does not catch T-1081. +- VERIFY: probe per-location elevation range rises from ~0-3 m; the new voxel-relief criterion flips green. Determinism: integer/D-010. +Files: voxel.rs (generators + central relief), detail_scatter.rs (sub-district wavelengths), believability.rs (metric). + +Implemented (2026-06-28, branch voxel-relief). The voxel tier now carries mid-scale relief (D-243 §2): a `voxel_relief` fBm pass at the 0.25–2 km octave band (detail_scatter.rs), body-global SeedDomain::VoxelRelief seed, added post-dispatch in derive_voxel_column, truncated to integer metres (D-010). + +Root cause refined during impl: the coarse heightmap yields slope_q ≈ 0 even on high ground (Arbour max slope_q = 13), so the relief envelope is `slope_q*3 + elev_q` (elevation drives ruggedness), not slope alone. Applied to the FLAT families only (AlluvialPlain/LavaField/BraidedDelta/DuneStrand/MeanderReach) — the dramatic families (Cliff/Fjord/Gorge) already carry strong internal relief and a position-varying field warps their features; WaterBody stays at sea level. + +Verification: new believability metric `contrast.voxel_relief_m` (mean within-district elevation range over a 2 km transect — a single 64 m chunk is narrower than the relief band). Arbour @ yolo: voxel relief 5 m → 59 m, criteria 6/8 → 7/8 (remaining fail = the Q-123 vegetation-denominator item, not relief). Edict also 59 m. 1586 lib tests + believability harness (determinism + golden) green; clippy -D warnings clean. + +Deferred (Q-123 / follow-up): provisional VOXEL_RELIEF_SPAN_M=300 + the ≥8 m threshold need calibration; the ABSOLUTE elevation span is still the compressed elev_q/N base — a per-body hypsometric relief model is the later refinement. D-239 amended (T-1081); Q-123 item 4 updated.', 'in_progress', 'high', NULL, 'server', 'D-227', '2026-06-28 05:54:50.008', '2026-06-28 18:41:37.651', NULL, 'bd8abb0e2034f7dcb9a811f4d40b9eff', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash); diff --git a/governance/decisions/architecture.md b/governance/decisions/architecture.md index 4197cdc63..f5394c9ac 100644 --- a/governance/decisions/architecture.md +++ b/governance/decisions/architecture.md @@ -1778,6 +1778,7 @@ Technical foundation decisions that constrain implementation: engine, client-ser - **Implementation note (T-1025/T-1027, 2026-06-08):** §6's frozen 17-zone `MorphologyZone` enum is realised in `server/src/simulation/generator.rs` (`repr(u8)`, discriminant-pinned). The §5 8-family gated classifier + §7 compatibility-matrix invariants live in `derive_morphology_zone` (`region_profile.rs`). **16 of the 17 zones are reachable at RegionProfile scale; `BraidedPlain` is the exception** — distinguishing it from `Delta` needs a lithology signal (§8 Gravel→braided) that `RegionProfile` does not carry, so `BraidedPlain` is **deferred to ChunkContext** sub-classification. The §7 compatibility invariant is enforced as a **classifier-gate-ordering** property (a build-time test that a single region cannot yield a forbidden pair), per §7's "build-time test" language; genuine cross-region sharp seams (cliff↔fjord, lithology faults) remain permitted. §2 climate-derived fields (`precipitation_class`, `glaciation_grade`, `vegetation_class`) all derive from the temperature(+moisture) primitive (T-1025). - **Amendment (T-1082, 2026-06-28):** §5's family set grows from **8 to 9** — a `WaterBody` generator family is added for the water zones **OpenOcean / Lake / TidalFlat**, which previously fell through to the dry-land `AlluvialPlain` fallback (the D-245 believability bug: oceans rendered as dry forested land — `water=Dry` + `Forest` on the sea). `WaterBody` lays `Water::Deep`/`Shallow` (a Shallow shoal band on the district-anchored coast line `coast_anchor_m`, Deep beyond; TidalFlat is all-Shallow), a seabed `TerrainMaterial` (`Rock` on steep districts / `Sand` on gentle / `Wetland` for tidal mud), `Vegetation::Barren`, the water surface at `elevation_m = 0` (the §8 mouths-at-sea-level convention), and seasonal `Ice` via `derive_cover` (frozen seas). This **refines §6**: open ocean / lake / tidal flat are still derived zone *labels*, but their *generator* is now `WaterBody`, not `AlluvialPlain` — so §6's "tidal flat … not [a] distinct generator famil[y]" is superseded for the dispatch of those three zones. `Wetland` stays a land zone on `AlluvialPlain` (a marsh — saturated ground, not open water). Lives in `server/src/atlas/voxel.rs` (`MorphologyFamily::WaterBody` + `generate_water_body`, dispatched by `zone_to_family`); verified by the T-1083 believability harness (water-renders-wet flips PASS for Arbour + Edict). The §5 8-family decision-tree classifier (`derive_morphology_zone`) is unchanged — this is a voxel-tier *generator* family, selected by zone, not a new RegionProfile classifier branch. - **Amendment (T-1080, 2026-06-28):** §2's **moisture** primitive is now a **body ceiling × per-district spatial gradient**, not a single body constant. The hydrosphere+atmosphere value (`derive_moisture_q`) is the *wettest* a district on the body can be; per-district **latitude** (equator wet → pole dry), **elevation** (orographic / rain-shadow), and **continentality** (`100 − ocean_fraction_q`, coast wet → interior dry) subtract from it. Integer (D-010); coefficients in `ClimateConstants` / `climate_constants.toml` `[moisture_gradient]` (provisional, Q-123 calibrates). This mirrors D-240's latitude-graded *temperature* model and fixes the T-1080 believability bug (`moisture_q = 80` for all 2 048 districts → uniform vegetation/terrain — "nothing to fuzz"). It **propagates**: `precipitation_class`, `vegetation_class`, and the `morphology_zone` Wetland gate all read `moisture_q`, so those diversify for free. Lives in `district_profile.rs::derive_moisture_q`; verified by the T-1083 believability harness (moisture-gradient flips PASS, `distinct` 1 → ~49 on Arbour). The body-scale ceiling keeps each world's character; the gradient varies it within. +- **Amendment (T-1081, 2026-06-28):** the voxel tier now carries **mid-scale relief** (D-243 §2's invented terrain). Before, the family generators set `elevation_m` from `elev_q` at a compressed scale plus only ±4 m per-voxel micro-scatter — the walkable surface read flat (the D-245 "0–3 m, no hills to navigate by" bug). A `voxel_relief` pass (`detail_scatter.rs`, the `terrain_detail` fBm at the **0.25–2 km octave band** rather than the district 4–40 km band, body-global `SeedDomain::VoxelRelief` seed, position-keyed; the `f64` perturbation truncated to integer metres before assignment — D-010) is added post-dispatch in `derive_voxel_column`. Two load-bearing choices: **(a) the relief envelope is `slope_q·3 + elev_q`, not slope alone** — the coarse heightmap (~40–78 km/px) yields `slope_q ≈ 0` even on high ground (observed max 13 on Arbour), so gating on slope would invent nothing; folding elevation in makes high terrain read rugged and coastal flats stay gentle. **(b) Only the flat families** (AlluvialPlain / LavaField / BraidedDelta / DuneStrand / MeanderReach) take it — CliffCoast / FjordWall / IncisedGorge already generate strong internal relief and a position-varying field would warp those features (e.g. drown a gorge's wall-to-floor drop); WaterBody stays at sea level. The relief **span** (`VOXEL_RELIEF_SPAN_M`) and the believability voxel-relief threshold are provisional (Q-123 calibrates, like the moisture coefficients). Verified by the T-1083 harness — a new `voxel_relief` contrast metric (mean within-district elevation range across a district-spanning transect, since a single 64 m sample chunk is narrower than the relief band) flips PASS (≈59 m on Arbour + Edict). **Deferred:** the *absolute* elevation span is still the compressed `elev_q/N` base — a per-body hypsometric relief model (so a body's true max relief sets the ceiling, not a constant) is a later refinement; the mid-scale relief gives navigable hills now without it. - **Cross-reference:** [D-227](#d-227) (derive-don't-store voxel model), [D-228](#d-228) (composite tile axes / cohesion / seasonal state), [D-210](#d-210) (temperature proxy — formalised), [D-203](#d-203) (BodyWorldState cache), [D-206](#d-206) (background analysis pass), [D-208](#d-208) (drainage / D8), [D-010](#d-010) (determinism), [D-234](#d-234) (street/footprint geometry — consumes morphology), [D-142](content.md#d-142) (zone types), [D-217](#d-217) (tile condition), [Q-102](../questions/architecture.md#q-102) (cohesion = the warp), [Q-103](../questions/architecture.md#q-103) (mutator schema — open), [Q-105](../questions/architecture.md#q-105) (seasonal/clock state — temperature/ElevationDelta forward contract) --- diff --git a/governance/questions/architecture.md b/governance/questions/architecture.md index 0c4d99107..11fce28bc 100644 --- a/governance/questions/architecture.md +++ b/governance/questions/architecture.md @@ -477,7 +477,7 @@ Technical foundation questions: engine, protocols, data structures, performance, 1. **Sampling power** — probes per body, number of bodies, number of seeds that constitute "anywhere" (enough that one lucky/unlucky draw is not decisive). 2. **Non-stationarity** — the similarity metric for "two same-class km² differ" (histogram distance / feature-vector cosine) and the *minimum* dissimilarity that counts as non-repeating; plus a tile-period autocorrelation ceiling (the Netherlands "no two km² alike" rule, made numeric). 3. **Intra-class variety** — minimum distinct micro-features per sampled patch (K), per terrain class (T-1084). - 4. **Relief floor** — minimum relief variance, *conditioned on the macro terrain's expectation* (a plain's floor ≠ a montane floor) (T-1081). + 4. **Relief floor** — minimum relief variance, *conditioned on the macro terrain's expectation* (a plain's floor ≠ a montane floor) (T-1081). *(T-1081 landed the metric — `contrast.voxel_relief_m`, mean within-district elevation range over a 2 km transect — with a provisional `≥ 8 m` threshold and a provisional `VOXEL_RELIEF_SPAN_M = 300`; calibrate both, and make the floor macro-conditioned rather than a single global number. Also pending: the **absolute** elevation span is still the compressed `elev_q/N` base — a per-body hypsometric model would set true relief ceilings.)* 5. **Coherence** — the vegetation ↔ (moisture / water-distance / slope / aspect) correlation floor; the water-zone-renders-wet check (T-1082); the allowed "impossible combo" set (should be empty). *(T-1082 review note: the current 50% "water renders wet" floor is weak for ocean-dominant bodies — calibrate it **per zone**: post-T-1082 an `OpenOcean` district should render wet ~100% (Dry is impossible), while a mixed coastal district may legitimately read partial.)* Also calibrate the **vegetation-present** denominator: dividing by *all* sampled districts (incl. water) makes the criterion structurally unreachable for a 2/3-ocean world — it should be **land-relative** (exclude water-body districts). 6. **Climate-appropriateness** — how "*structured-sparse* passes / *blank* fails" is measured on cold/arid bodies, so a frozen world is not failed for being legitimately sparse. 7. **Pass budget + ratchet** — the % of land probes that must pass to ship (budgeted), and the trigger to ratchet advisory → strict (hard push-gate block). diff --git a/server/src/atlas/believability.rs b/server/src/atlas/believability.rs index 3f7e1a229..5f4543801 100644 --- a/server/src/atlas/believability.rs +++ b/server/src/atlas/believability.rs @@ -67,6 +67,11 @@ const WATER_PRESENCE_Q: i32 = 10; const VOXEL_SAMPLE_DISTRICTS: usize = 64; /// Stride over a district's representative 64 m chunk (8 → an 8×8 = 64-voxel sample). const VOXEL_SAMPLE_STRIDE: usize = 8; +/// Chunk offsets within a district (×64 m ⇒ 128 m … 1920 m) for the mid-scale relief +/// transect (T-1081) — samples the elevation range a character crosses over the +/// district's ~2 km span (the voxel-relief band), which the single 64 m sample chunk +/// is too narrow to register. +const RELIEF_TRANSECT_CHUNK_OFFSETS: [i32; 6] = [2, 8, 14, 20, 26, 30]; // --------------------------------------------------------------------------- // Report types (serde — golden-snapshot-able) @@ -101,6 +106,12 @@ pub struct ContrastMetrics { pub vegetation_classes: usize, /// Distinct voxel `TerrainMaterial` values across the voxel sample. pub terrain_materials: usize, + /// Mean within-district voxel elevation *range* (metres) across a district-spanning + /// transect of the sampled land districts (T-1081). The walkable-relief screen: + /// near 0 on flat terrain (the "0–3 m, no hills" symptom), tens of metres once the + /// voxel tier carries mid-scale relief. Measured over the 0.25–2 km band a character + /// traverses — not the single 64 m sample chunk, which is too narrow to register it. + pub voxel_relief_m: i32, } /// Coherence checks — does the world render as a *caused* place? Pass/fail counts @@ -171,6 +182,7 @@ pub fn analyze( .map(|d| format!("{:?}", d.vegetation_class)), ), terrain_materials: 0, // filled from the voxel sample below + voxel_relief_m: 0, // filled from the relief transect below }; // ── Voxel-derived metrics over a deterministic seeded spread of districts ── @@ -181,6 +193,9 @@ pub fn analyze( let mut terrain_set: BTreeSet = BTreeSet::new(); let mut coh = CoherenceMetrics::default(); let mut sampled = 0usize; + // Mid-scale relief accumulators (T-1081): sum of per-district transect ranges over + // land districts, and the count contributing, → mean within-district relief. + let (mut relief_range_sum, mut relief_district_count) = (0i64, 0i64); let keys: Vec = districts.keys().copied().collect(); for idx in sample_indices(world_seed, keys.len(), VOXEL_SAMPLE_DISTRICTS) { @@ -243,8 +258,43 @@ pub fn analyze( coh.drainage_monotonic += 1; } } + + // Mid-scale relief transect (T-1081): the dry-ground elevation range across the + // district's ~2 km span — where the voxel tier's mid-scale relief lives. The + // single 64 m sample chunk above is narrower than the relief's 0.25–2 km band, + // so it would read flat even on rolling terrain; this transect is the screen. + // Only land-dominant districts count (≥3 of 6 transect points dry) — coastal + // districts with one stray dry point would otherwise contribute a 0 range and + // bias the mean toward "flat" on an ocean world (a measurement artifact, not + // flat land). + let (mut relief_lo, mut relief_hi, mut relief_dry) = (i32::MAX, i32::MIN, 0i32); + for &co in &RELIEF_TRANSECT_CHUNK_OFFSETS { + let cpos = ( + dp.0 * CHUNKS_PER_DISTRICT + co, + dp.1 * CHUNKS_PER_DISTRICT + co, + ); + let cctx = derive_chunk_context(world_seed, body_id, prof, cpos, None); + let tx = cpos.0 * CHUNK_M + CHUNK_M / 2; + let ty = cpos.1 * CHUNK_M + CHUNK_M / 2; + let col = derive_voxel_column(world_seed, body_id, prof, &cctx, tx, ty); + if col.water == Water::Dry { + relief_lo = relief_lo.min(col.elevation_m); + relief_hi = relief_hi.max(col.elevation_m); + relief_dry += 1; + } + } + if relief_dry >= 3 { + relief_range_sum += (relief_hi - relief_lo) as i64; + relief_district_count += 1; + } } + let voxel_relief_m = if relief_district_count > 0 { + (relief_range_sum / relief_district_count) as i32 + } else { + 0 + }; + BelievabilityReport { body_id: body_id.to_string(), world_seed, @@ -252,6 +302,7 @@ pub fn analyze( voxel_sampled_districts: sampled, contrast: ContrastMetrics { terrain_materials: terrain_set.len(), + voxel_relief_m, ..contrast_scalar }, coherence: coh, @@ -280,6 +331,11 @@ pub fn evaluate_criteria(r: &BelievabilityReport) -> Vec { pass: c.elev_q.spread() >= 10, detail: format!("elev_q spread={}", c.elev_q.spread()), }, + Criterion { + name: "voxel relief", + pass: c.voxel_relief_m >= 8, + detail: format!("mean within-district relief {} m", c.voxel_relief_m), + }, Criterion { name: "morphology variety", pass: c.morphology_zones >= 2, diff --git a/server/src/atlas/detail_scatter.rs b/server/src/atlas/detail_scatter.rs index 479361f39..e317c0e2f 100644 --- a/server/src/atlas/detail_scatter.rs +++ b/server/src/atlas/detail_scatter.rs @@ -24,6 +24,12 @@ use crate::seed::splitmix64; /// coarsest (~33 km) the heightmap itself carries the shape. const OCTAVE_WAVELENGTHS_M: [f64; 4] = [32_768.0, 16_384.0, 8_192.0, 4_096.0]; +/// Voxel-tier octave wavelengths in metres — the ≈0.25–2 km band that the +/// district-tier [`terrain_detail`] (4–40 km) is too coarse to carry and the +/// per-voxel scatter (<64 m) is too fine to reach. This is the [`voxel_relief`] +/// band: the rolling/ridged hills a *walking character* navigates by (T-1081). +const VOXEL_OCTAVE_WAVELENGTHS_M: [f64; 4] = [2_048.0, 1_024.0, 512.0, 256.0]; + /// Deterministic lattice value in `[-1, 1)` for an integer noise cell. #[inline] fn lattice(seed: u64, ix: i64, iy: i64) -> f64 { @@ -64,6 +70,40 @@ fn value_noise(seed: u64, wx: f64, wy: f64, wavelength_m: f64) -> f64 { /// Returns roughly `[-envelope, +envelope]`, smaller and smoother as `ruggedness` /// drops toward 0 (gentle flats), larger and ridged as it rises toward 1. pub fn terrain_detail(seed: u64, wx: f64, wy: f64, envelope: f64, ruggedness: f64) -> f64 { + enveloped_fbm(seed, wx, wy, envelope, ruggedness, &OCTAVE_WAVELENGTHS_M) +} + +/// The voxel-tier mid-scale relief perturbation in `[0,1]`-normalized units +/// (multiply by the body's relief span for metres) — the ≈0.25–2 km rolling/ridged +/// hills a walking character navigates by (T-1081). Identical envelope/ruggedness +/// contract as [`terrain_detail`]; only the octave band differs +/// ([`VOXEL_OCTAVE_WAVELENGTHS_M`] vs the district [`OCTAVE_WAVELENGTHS_M`]). +/// +/// Seed must be **body-global** (constant across the body) — the position +/// `(wx, wy)` carries the variation. A per-voxel seed would make every voxel a +/// fresh lattice (white noise, not smooth hills). +pub fn voxel_relief(seed: u64, wx: f64, wy: f64, envelope: f64, ruggedness: f64) -> f64 { + enveloped_fbm( + seed, + wx, + wy, + envelope, + ruggedness, + &VOXEL_OCTAVE_WAVELENGTHS_M, + ) +} + +/// Shared adaptive-fBm core for [`terrain_detail`] and [`voxel_relief`] — the only +/// difference between the two tiers is the octave wavelength band. Returns the +/// enveloped, ruggedness-modulated perturbation (roughly `[-envelope, +envelope]`). +fn enveloped_fbm( + seed: u64, + wx: f64, + wy: f64, + envelope: f64, + ruggedness: f64, + wavelengths: &[f64], +) -> f64 { let env = envelope.clamp(0.0, 1.0); let rug = ruggedness.clamp(0.0, 1.0); if env == 0.0 { @@ -73,7 +113,7 @@ pub fn terrain_detail(seed: u64, wx: f64, wy: f64, envelope: f64, ruggedness: f6 let mut sum = 0.0; let mut amp = 1.0; let mut norm = 0.0; - for (i, &wl) in OCTAVE_WAVELENGTHS_M.iter().enumerate() { + for (i, &wl) in wavelengths.iter().enumerate() { let mut n = value_noise( seed.wrapping_add((i as u64).wrapping_mul(0x1000)), wx, @@ -159,4 +199,38 @@ mod tests { (base - near).abs() ); } + + // ── voxel_relief (T-1081): same contract, sub-district band ────────────── + + #[test] + fn voxel_relief_deterministic_and_bounded() { + let a = voxel_relief(42, 12_345.0, -6_789.0, 0.6, 0.5); + let b = voxel_relief(42, 12_345.0, -6_789.0, 0.6, 0.5); + assert_eq!(a, b); + // Same envelope rule + amplitude ceiling as terrain_detail. + assert_eq!(voxel_relief(7, 1_000.0, 2_000.0, 0.0, 1.0), 0.0); + for i in 0..400 { + let v = voxel_relief(99, i as f64 * 137.0, i as f64 * -91.0, 0.5, 1.0); + assert!(v.abs() <= 0.5 + 1e-9, "v={v} exceeded envelope at {i}"); + } + } + + #[test] + fn voxel_relief_varies_at_sub_district_scale() { + // The voxel band must produce DISTINCT relief within a single 2 km district — + // exactly the variation terrain_detail's 4–40 km band cannot carry, and the + // flatness T-1081 fixes. Body-global seed; position carries the variation. Steps + // are deliberately non-aligned with the octave wavelengths to avoid aliasing. + let seed = 1234; + let vals: Vec = (0..16) + .map(|i| voxel_relief(seed, i as f64 * 137.0, i as f64 * 89.0, 0.7, 0.6)) + .collect(); + let min = vals.iter().cloned().fold(f64::INFINITY, f64::min); + let max = vals.iter().cloned().fold(f64::NEG_INFINITY, f64::max); + assert!( + max - min > 0.1, + "voxel relief is near-flat across a district: range {}", + max - min + ); + } } diff --git a/server/src/atlas/voxel.rs b/server/src/atlas/voxel.rs index 4a0faa9ba..16629448e 100644 --- a/server/src/atlas/voxel.rs +++ b/server/src/atlas/voxel.rs @@ -333,6 +333,15 @@ fn zone_to_family(zone: &MorphologyZone) -> MorphologyFamily { /// ## Returns /// /// A fully derived `VoxelColumn` with all D-228 axes populated. +/// T-1081: the metre span of the voxel-tier mid-scale relief field — the amplitude +/// ceiling for the ≈0.25–2 km rolling/ridged hills added to the family base +/// elevation. Actual relief at a voxel is `voxel_relief(...) * SPAN`, which the +/// envelope (district ruggedness) scales down on gentle terrain, so this is the +/// steep-terrain ceiling, not a uniform amplitude. Provisional — tuned against the +/// believability probe (T-1079) like the moisture gradient; per-body *absolute* +/// elevation span is a later refinement (the T-1080 climate-fields lineage). +const VOXEL_RELIEF_SPAN_M: i32 = 300; + pub fn derive_voxel_column( world_seed: u64, body_id: &str, @@ -427,6 +436,58 @@ pub fn derive_voxel_column( } }; + // ── 3b. Voxel-tier mid-scale relief (T-1081, D-227 / D-243 §2) ──────── + // The family generators set a coarse district-derived base elevation plus only + // ±4 m of per-voxel micro-scatter, leaving the walkable surface near-flat — no + // hills or landmarks to navigate by (the T-1081 symptom). Add the mid-scale + // (≈0.25–2 km) rolling/ridged relief the district→voxel seam does not otherwise + // reach (D-243 §2): detail-scatter enveloped by district ruggedness (slope_q) so + // an authored plain stays flat and rugged terrain gains real relief. The relief + // field is body-global (SeedDomain::VoxelRelief, position-keyed); the f64 + // perturbation is truncated to integer metres before assignment (D-010, + // truncate-before-decision — the same sanctioned f64 path as the domain warp). + // + // Skipped for the WaterBody family: open ocean / lake / tidal-flat surfaces stay + // at sea level (elevation 0). River channels inside the land families ride up + // with their banks — value noise is smooth at 1 m steps, so a channel voxel and + // its bank get near-identical relief and the family's relative channel cut holds. + // Only the FLAT families take the mid-scale relief. The dramatic families + // (CliffCoast / FjordWall / IncisedGorge) already generate strong internal relief + // from their own geometry — layering a position-varying field over them would warp + // those features (e.g. drown a gorge's wall-to-floor drop under a relief swell). + // WaterBody stays at sea level. The flat families are the ones that read flat and + // need invented relief; their only feature is a 3–15 m channel, narrow enough that + // the smooth relief shifts it bodily with its banks. + let takes_mid_scale_relief = matches!( + family, + MorphologyFamily::AlluvialPlain + | MorphologyFamily::LavaField + | MorphologyFamily::BraidedDelta + | MorphologyFamily::DuneStrand + | MorphologyFamily::MeanderReach + ); + if takes_mid_scale_relief { + let relief_seed = SeedChain::for_body(world_seed, body_id) + .derive(SeedDomain::VoxelRelief, 0) + .seed(); + // Relief envelope (T-1081): the coarse heightmap (~40–78 km/px) carries almost + // no slope at this tier (slope_q tops out near 0.1 on a real body), so gating on + // slope_q alone would kill the invented relief everywhere. Combine slope with + // elevation — high ground reads rugged, coastal lowlands stay gentle — so + // detail-scatter raises hills where the body has high terrain and keeps flats + // soft. (Tectonic-class ridge sharpening is a later refinement.) + let signal = (district_eff.slope_q * 3 + district_eff.elev_q).clamp(0, 100) as f64 / 100.0; + let relief = crate::atlas::detail_scatter::voxel_relief( + relief_seed, + voxel_x as f64, + voxel_y as f64, + signal, + signal, + ); + let relief_m = (relief * VOXEL_RELIEF_SPAN_M as f64) as i32; // truncate (D-010) + column.elevation_m = (column.elevation_m + relief_m).max(0); + } + // ── 4. Seasonal cover overlay (D-239 §3, T-1030) ────────────────────── // Applied AFTER family dispatch: the 8 family generators produce the base // axes (terrain/water/vegetation/elevation); cover is a separate orthogonal @@ -2235,6 +2296,86 @@ mod tests { assert_eq!(a, b); } + // ── Voxel-tier mid-scale relief (T-1081) ───────────────────────────────── + + #[test] + fn voxel_relief_gives_rugged_land_navigable_hills() { + // T-1081: a rugged land district must produce mid-scale relief across a ~2 km + // span far exceeding the ±4 m micro-scatter (the "flat 0–3 m, no hills" symptom). + let mut district = alluvial_district(); + district.slope_q = 60; // rugged → relief headroom + let chunk = alluvial_chunk(&district); + let mut elevs = Vec::new(); + for i in 0..12 { + let p = i * 256; // step across the voxel-relief band (0.25–2 km) + let col = derive_voxel_column(42, "GJ1c", &district, &chunk, p, p); + if col.water == Water::Dry { + elevs.push(col.elevation_m); + } + } + assert!(elevs.len() >= 4, "expected mostly dry-land samples"); + let min = *elevs.iter().min().unwrap(); + let max = *elevs.iter().max().unwrap(); + assert!( + max - min > 20, + "rugged district should have navigable mid-scale relief, got {min}..{max} m" + ); + } + + #[test] + fn voxel_relief_respects_low_flat_envelope() { + // No slope AND no elevation → no terrain signal → no invented relief (the + // envelope rule: don't sprout hills on a flat sea-level plain). Elevation stays + // the family base (elev_q/2 = 0 m) plus only the ±4 m micro-scatter / channel cut. + let mut district = alluvial_district(); + district.slope_q = 0; + district.elev_q = 0; + let chunk = alluvial_chunk(&district); + for i in 0..12 { + let p = i * 256; + let col = derive_voxel_column(42, "GJ1c", &district, &chunk, p, p); + assert!( + col.elevation_m <= 4, + "low-flat district gained mid-scale relief: {} m", + col.elevation_m + ); + } + } + + #[test] + fn voxel_relief_from_elevation_even_with_zero_slope() { + // The T-1081 root cause: the coarse heightmap gives slope_q ≈ 0 even on high + // terrain, so ELEVATION must drive the relief envelope. A high district with + // slope_q == 0 must still gain navigable hills (else high ground reads flat). + let mut district = alluvial_district(); + district.slope_q = 0; + district.elev_q = 90; // high ground, coarse-heightmap-flat + let chunk = alluvial_chunk(&district); + let mut elevs = Vec::new(); + for i in 0..12 { + let p = i * 256; + let col = derive_voxel_column(42, "GJ1c", &district, &chunk, p, p); + if col.water == Water::Dry { + elevs.push(col.elevation_m); + } + } + let range = elevs.iter().max().unwrap() - elevs.iter().min().unwrap(); + assert!( + range > 20, + "high terrain must gain relief despite slope_q=0, got range {range} m" + ); + } + + #[test] + fn voxel_relief_is_deterministic() { + let mut district = alluvial_district(); + district.slope_q = 50; + let chunk = alluvial_chunk(&district); + let a = derive_voxel_column(42, "GJ1c", &district, &chunk, 777, 1234); + let b = derive_voxel_column(42, "GJ1c", &district, &chunk, 777, 1234); + assert_eq!(a.elevation_m, b.elevation_m); + } + // ----------------------------------------------------------------------- // TerrainMaterial discriminant pins // ----------------------------------------------------------------------- diff --git a/server/src/seed.rs b/server/src/seed.rs index 27955ee3b..e454f3cec 100644 --- a/server/src/seed.rs +++ b/server/src/seed.rs @@ -110,6 +110,11 @@ pub enum SeedDomain { /// so the per-cluster cover hash can never collide with the per-voxel terrain /// stream — domain separation guarantees no freeze-to-terrain correlation. Cover = 10, + /// Voxel-tier mid-scale relief noise field (T-1081, D-243 §2). A body-global, + /// position-keyed elevation perturbation at sub-district wavelengths (≈0.25–2 km), + /// keyed by a single constant id (one field per body). Distinct domain so the + /// relief lattice can never correlate with the per-voxel terrain or cover streams. + VoxelRelief = 11, } /// A position in the deterministic seed tree (D-224). diff --git a/server/tests/golden/believability.json b/server/tests/golden/believability.json index cbc3c9837..323a2457d 100644 --- a/server/tests/golden/believability.json +++ b/server/tests/golden/believability.json @@ -27,7 +27,8 @@ }, "morphology_zones": 9, "vegetation_classes": 3, - "terrain_materials": 4 + "terrain_materials": 4, + "voxel_relief_m": 49 }, "coherence": { "water_districts": 49, @@ -66,7 +67,8 @@ }, "morphology_zones": 9, "vegetation_classes": 1, - "terrain_materials": 4 + "terrain_materials": 4, + "voxel_relief_m": 45 }, "coherence": { "water_districts": 25,