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>
171 lines
6.9 KiB
Python
171 lines
6.9 KiB
Python
#!/usr/bin/env python3
|
||
"""
|
||
Kallast (GJ144d) heightmap + globe generator — Settled Reach
|
||
|
||
Reads planet parameters from the wiki (wiki/star-systems/GJ-144/index.md),
|
||
runs the FBM+Voronoi simulation pipeline, and produces:
|
||
|
||
1. Annotated equirectangular heightmap PNG (geographic features only)
|
||
2. Globe render PNG (ray-traced sphere with terrain data wrapped onto it)
|
||
|
||
Both outputs share the same simulation run — terrain is computed once.
|
||
|
||
Pipeline:
|
||
body_definition_parser → parse wiki → body_def dict
|
||
planet_simulation → FBM+Voronoi elevation, temperature, moisture,
|
||
hillshade, rivers, biome classification
|
||
render_heightmap → annotated equirectangular PNG
|
||
planet_renderer → ray-traced globe PNG (terrain-driven surface)
|
||
|
||
Usage:
|
||
python3 generate_kallast.py [options]
|
||
|
||
--small Fast iteration: 1024×512 heightmap, 512×512 globe
|
||
--render-mode cartographic (NG map style) | photographic (orbital)
|
||
|
||
Default outputs:
|
||
/mnt/user-data/outputs/GJ144d_heightmap.png (4096×2048)
|
||
/mnt/user-data/outputs/GJ144d_globe.png (2048×2048)
|
||
"""
|
||
|
||
import argparse
|
||
import os
|
||
import sys
|
||
import time
|
||
|
||
# ── Prototype pipeline on path ────────────────────────────────────────────────
|
||
_HERE = os.path.dirname(os.path.abspath(__file__))
|
||
_PROTO = os.path.join(_HERE, "prototype")
|
||
if _PROTO not in sys.path:
|
||
sys.path.insert(0, _PROTO)
|
||
|
||
from body_definition_parser import parse_system
|
||
from planet_simulation import simulate
|
||
from render_heightmap import render_heightmap
|
||
from planet_renderer import render_globe
|
||
|
||
# ── Default paths ─────────────────────────────────────────────────────────────
|
||
# Sprint worktree root is two levels above the spike dir.
|
||
_SPRINT_ROOT = os.path.normpath(os.path.join(_HERE, "..", ".."))
|
||
DEFAULT_WIKI = os.path.join(_SPRINT_ROOT, "wiki", "star-systems", "GJ-144", "index.md")
|
||
DEFAULT_OUT_HM = "/mnt/user-data/outputs/GJ144d_heightmap.png"
|
||
DEFAULT_OUT_GL = "/mnt/user-data/outputs/GJ144d_globe.png"
|
||
DEFAULT_BODY = "GJ144d"
|
||
|
||
|
||
def main():
|
||
parser = argparse.ArgumentParser(
|
||
description="Generate Kallast (GJ144d) heightmap + globe",
|
||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||
epilog=__doc__,
|
||
)
|
||
parser.add_argument(
|
||
"--output", default=DEFAULT_OUT_HM, metavar="PATH",
|
||
help="Heightmap PNG output path")
|
||
parser.add_argument(
|
||
"--globe-output", dest="globe_output",
|
||
default=DEFAULT_OUT_GL, metavar="PATH",
|
||
help="Globe PNG output path")
|
||
parser.add_argument(
|
||
"--body-id", default=DEFAULT_BODY, metavar="ID",
|
||
help="Body ID to render (default: GJ144d)")
|
||
parser.add_argument(
|
||
"--wiki", default=DEFAULT_WIKI, metavar="PATH",
|
||
help="Path to GJ-144 wiki index.md")
|
||
parser.add_argument(
|
||
"--out-w", type=int, default=4096, metavar="N",
|
||
help="Heightmap output width (default: 4096)")
|
||
parser.add_argument(
|
||
"--out-h", type=int, default=2048, metavar="N",
|
||
help="Heightmap output height (default: 2048)")
|
||
parser.add_argument(
|
||
"--globe-size", type=int, default=2048, metavar="N",
|
||
help="Globe output size in px (default: 2048)")
|
||
parser.add_argument(
|
||
"--render-mode",
|
||
choices=["cartographic", "photographic"],
|
||
default="cartographic",
|
||
help="Heightmap colour mode (default: cartographic)")
|
||
parser.add_argument(
|
||
"--small", action="store_true",
|
||
help="Fast iteration: 1024×512 heightmap, 512×512 globe")
|
||
args = parser.parse_args()
|
||
|
||
if args.small:
|
||
args.out_w = 1024
|
||
args.out_h = 512
|
||
args.globe_size = 512
|
||
|
||
t_start = time.time()
|
||
|
||
# ── 1. Parse body definition from wiki ───────────────────────────────────
|
||
print(f"Parsing wiki: {args.wiki}")
|
||
if not os.path.exists(args.wiki):
|
||
raise SystemExit(
|
||
f"Wiki not found: {args.wiki}\n"
|
||
"Pass --wiki <path/to/index.md> if running from a non-standard location.")
|
||
|
||
bodies = parse_system(args.wiki)
|
||
body_def = next((b for b in bodies if b["id"] == args.body_id), None)
|
||
if body_def is None:
|
||
available = [b["id"] for b in bodies]
|
||
raise SystemExit(
|
||
f"Body {args.body_id!r} not found in wiki.\n"
|
||
f"Available: {available}\n"
|
||
"Use --body-id to specify a different body.")
|
||
|
||
pc = body_def.get("planet_class", "?")
|
||
seed = body_def.get("seed", 0)
|
||
print(f" {body_def['id']} class={pc} seed={seed}")
|
||
|
||
# ── 2. Simulate terrain ──────────────────────────────────────────────────
|
||
print("Simulating terrain…")
|
||
t0 = time.time()
|
||
terrain = simulate(body_def)
|
||
sim_t = time.time() - t0
|
||
|
||
if not terrain:
|
||
raise SystemExit(f"{args.body_id} is a gas giant — no terrain to render.")
|
||
|
||
sea = terrain["sea_level"]
|
||
nriv = len(terrain["rivers"])
|
||
print(f" Done in {sim_t:.1f}s "
|
||
f"sea_level={sea:.3f} rivers={nriv}")
|
||
|
||
if terrain.get("temperature_clamped"):
|
||
raw_K = terrain.get("temperature_raw_K", "?")
|
||
band = terrain.get("temperature_band_K", [])
|
||
print(f" T_raw={raw_K}K clamped to {band} for {pc}")
|
||
|
||
# ── 3. Render heightmap ──────────────────────────────────────────────────
|
||
size_str = f"{args.out_w}×{args.out_h}"
|
||
print(f"Rendering heightmap {size_str} ({args.render_mode})…")
|
||
t1 = time.time()
|
||
hm = render_heightmap(
|
||
body_def, terrain,
|
||
out_w=args.out_w, out_h=args.out_h,
|
||
render_mode=args.render_mode)
|
||
hm_t = time.time() - t1
|
||
|
||
os.makedirs(os.path.dirname(os.path.abspath(args.output)), exist_ok=True)
|
||
hm.save(args.output, format="PNG")
|
||
print(f" Saved: {args.output} ({hm_t:.1f}s)")
|
||
|
||
# ── 4. Render globe ──────────────────────────────────────────────────────
|
||
print(f"Rendering globe {args.globe_size}×{args.globe_size}…")
|
||
t2 = time.time()
|
||
glob = render_globe(body_def, terrain=terrain, size=args.globe_size)
|
||
gl_t = time.time() - t2
|
||
|
||
os.makedirs(os.path.dirname(os.path.abspath(args.globe_output)), exist_ok=True)
|
||
glob.save(args.globe_output, format="PNG")
|
||
print(f" Saved: {args.globe_output} ({gl_t:.1f}s)")
|
||
|
||
total = time.time() - t_start
|
||
print(f"\nTotal: {total:.1f}s "
|
||
f"(sim={sim_t:.1f}s hm={hm_t:.1f}s glob={gl_t:.1f}s)")
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()
|