chat1/05 renamed 01_pass1_port to 02_pass1_port, bringing it into line with the authoring-task naming convention. ReviewBatchTool and ReliefRenderTool both defaulted ISLA_SOURCE to the old name, so running either without an explicit override found no .f32 dumps. That failed quietly, which is the reason to fix it rather than document it: HeightField.Load returns null on a missing file, so ReliefRenderTool silently regenerated instead of loading, and ReviewBatchTool skipped 1_noise_grayscale entirely with only a printed warning. Nothing corrupted; it just cost generation time and dropped a view, both easy to miss. Both defaults now name 02_pass1_port — where TerrainGenTool writes by default, BatchRoot(task 2, "pass1_port"). Generator output and renderer source agree again. ISLA_SOURCE stays overridable and unchanged in shape. Note in passing that it is a FULL folder name, prefix included, not a bare descriptor: it names an existing folder to read rather than composing a new one, so it does not go through BatchRoot. Commented at both sites so the asymmetry with ISLA_BATCH reads as deliberate. Verified end to end, not just by build: with no ISLA_SOURCE set, ReliefRenderTool reports "loaded in 173 ms" against 02_pass1_port, and ReviewBatchTool produces 1_noise_grayscale again.
224 lines
10 KiB
C#
224 lines
10 KiB
C#
using System;
|
||
using System.Collections.Generic;
|
||
using System.IO;
|
||
using System.Text;
|
||
using Godot;
|
||
using IslaApocalypse.Core;
|
||
|
||
namespace IslaApocalypse.Tools
|
||
{
|
||
/// <summary>
|
||
/// The taste-gate renderer: re-colours existing height fields into shaded-relief maps across a
|
||
/// small set of named looks, and writes an INDEX.md for browsing.
|
||
///
|
||
/// ⚠ IT RE-COLOURS; IT DOES NOT REGENERATE. Height fields are LOADED from the `.f32` dumps a
|
||
/// generation run left behind, so a look change provably cannot move the terrain. Only when a
|
||
/// dump is missing for the requested size — a showpiece at 4096, say, where no dump exists —
|
||
/// does it generate one, deterministically from the same seed. Same seed, same island.
|
||
///
|
||
/// ═══ RUNNING IT ═══
|
||
///
|
||
/// Godot_v4.7.2-stable_mono_linux.x86_64 --headless \
|
||
/// --path ~/celerNexus/islaApocalypse-v2 res://Tools/Scenes/ReliefRenderTool.tscn
|
||
///
|
||
/// ISLA_MAPSIZE side in columns (default 2048)
|
||
/// ISLA_SEEDS comma-separated positive (default: the 4 pinned seeds)
|
||
/// ISLA_LOOKS comma-separated look names (default: atlas,relief,dusk)
|
||
/// ISLA_TASK authoring task number (default 3)
|
||
/// ISLA_BATCH descriptor, NO prefix (default "relief_taste")
|
||
/// ISLA_SOURCE batch to read .f32 from (default 02_pass1_port)
|
||
/// ISLA_DUMP_RAW "1" to also dump .f32 when a field had to be generated
|
||
/// </summary>
|
||
public partial class ReliefRenderTool : Node
|
||
{
|
||
private static readonly int[] DefaultSeeds = { 1063685222, 20260819, 777001, 424242 };
|
||
|
||
public override void _Ready()
|
||
{
|
||
ToolingPaths.Configure(OS.GetUserDataDir());
|
||
|
||
int mapSize = EnvInt("ISLA_MAPSIZE", 2048);
|
||
int[] seeds = EnvSeeds("ISLA_SEEDS", DefaultSeeds);
|
||
int task = EnvInt("ISLA_TASK", 3); // the task that authored this batch
|
||
string batch = EnvStr("ISLA_BATCH", "relief_taste");
|
||
// ⚠ A FULL batch folder name, prefix included — it names an EXISTING folder rather than
|
||
// composing a new one, so it is not run through BatchRoot. Tracks TerrainGenTool's
|
||
// default output: BatchRoot(task 2, "pass1_port") = 02_pass1_port.
|
||
string source = EnvStr("ISLA_SOURCE", "02_pass1_port");
|
||
bool dumpRaw = EnvStr("ISLA_DUMP_RAW", "0") == "1";
|
||
LookConfig[] looks = SelectLooks(EnvStr("ISLA_LOOKS", null));
|
||
|
||
// ⚠ Composed by BatchRoot, never free-form: the prefix is the AUTHORING TASK number,
|
||
// not a counter, and a descriptor carrying its own prefix is refused. → Tools/README.md.
|
||
string batchRoot = ToolingPaths.BatchRoot(task, batch);
|
||
string sourceRoot = Path.Combine(ToolingPaths.BatchesRoot, source);
|
||
DirAccess.MakeDirRecursiveAbsolute(batchRoot);
|
||
DirAccess.MakeDirRecursiveAbsolute(ToolingPaths.BatchScratch(batchRoot));
|
||
|
||
GD.Print("==================================================================");
|
||
GD.Print(" SHADED RELIEF — hypsometric tint + hillshade (Phase 1, the look)");
|
||
GD.Print("==================================================================");
|
||
GD.Print($"MapSize : {mapSize}");
|
||
GD.Print($"seeds : {string.Join(", ", seeds)}");
|
||
GD.Print($"looks : {string.Join(", ", Array.ConvertAll(looks, l => l.Name))}");
|
||
GD.Print($"source : {sourceRoot} (.f32 dumps; generated only if absent)");
|
||
GD.Print($"out : {batchRoot}");
|
||
GD.Print("------------------------------------------------------------------");
|
||
foreach (var l in looks) GD.Print($" {l}");
|
||
GD.Print("==================================================================");
|
||
|
||
var rows = new List<string>();
|
||
|
||
foreach (int seed in seeds)
|
||
{
|
||
// --- get the height field: LOAD if a dump exists, generate only as a fallback ---
|
||
ulong t0 = Time.GetTicksMsec();
|
||
string dumpPath = Path.Combine(sourceRoot, $"{seed}_full", "height.f32");
|
||
float[,] height = HeightField.Load(dumpPath, mapSize);
|
||
string origin;
|
||
|
||
if (height != null)
|
||
{
|
||
origin = "loaded";
|
||
}
|
||
else
|
||
{
|
||
var cfg = new TerrainGenConfig { MapSize = mapSize, Seed = seed, VariantLabel = "full" };
|
||
Pass1Result r = Topography.Generate(cfg);
|
||
height = r.Height;
|
||
origin = "generated";
|
||
if (dumpRaw)
|
||
{
|
||
string dir = Path.Combine(batchRoot, $"{seed}_source");
|
||
DirAccess.MakeDirRecursiveAbsolute(dir);
|
||
HeightField.Save(height, mapSize, Path.Combine(dir, "height.f32"));
|
||
}
|
||
}
|
||
ulong tLoad = Time.GetTicksMsec() - t0;
|
||
|
||
float hMin = float.MaxValue, hMax = float.MinValue;
|
||
for (int x = 0; x < mapSize; x++)
|
||
for (int y = 0; y < mapSize; y++)
|
||
{
|
||
float v = height[x, y];
|
||
if (v < hMin) hMin = v;
|
||
if (v > hMax) hMax = v;
|
||
}
|
||
|
||
GD.Print($"\n seed {seed} [{origin} in {tLoad} ms] h[{hMin:F3} .. {hMax:F3}]");
|
||
|
||
foreach (LookConfig look in looks)
|
||
{
|
||
ulong t1 = Time.GetTicksMsec();
|
||
string dir = Path.Combine(batchRoot, $"{seed}_{look.Name}");
|
||
DirAccess.MakeDirRecursiveAbsolute(dir);
|
||
ReliefRenderer.SavePng(height, mapSize, look, Path.Combine(dir, "relief.png"));
|
||
ulong ms = Time.GetTicksMsec() - t1;
|
||
|
||
GD.Print($" {look.Name,-8} -> {dir}/relief.png {ms} ms");
|
||
rows.Add($"| `{seed}_{look.Name}` | {seed} | {look.Name} | {look.Palette} | " +
|
||
$"{look.ZExaggeration:F0} | {look.LightAzimuth:F0}°/{look.LightAltitude:F0}° | " +
|
||
$"{look.HillshadeStrength:F2} | {ms} ms |");
|
||
}
|
||
}
|
||
|
||
WriteIndex(batchRoot, mapSize, seeds, looks, source, rows);
|
||
|
||
GD.Print("\n==================================================================");
|
||
GD.Print($" DONE — {rows.Count} renders in {batchRoot}");
|
||
GD.Print("==================================================================");
|
||
GetTree().Quit(0);
|
||
}
|
||
|
||
private static LookConfig[] SelectLooks(string csv)
|
||
{
|
||
LookConfig[] all = LookConfig.Variants();
|
||
if (string.IsNullOrWhiteSpace(csv)) return all;
|
||
|
||
var picked = new List<LookConfig>();
|
||
foreach (string want in csv.Split(',', StringSplitOptions.RemoveEmptyEntries))
|
||
foreach (LookConfig l in all)
|
||
if (string.Equals(l.Name, want.Trim(), StringComparison.OrdinalIgnoreCase))
|
||
picked.Add(l);
|
||
return picked.Count > 0 ? picked.ToArray() : all;
|
||
}
|
||
|
||
private static void WriteIndex(string batchRoot, int mapSize, int[] seeds,
|
||
LookConfig[] looks, string source, List<string> rows)
|
||
{
|
||
var (landLo, landHi) = ReliefPalette.LandAnchors;
|
||
var sb = new StringBuilder();
|
||
sb.AppendLine("# Batch — shaded relief, the taste gate (Phase 1)");
|
||
sb.AppendLine();
|
||
sb.AppendLine("**Presentation only.** These are the *same* height fields as");
|
||
sb.AppendLine($"`{source}` — loaded from its `.f32` dumps, not regenerated. Nothing here can move the");
|
||
sb.AppendLine("terrain; only its colours and its lighting differ.");
|
||
sb.AppendLine();
|
||
sb.AppendLine($"- **MapSize:** {mapSize}");
|
||
sb.AppendLine($"- **Sea colour boundary:** 0.15 — a *colour* boundary, **not a water surface.** No water is modelled (Phase 2).");
|
||
sb.AppendLine($"- **Palette anchors (FIXED across every image):** land {landLo} .. {landHi}; sea compressed by `d/(d+{ReliefPalette.SeaCompression})`");
|
||
sb.AppendLine($"- **Seeds:** {string.Join(", ", seeds)}");
|
||
sb.AppendLine();
|
||
sb.AppendLine("## ⭐ Pick a look");
|
||
sb.AppendLine();
|
||
sb.AppendLine("Three, deliberately — a subjective gate drowns in a wall of near-duplicates. Each pair");
|
||
sb.AppendLine("isolates one question:");
|
||
sb.AppendLine();
|
||
sb.AppendLine("| Look | Palette | Z-exag | Light | Strength | The question it answers |");
|
||
sb.AppendLine("|---|---|---|---|---|---|");
|
||
foreach (LookConfig l in looks)
|
||
{
|
||
string q = l.Name switch
|
||
{
|
||
"atlas" => "the baseline — classic physical-atlas plate, tint-forward",
|
||
"relief" => "**vs `atlas`:** how strong should the relief be? (same palette + light)",
|
||
"dusk" => "**vs `atlas`:** warmer palette and a lower sun — better, or too much?",
|
||
_ => "",
|
||
};
|
||
sb.AppendLine($"| `{l.Name}` | {l.Palette} | {l.ZExaggeration:F0} | {l.LightAzimuth:F0}°/{l.LightAltitude:F0}° | {l.HillshadeStrength:F2} | {q} |");
|
||
}
|
||
sb.AppendLine();
|
||
sb.AppendLine("Compare the **same seed** across the three folders. `1063685222` is the reference's own");
|
||
sb.AppendLine("commented seed and the one used throughout tasks 02–03.");
|
||
sb.AppendLine();
|
||
sb.AppendLine("> ⚠ **The seabed is raw, and that is expected — not unfinished-because-broken.** The");
|
||
sb.AppendLine("> submarine coast shelf and the offshore islets are DEFERRED Phase-2 items; they act only");
|
||
sb.AppendLine("> below sea level and are judged once water renders. Relief is deliberately dialled back");
|
||
sb.AppendLine("> underwater so the eye is not dragged to the one surface that is knowingly incomplete.");
|
||
sb.AppendLine();
|
||
sb.AppendLine("> ⚠ **No biome colour here.** This is height + slope + above/below sea, on a continuous");
|
||
sb.AppendLine("> ramp. White at the top is snow-*coloured cartography*, not a snow biome.");
|
||
sb.AppendLine();
|
||
sb.AppendLine("## Renders");
|
||
sb.AppendLine();
|
||
sb.AppendLine("| Folder | Seed | Look | Palette | Z-exag | Light | Strength | Time |");
|
||
sb.AppendLine("|---|---|---|---|---|---|---|---|");
|
||
foreach (string r in rows) sb.AppendLine(r);
|
||
sb.AppendLine();
|
||
sb.AppendLine("`scratch/` is persistent and is never cleaned.");
|
||
|
||
using var f = Godot.FileAccess.Open(Path.Combine(batchRoot, "INDEX.md"), Godot.FileAccess.ModeFlags.Write);
|
||
if (f == null) { GD.PrintErr("could not write INDEX.md"); return; }
|
||
f.StoreString(sb.ToString());
|
||
}
|
||
|
||
private static string EnvStr(string k, string fallback)
|
||
{
|
||
string v = System.Environment.GetEnvironmentVariable(k);
|
||
return string.IsNullOrWhiteSpace(v) ? fallback : v;
|
||
}
|
||
|
||
private static int EnvInt(string k, int fallback)
|
||
=> int.TryParse(EnvStr(k, null) ?? "", out int v) ? v : fallback;
|
||
|
||
private static int[] EnvSeeds(string k, int[] fallback)
|
||
{
|
||
string v = EnvStr(k, null);
|
||
if (v == null) return fallback;
|
||
var o = new List<int>();
|
||
foreach (string p in v.Split(',', StringSplitOptions.RemoveEmptyEntries))
|
||
if (int.TryParse(p.Trim(), out int s) && s > 0) o.Add(s);
|
||
return o.Count > 0 ? o.ToArray() : fallback;
|
||
}
|
||
}
|
||
}
|