Files
2026-09-12 19:04:57 +02:00

150 lines
7.8 KiB
Python
Raw Permalink 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.
#!/usr/bin/env python3
"""Render TerrainAtlas305GameTests native exports. No synthesized terrain or image smoothing.
python scripts/terrain_atlas.py --output build/atlas305 <native-json> [<native-json> ...]
See docs/terrain-atlas.md for acquisition, provenance and sampling limits.
"""
import argparse
import hashlib
import json
from pathlib import Path
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
from matplotlib.colors import TwoSlopeNorm
import numpy as np
LABELS = {
"plateaus27": "Plateaux 24 / 27",
"global301": "Bruit 3D global · 30.1",
"stretch304": "Étirement vertical · 30.4",
"current305": "Plateaux + déformation 3D · 30.5",
"current306": "Relief légèrement renforcé · 30.6",
}
def save(fig, directory, name):
fig.savefig(directory / f"{name}.png", dpi=150, facecolor="white")
fig.savefig(directory / f"{name}.pdf", facecolor="white")
plt.close(fig)
def render(source, output):
raw = source.read_bytes()
data = json.loads(raw)
if data["schema"] != 1 or len(data["variants"]) != 4 or any(k not in LABELS for k in data["variants"]):
raise ValueError(f"Unsupported atlas schema/variants: {source}")
labels = {key: LABELS[key] for key in data["variants"]}
target = output / source.stem
target.mkdir(parents=True, exist_ok=True)
title = f"Sanctuary · diamètre {data['diameter']} · graine {data['seed']}"
provenance = (f"Champ natif Minecraft {data['minecraft']} · {data['version']} · {data['revision']}\n"
"Avant eau, matériaux de surface, végétation et structures. Le plafond de construction reste Y640.")
variants = list(data["variants"].values())
maps = []
for variant in variants:
columns = np.asarray(variant["columns"], dtype=float)
xs, zs = np.unique(columns[:, 0]), np.unique(columns[:, 1])
if columns.shape[0] != len(xs) * len(zs):
raise ValueError("Incomplete native map grid")
maps.append(columns.reshape(len(zs), len(xs), 5))
step = data["map_step"]
extent = (xs[0] - step / 2, xs[-1] + step / 2, zs[-1] + step / 2, zs[0] - step / 2)
fig, axes = plt.subplots(3, 4, figsize=(18, 14), layout="constrained", sharex=True, sharey=True)
fig.suptitle(title + "\nRelief et épaisseur de roche", fontsize=18)
metrics = [(2, "Altitude du sommet (Y)", "terrain", 0, data["domain_y"][1]),
(3, "Épaisseur solide cumulée (blocs)", "viridis", 0, 300),
(4, "Roche continue sous le sommet (blocs)", "magma", 0, 100)]
for row, (index, label, cmap, vmin, vmax) in enumerate(metrics):
for col, (key, grid) in enumerate(zip(labels, maps)):
ax = axes[row, col]
values = np.ma.masked_where(grid[:, :, 2] == 0, grid[:, :, index])
image = ax.imshow(values, extent=extent, origin="upper", interpolation="nearest",
cmap=cmap, vmin=vmin, vmax=vmax, aspect="equal")
ax.set_facecolor("#e8edf0")
ax.set_xlabel("X (blocs)")
if col == 0:
ax.set_ylabel("Z (blocs) · nord en haut")
if row == 0:
ax.set_title(labels[key], fontsize=11)
ax.plot(0, 0, "+", color="black", ms=8, label="Origine")
fig.colorbar(image, ax=axes[row, :], shrink=.85, label=label, extend="max")
fig.supxlabel(f"Pas horizontal : {step} blocs · vertical : {data['vertical_step']} blocs · gris = aucun solide échantillonné.\n" + provenance, fontsize=9)
save(fig, target, "maps")
section_count = len(variants[0]["sections"])
fig, axes = plt.subplots(4, section_count, figsize=(20, 12), layout="constrained", sharex=True, sharey=True)
fig.suptitle(title + "\nCoupes de densité · proportions spatiales conservées", fontsize=18)
for row, (key, variant) in enumerate(zip(labels, variants)):
for col, section in enumerate(variant["sections"]):
ax = axes[row, col]
density = np.asarray(section["density"], dtype=float) / data["density_scale"]
dy = section["step"]
x = section["min_x"] + np.arange(density.shape[1]) * dy
y = section["max_y"] - np.arange(density.shape[0]) * dy
image = ax.imshow(density, extent=(x[0]-dy/2, x[-1]+dy/2, y[-1]-dy/2, y[0]+dy/2),
interpolation="nearest", cmap="RdBu_r", norm=TwoSlopeNorm(0, -.3, .3), aspect="equal")
ax.contour(x, y, density, levels=[0], colors="#262626", linewidths=.35)
if row == 0:
ax.set_title(f"Plan vertical Z = {section['z']}")
if col == 0:
ax.set_ylabel(labels[key] + "\nY (blocs)", fontsize=10)
ax.set_xlabel("X (blocs)")
ax.set_ylim(0, data["domain_y"][1] + 1)
fig.colorbar(image, ax=axes, shrink=.6, label="Densité sans unité · bleu : vide (≤ 0) · rouge : roche (> 0)", extend="both")
fig.supxlabel("Coupes échantillonnées tous les 2 blocs, sans interpolation daffichage ; isosurface zéro en noir.\n" + provenance, fontsize=9)
save(fig, target, "sections")
upper = data["variants"].get("current306", {}).get("upper_sections", [])
if upper:
fig, axes = plt.subplots(1, len(upper), figsize=(5*len(upper), 6), layout="constrained", squeeze=False)
fig.suptitle(title + "\nMorceaux de terrain supérieur · densité native", fontsize=15)
for ax, cut in zip(axes[0], upper):
values = np.array(cut["solid"])
dy=cut["step"]
ax.imshow(values, cmap="Greys", vmin=0, vmax=1, interpolation="nearest", aspect="equal",
extent=(cut["min_x"]-1,cut["min_x"]+values.shape[1]*dy-1,cut["max_y"]-values.shape[0]*dy+1,cut["max_y"]+1))
ax.set_title(f"Z = {cut['z']}"); ax.set_xlabel("X");ax.set_ylabel("Y")
fig.supxlabel("Noir : roche · blanc : air. Aucune interpolation daffichage. Avant végétation et structures.",fontsize=9)
save(fig,target,"upper-terrain")
summary = {"seed": data["seed"], "diameter": data["diameter"], "variants": {}}
base = maps[0][:, :, 2]
common = np.logical_and.reduce([m[:, :, 2] >= 200 for m in maps])
for key, grid in zip(labels, maps):
land = grid[:, :, 2] > 0
uplands = base >= 200
summary["variants"][key] = {
"sampled_land_columns": int(land.sum()),
"max_sampled_y": float(grid[:, :, 2].max()),
"original_uplands_within_2_blocks_fraction": float((np.abs(grid[:, :, 2]-base)[uplands] <= 2).mean()),
"common_upper_columns": int(common.sum()),
"median_roof_thickness_common_upper_columns": float(np.median(grid[:, :, 4][common])),
"roof_at_most_8_blocks_common_upper_fraction": float((grid[:, :, 4][common] <= 8).mean()),
}
(target / "summary.json").write_text(json.dumps(summary, indent=2) + "\n")
(target / "provenance.json").write_text(json.dumps({
"source": source.name, "source_sha256": hashlib.sha256(raw).hexdigest(),
"script_sha256": hashlib.sha256(Path(__file__).read_bytes()).hexdigest(),
"matplotlib": matplotlib.__version__, "numpy": np.__version__,
"version": data["version"], "revision": data["revision"], "process_id": data["process_id"],
"scope": data["scope"], "map_step": step, "vertical_step": data["vertical_step"],
"density_quantization": 1 / data["density_scale"], "density_plot_saturation": [-.3, .3],
}, indent=2) + "\n")
print(json.dumps(summary))
return summary
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("sources", nargs="+", type=Path)
parser.add_argument("--output", type=Path, required=True)
args = parser.parse_args()
summaries = [render(source, args.output) for source in args.sources]
(args.output / "summary.json").write_text(json.dumps(summaries, indent=2) + "\n")
if __name__ == "__main__":
main()