feat(simulation): voxel-tier mid-scale relief — navigable hills (T-1081)
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) <noreply@anthropic.com>
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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)
|
||||
|
||||
---
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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<String> = 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<DistrictPos> = 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<Criterion> {
|
||||
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,
|
||||
|
||||
@@ -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<f64> = (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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user