islaApocalypse-v2/Tools/Scripts/LookConfig.cs
beezm b1cf282295 Phase 1: shaded relief and the Hispaniola palette — the grin gate
Presentation only. Nothing here touches Topography, the noise, or any generation
constant; the renderer is handed a height field LOADED from a .f32 dump and has no
way to produce one, so a look change provably cannot move the terrain.

Hillshade: Horn 3x3, cell size 1, edges clamped (never wrapped — the Trench border
is a wall, not a seam). ZExaggeration is required rather than optional: measured
median land slope is 0.22 degrees unexaggerated, so the island shades as a flat
plane. Chosen from a measured slope table — zex 75 gives 16/28/37 deg at land
median/p90/p99, which reads as natural relief. It is a look dial; raw units are not
metres and no code may read it as if they were.

Palette: stops placed on the MEASURED height distribution, not spread linearly.
Land is bottom-heavy (median 0.456, p99 1.113, max 1.415 over 8.6M land columns
across four seeds), so a linear ramp would spend 99% of its range on 1% of the land
and the map would read as green with a few white dots. Bathymetry is compressed by
d/(d+0.35) so the shallow shelf gets the range and the featureless abyss flattens.
Anchors are absolute and fixed across the batch — a map coloured on its own min/max
cannot be compared with its neighbour, and comparison is the point of a batch.

Blend: the naive tint x hillshade darkens everything by 29% before any slope is
involved, because flat ground shades to sin(altitude). So shade is normalized by
its flat-ground value first — flat terrain keeps its true tint and only SLOPE moves
the colour — then shadows multiply while highlights screen toward white. That
asymmetry is the difference between a colour ramp and a map you would frame.

Relief fades to zero with depth below sea. Not only taste: the deep floor is the
Trench, a synthetic wall whose gradient is ~1.5x the p99 land gradient, so shading
it faithfully draws a bright rim around the map and lights the abyss with noise
mottle. The fade puts relief on the near-shore shelf where the bathymetry is real.

Three named looks (atlas / relief / dusk), gated like any other change and kept
deliberately few — iteration fatigue on a subjective gate is a real failure mode.
Each pair isolates one question.

HeightMapRenderer.cs is retired: its raw I/O moved to HeightField.cs and its ramp
to ReliefPalette.cs, so there is one palette everywhere and the generator's own
quick-look is coloured identically to a beauty render.

No biome words, no classification, no water. The 0.15 line is a colour boundary.
2026-08-19 21:58:44 -04:00

99 lines
4.2 KiB
C#
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.

namespace IslaApocalypse.Tools
{
/// <summary>
/// The presentation dials — everything about how a heightmap is SHOWN, and nothing about how it
/// is MADE. → `Design - Rendering - Roughness Is Presentation.md`.
///
/// ⚠ Config-gated for the same reason every shaping change is: a look decision should be an A/B
/// pair, not a memory of last week's render, and a rejected look should cost a flipped default
/// rather than a reverted commit. → `Design - Tooling - Iteration and Batching.md`.
///
/// ⚠ Kept DELIBERATELY SMALL. This is a taste gate, and iteration fatigue on a subjective gate
/// is a real failure mode — a handful of variants the eye can actually judge beats a wall of
/// near-duplicates. Three named looks, chosen so each pair isolates one question.
/// </summary>
public sealed class LookConfig
{
/// <summary>Short label, used in output filenames.</summary>
public string Name = "atlas";
/// <summary>Which hypsometric palette. → <see cref="ReliefPalette.Kind"/>.</summary>
public ReliefPalette.Kind Palette = ReliefPalette.Kind.Atlas;
/// <summary>
/// Vertical exaggeration for the relief normal. ⚠ A LOOK DIAL, NOT A PHYSICAL CLAIM — the
/// raw height units are not metres. See <see cref="Hillshade"/> for the measured slope
/// table this was chosen from.
/// </summary>
public float ZExaggeration = 75f;
/// <summary>Compass degrees clockwise from north. 315° (NW) is the cartographic standard.</summary>
public float LightAzimuth = 315f;
/// <summary>Degrees above the horizon. 45° standard; lower = longer, more dramatic shadows.</summary>
public float LightAltitude = 45f;
/// <summary>
/// How much the relief moves the tint. 0 = flat hypsometric tint, no shading at all;
/// 1 = full relief. Above ~0.85 the tint starts washing out and the map reads as a
/// greyscale DEM wearing colour.
/// </summary>
public float HillshadeStrength = 0.55f;
/// <summary>
/// Relief multiplier BELOW sea level.
///
/// ⚠ Deliberately much weaker than on land, and the reason is the deferral: the seabed is
/// RAW — no coast shelf, no islets, both Phase-2 items — so it is steep and noisy in a way
/// the finished terrain will not be. Shading it at full strength drags the eye to the one
/// part of the map that is knowingly unfinished. Cartographic bathymetry is conventionally
/// shown flat or lightly shaded anyway, so the convention and the deferral agree.
/// </summary>
public float SeaHillshadeFactor = 0.35f;
/// <summary>
/// How much of the lit side is allowed to lift toward white. Highlights SCREEN rather than
/// multiply, so sunlit slopes brighten without blowing out to paper.
/// </summary>
public float HighlightGain = 0.5f;
/// <summary>The colour boundary. NOT a water surface — no water is modelled (Phase 2).</summary>
public float SeaLevel = 0.15f;
/// <summary>
/// The three looks offered to the taste gate. Each pair isolates one question:
/// atlas vs relief → how strong should the relief be? (same palette and light)
/// atlas vs dusk → which palette, and how low a sun? (comparable relief)
/// </summary>
public static LookConfig[] Variants() => new[]
{
// Classic physical-atlas plate: tint-forward, relief present but polite.
new LookConfig
{
Name = "atlas", Palette = ReliefPalette.Kind.Atlas,
ZExaggeration = 75f, LightAzimuth = 315f, LightAltitude = 45f,
HillshadeStrength = 0.55f,
},
// Shape-forward: the same palette and light, harder relief. Isolates the relief dial.
new LookConfig
{
Name = "relief", Palette = ReliefPalette.Kind.Atlas,
ZExaggeration = 120f, LightAzimuth = 315f, LightAltitude = 45f,
HillshadeStrength = 0.80f,
},
// Warmer palette, lower sun, longer shadows. Isolates palette + light.
new LookConfig
{
Name = "dusk", Palette = ReliefPalette.Kind.Dusk,
ZExaggeration = 100f, LightAzimuth = 300f, LightAltitude = 35f,
HillshadeStrength = 0.70f,
},
};
public override string ToString() =>
$"{Name}: palette={Palette} zex={ZExaggeration:F0} light={LightAzimuth:F0}°/{LightAltitude:F0}° " +
$"strength={HillshadeStrength:F2} sea×{SeaHillshadeFactor:F2}";
}
}