Files
settled-reach/spikes/heightmap-pipeline/PIPELINE.md
T
jpmschweitzerandClaude Opus 4.6 1c6c5b9978 feat(assets): heightmap pipeline spike — prototype + test inputs
PO-built prototype (FBM terrain, Whittaker biomes, procedural globe)
with test body definitions for all planet types. Replaces pyplatec
approach. Spike validates the pipeline architecture for batch #817.

Includes handover doc, 9 test body definitions, and updated spike
pipeline documentation. Stale pyplatec outputs removed.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-06 15:54:35 +02:00

284 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Heightmap Pipeline — Spike Documentation
**Ticket:** #778
**Author:** Araminta
**Date:** 2026-04-05
**Status:** Spike complete — awaiting review before batch (#794, Sprint 33)
**Outputs:**
- `GJ144d_heightmap.png` (4096×2048px, equirectangular, cartographic)
- `GJ144d_globe.png` (2048×2048px, ray-traced sphere)
---
## What This Spike Validates
This spike validates the full annotated heightmap + globe pipeline from wiki data
through to deliverable PNGs. Both outputs are produced from a single simulation run.
- Wiki parsing → body_def dict (body_definition_parser)
- FBM+Voronoi tectonic simulation → elevation grid (no external dependencies)
- Temperature model (stellar physics + class clamping)
- Moisture model (Hadley cells + ocean proximity + rain shadow)
- Hillshade (gradient-based)
- River network (D8 steepest descent, polyline output)
- Extended Whittaker biome classification (absolute Kelvin — no frozen-world tropics)
- Annotated equirectangular heightmap render
- Ray-traced globe render with terrain-driven surface, PBR lighting, atmosphere
**The pipeline is confirmed viable for batch production (#794).**
---
## Planet: Kallast (GJ144d)
Selected because it showcases temperate terrain variety and the wiki narrative
provides a direct visual brief.
| Property | Value | Source |
|----------|-------|--------|
| Planet ID | `GJ144d` | wiki bodies table |
| System | Ran (GJ 144) | wiki |
| Star | K2V | wiki |
| Planet class | temperate | wiki |
| Hydrosphere | ocean | wiki |
| Atmosphere | breathable | wiki |
| Gravity | 0.95g | wiki |
| Population | 2,000,000,000 | wiki |
| Terrain character | Amber continental shelves, irrigation channels | wiki narrative |
---
## Pipeline Architecture
```
Input: wiki/star-systems/GJ-144/index.md
↓ body_definition_parser.parse_system()
Stage 1: Body definition
Parses bodies table → body_def dict per planet
Derives: seed (MD5 of body_id), distance_au (Kepler),
land_fraction (hydrosphere→ HYDRO_LAND), polar_ice_lat,
axial_tilt (CLASS_TILT), tectonics, atmosphere
"rand" sentinels → seeded randomization within class bounds
Runtime: <0.1s
Stage 2: Elevation (planet_simulation.compute_elevation)
Primary continent mask: FBM with domain warp (3 independent fields)
Tectonic ridges: Voronoi plate boundaries + domain warp (curves)
Detail noise: FBM high-frequency layer
Dynamic sea level: np.percentile(elev, ocean_pct)
CRITICAL: right-skewed FBM output requires percentile-derived threshold.
Fixed fraction does not produce the target land coverage.
Erosion: slope-weighted gaussian smoothing, N passes (varies by tectonics)
Polar ice flattening at high latitudes
All longitude noise uses 3D circle projection for seamless wrapping.
Runtime: ~3-5s (512×256 grid)
Stage 3: Temperature (planet_simulation.compute_temperature)
Stellar equilibrium temp (Stefan-Boltzmann) → greenhouse offset →
CLASS_T_BAND clamp → latitude gradient → elevation lapse rate →
class offset → geothermal boost
CRITICAL: output is absolute Kelvin, not normalised.
Biome classification uses raw K values to avoid frozen-world misclassification.
Normalisation happens AFTER biome classification for renderer display.
Stage 4: Moisture (planet_simulation.compute_moisture)
Hadley cell bands (ITCZ + subtropical high + polar) + ocean proximity +
temperature contribution × rain shadow factor
Class/hydrosphere scale factors applied per planet type
Stage 5: Hillshade (planet_simulation.compute_hillshade)
Gradient-based normal → dot product with sun direction (315°az, 45°alt)
Used for elevation shading in both heightmap and globe renders
Stage 6: Rivers (planet_simulation.compute_rivers)
D8 steepest-descent flow from high-moisture local maxima
Output: list of (row, col) polylines in simulation grid coordinates
Max rivers capped per planet class (arid=3, frozen=2, default=12)
River list stored in terrain dict; renderer scales coords to output resolution
Stage 7: Biome classification (planet_simulation.compute_biome)
Extended Whittaker table lookup in absolute Kelvin × moisture [0,1]
Ocean depth bands (0=deep, 1=mid, 2=shallow)
Frozen ocean override (class 26 = ice shelf, distinct from land ice)
Modifier stack: geothermal, chemosynthetic, UV, substrate overrides
26 biome classes + 1 unused slot (0=deep ocean … 26=ice shelf)
Stage 8: Heightmap render (render_heightmap.render_heightmap)
Layer compositing order:
1. Biome base colour (cartographic or photographic palette)
2. Ocean depth gradient (3-stop blend: shallow → mid → deep)
3. Elevation shading on land [0.88, 1.06] factor
4. Hillshade blend (0.55 hs + 0.45 flat) — land only
5. Coastline ring (binary_dilation XOR, 2px dark border)
6. Rivers: PIL polylines scaled from grid coords to output pixels
Width 1-3px scaled by path length (longer = wider)
7. Lat/lon grid every 30° (12% white overlay, 2px)
8. Title panel (name, class, star, orbit, atmo, hydro)
9. Biome legend (present-only swatches, natural features only)
Output resolution: configurable, default 4096×2048. UI_SCALE = W/1024.
Stage 9: Globe render (planet_renderer.render_globe)
Ray-traced sphere (camera at z=3, looking at origin)
Terrain-driven surface: biome→ photographic palette (27 classes, 0-26)
Extended colour array (_EXTENDED_BIOME_COLORS) covers full class range
including exotic classes 20-26. BIOME_COLORS (0-19) used only for
procedural fallback when terrain=None.
Elevation shading on land cells
Full lighting: smoothstep diffuse, terminator warm scatter, ocean specular,
atmospheric rim glow, night-side ambient
Optional cloud layer: moisture-driven coverage + gaussian blur
Star field background, atmosphere halo
Output: RGBA PNG (alpha=255 on sphere+ring pixels, 0 on background)
Default size: 2048×2048
```
---
## Two-Layer Model
Heightmaps are **geographic only**. Human data lives in JSON sidecars.
```
GJ144d_heightmap.png ← geographic render: terrain, rivers, biomes, grid
GJ144d_settlements.json ← human layer: city names + grid coordinates (Phase 3)
```
The PNG renders: terrain classification colors, elevation shading, river network,
lat/lon grid, title panel, biome legend.
The PNG does NOT render: settlements, roads, freight elevators, city labels,
irrigation channels, or any human-activity markers. Those exist in the JSON
sidecar and are overlaid by the atlas app (Phase 3) when the map is interactive.
**Rationale:** A geographic heightmap is a stable base layer. The human overlay
changes as the simulation runs. Keeping them separate means the PNG can be
regenerated from terrain data without recomputing settlement placement.
---
## Configuration
The body_def drives all simulation parameters. Key fields parsed from wiki:
| Field | Source | Effect |
|-------|--------|--------|
| `planet_class` | wiki type column | CLASS_T_BAND, erosion passes, river cap |
| `hydrosphere` | wiki hydro column | land_fraction, moisture scale |
| `atmosphere` | wiki atmo column | greenhouse offset, moisture computation |
| `gravity_g` | wiki gravity column | informs max_elevation_km |
| `star.type` | system profile | luminosity, star tint on globe |
| `orbit.distance_au` | derived (Kepler) | equilibrium temperature |
No manual per-planet configuration required for batch. All parameters derive
from the wiki's bodies table.
---
## Running
```bash
# Standard (4096×2048 heightmap + 2048×2048 globe)
python3 spikes/heightmap-pipeline/generate_kallast.py
# Fast iteration (1024×512 + 512×512)
python3 spikes/heightmap-pipeline/generate_kallast.py --small
# Photographic colour mode (orbital appearance, dark/muted)
python3 spikes/heightmap-pipeline/generate_kallast.py --render-mode photographic
# High resolution (8192×4096 heightmap)
python3 spikes/heightmap-pipeline/generate_kallast.py --out-w 8192 --out-h 4096
# Custom output paths
python3 spikes/heightmap-pipeline/generate_kallast.py \
--output /tmp/kallast_hm.png \
--globe-output /tmp/kallast_globe.png
```
Dependencies: `scipy`, `numpy`, `Pillow` (no external simulation engine required)
Individual prototype modules also have standalone CLIs:
```bash
# Parse body defs from wiki (inspect what the parser produces)
python3 prototype/body_definition_parser.py wiki/star-systems/GJ-144/index.md --out-dir /tmp/defs/
# Simulate only (inspect terrain grids)
python3 prototype/planet_simulation.py /tmp/defs/GJ144d_def.json --save-grids
# Render heightmap from body_def JSON
python3 prototype/render_heightmap.py /tmp/defs/GJ144d_def.json [--small]
```
---
## Known Issues / Calibration Notes for Batch
1. **Sea level uses percentile, not fixed fraction.**
FBM output is non-uniformly distributed — `np.percentile(elev, ocean_pct)` gives
correct land coverage. Fixed fractions do NOT work reliably.
2. **Temperature is absolute Kelvin throughout.**
The Whittaker biome table uses K, not normalised [0,1]. This is intentional:
prevents a frozen world's "warm" pole from classifying as tropical.
Normalisation happens after biome classification for renderer display only.
3. **Class clamping (CLASS_T_BAND).**
If stellar physics puts a temperate world outside its expected band (e.g. wiki
says "temperate" but distance_au makes it hotter), temperature is clamped.
The script logs a warning when clamping occurs. Check these during batch review.
4. **Rivers are polylines, not pixel masks.**
The simulation outputs `(row, col)` paths. The renderer scales to output resolution
and draws with PIL's anti-aliased line tool. Width 1-3px scales with path length.
No pre-upscale painting / post-LANCZOS issues (old pyplatec pipeline concern).
5. **Globe uses photographic palette.**
When terrain data is provided, `_EXTENDED_BIOME_COLORS` (27 entries, photographic
values) is used instead of `BIOME_COLORS` (19 entries, procedural-style values).
The extended array covers exotic classes 20-26 (lava fields, chemosynthetic mats,
ash fields, ice shelves) which the old array silently clipped to index 19.
---
## Batch Estimate (#794)
Grid size: 512×256 (prototype default). Output: 4096×2048 heightmap + 2048 globe.
Timing per planet (single-threaded, approximate):
| Stage | Time |
|-------|------|
| Parse wiki | <0.1s |
| Simulation (elev+temp+moist+hs+rivers+biome) | ~36s |
| Heightmap render (4096×2048) | ~24s |
| Globe render (2048×2048) | ~36s |
| **Total per planet** | **~816s** |
Full batch of 301 planets: ~4080 minutes single-threaded.
Parallelisable across all CPU cores (no shared state) — ~1020 min on 4 cores.
For faster batch: `--globe-size 1024` halves globe render time with acceptable
fidelity for thumbnail/wiki use. Full 2048 globe recommended for atlas app.
---
## Open Questions for Review
Before starting batch (#794):
1. **Output resolution.** Default 4096×2048 for heightmap and 2048 for globe.
Is this sufficient for the Phase 3 atlas app, or do we need 8192×4096?
8192×4096 is available via `--out-w 8192 --out-h 4096` — adds ~4× render time.
2. **Render mode for batch.** Cartographic (NG map style) or photographic (orbital)?
Cartographic reads more clearly as a map; photographic looks more realistic as a
wiki thumbnail. Could produce both.
3. **River calibration per class.** max_rivers=12 is the default. Arid worlds get 3,
frozen get 2. Is this density appropriate? Can compare against wiki narrative.
4. **Cloud layer.** Globe render supports moisture-driven clouds (enabled via body_def
`clouds.enabled: true`). Should this be enabled for inhabited temperate worlds?