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.
143 lines
6.5 KiB
C#
143 lines
6.5 KiB
C#
using Godot;
|
||
|
||
namespace IslaApocalypse.Tools
|
||
{
|
||
/// <summary>
|
||
/// The hypsometric palettes — a continuous elevation tint, in the physical-atlas tradition.
|
||
///
|
||
/// ═══ ⚠⚠ THIS IS CARTOGRAPHY, NOT CLASSIFICATION ═══
|
||
///
|
||
/// There are NO BIOME WORDS here and there must never be any. Nothing in this file knows about
|
||
/// jungle, wasteland, desert or snow. It maps ONE FLOAT — height — onto a continuous colour
|
||
/// ramp. White at the top is snow-COLOURED cartography; green at the bottom is
|
||
/// lowland-COLOURED. Biomes are a stage-5 CLASSIFICATION of finished shape (D-049 §2, D-056),
|
||
/// and the entire point of the rewrite's seam is that terrain never learns their names.
|
||
///
|
||
/// ⚠ CONTINUOUS, NOT BANDED. The stops below are interpolated, never quantized. Hard bands
|
||
/// would read as discrete classes — exactly the visual language of a biome map, and exactly the
|
||
/// wrong thing to suggest.
|
||
///
|
||
/// ═══ ⚠ THE STOPS ARE IN ABSOLUTE HEIGHT, AND THEY ARE FIXED ═══
|
||
///
|
||
/// Every stop is a raw height value, not a normalized fraction, and the same stops are used for
|
||
/// every image in a batch. A map coloured on its own min/max cannot be compared with its
|
||
/// neighbour — and comparison is the entire point of a batch. Out-of-range values clamp; the
|
||
/// run prints the true min/max so saturation shows up as a number rather than a guess.
|
||
///
|
||
/// ═══ WHY THE STOPS SIT WHERE THEY DO — measured, not eyeballed ═══
|
||
///
|
||
/// Land height is very bottom-heavy. Measured over all four pinned seeds at 2048 (8.6M land
|
||
/// columns): p10 0.214 · p25 0.306 · MEDIAN 0.456 · p75 0.648 · p90 0.835 · p99 1.113 ·
|
||
/// p99.9 1.267 · max 1.415.
|
||
///
|
||
/// A ramp spread linearly from 0.15 to 1.45 would therefore spend 99% of its colour range on
|
||
/// 1% of the land, and the map would read as "green with a few white dots". So the land stops
|
||
/// are placed on PERCENTILES of the measured distribution instead — the same thing an atlas
|
||
/// plate does when it picks non-uniform contour intervals.
|
||
///
|
||
/// Sea is the mirror problem: depth runs to −6.7 but 43% of it is shallower than 0.5 and the
|
||
/// deep floor is featureless. So the bathymetric ramp is COMPRESSED — it is indexed by
|
||
/// <c>d / (d + k)</c>, which gives the shallow shelf most of the range and lets the abyss
|
||
/// flatten to one dark tone. The interesting bathymetry is near shore.
|
||
///
|
||
/// ⚠ THE SEABED IS RAW, AND IT IS SUPPOSED TO BE. The submarine coast shelf and the offshore
|
||
/// islets are DEFERRED Phase-2 items. Plain, steep bathymetry here is the deferral showing, not
|
||
/// a broken render.
|
||
/// </summary>
|
||
public static class ReliefPalette
|
||
{
|
||
/// <summary>Named palettes. Gated so the look can be A/B'd like any other change.</summary>
|
||
public enum Kind { Atlas, Dusk }
|
||
|
||
/// <summary>
|
||
/// Bathymetric compression constant, in raw height units. The sea ramp is indexed by
|
||
/// <c>d / (d + SeaCompression)</c>. Smaller = more range spent on the shallows.
|
||
/// At 0.35: depth 0.05 → 12.5% of the ramp, 0.5 → 59%, 2.0 → 85%, 6.7 → 95%.
|
||
/// </summary>
|
||
public const float SeaCompression = 0.35f;
|
||
|
||
// ---- LAND: (absolute height, colour), interpolated. Placed on measured percentiles. ----
|
||
private static readonly (float h, Color c)[] AtlasLand =
|
||
{
|
||
(0.150f, new Color(0.427f, 0.561f, 0.376f)), // shoreline — soft green
|
||
(0.220f, new Color(0.475f, 0.596f, 0.388f)), // p10 lowland green
|
||
(0.310f, new Color(0.549f, 0.635f, 0.404f)), // p25
|
||
(0.460f, new Color(0.667f, 0.686f, 0.435f)), // p50 yellow-green
|
||
(0.650f, new Color(0.780f, 0.729f, 0.494f)), // p75 khaki
|
||
(0.840f, new Color(0.808f, 0.678f, 0.478f)), // p90 tan
|
||
(0.980f, new Color(0.757f, 0.588f, 0.427f)), // p95 light brown
|
||
(1.120f, new Color(0.667f, 0.510f, 0.404f)), // p99 brown
|
||
(1.270f, new Color(0.706f, 0.667f, 0.639f)), // p99.9 grey rock
|
||
(1.450f, new Color(0.988f, 0.988f, 0.980f)), // peak white
|
||
};
|
||
|
||
private static readonly (float h, Color c)[] AtlasSea =
|
||
{
|
||
(0.000f, new Color(0.478f, 0.729f, 0.769f)), // waterline — pale teal
|
||
(0.125f, new Color(0.325f, 0.596f, 0.706f)), // shelf
|
||
(0.310f, new Color(0.196f, 0.451f, 0.627f)), // mid blue
|
||
(0.590f, new Color(0.114f, 0.310f, 0.510f)), // deep
|
||
(0.760f, new Color(0.063f, 0.196f, 0.376f)), // deeper
|
||
(0.880f, new Color(0.035f, 0.118f, 0.259f)), // navy
|
||
(1.000f, new Color(0.020f, 0.063f, 0.157f)), // abyss floor — flat
|
||
};
|
||
|
||
// ---- DUSK: warmer land, deeper cooler water. A genuine alternative, not a tweak. ----
|
||
private static readonly (float h, Color c)[] DuskLand =
|
||
{
|
||
(0.150f, new Color(0.353f, 0.494f, 0.353f)),
|
||
(0.220f, new Color(0.427f, 0.541f, 0.361f)),
|
||
(0.310f, new Color(0.529f, 0.588f, 0.376f)),
|
||
(0.460f, new Color(0.663f, 0.639f, 0.408f)),
|
||
(0.650f, new Color(0.796f, 0.690f, 0.451f)),
|
||
(0.840f, new Color(0.831f, 0.627f, 0.412f)),
|
||
(0.980f, new Color(0.780f, 0.522f, 0.361f)),
|
||
(1.120f, new Color(0.663f, 0.435f, 0.353f)),
|
||
(1.270f, new Color(0.678f, 0.612f, 0.588f)),
|
||
(1.450f, new Color(0.980f, 0.973f, 0.949f)),
|
||
};
|
||
|
||
private static readonly (float h, Color c)[] DuskSea =
|
||
{
|
||
(0.000f, new Color(0.412f, 0.686f, 0.706f)),
|
||
(0.125f, new Color(0.263f, 0.545f, 0.647f)),
|
||
(0.310f, new Color(0.145f, 0.396f, 0.565f)),
|
||
(0.590f, new Color(0.078f, 0.263f, 0.443f)),
|
||
(0.760f, new Color(0.043f, 0.161f, 0.318f)),
|
||
(0.880f, new Color(0.024f, 0.094f, 0.212f)),
|
||
(1.000f, new Color(0.012f, 0.047f, 0.125f)),
|
||
};
|
||
|
||
/// <summary>The hypsometric tint for a height, before any relief shading.</summary>
|
||
public static Color Tint(Kind kind, float height, float seaLevel)
|
||
{
|
||
bool dusk = kind == Kind.Dusk;
|
||
|
||
if (height >= seaLevel)
|
||
return Sample(dusk ? DuskLand : AtlasLand, height);
|
||
|
||
// Compressed bathymetry: index by d/(d+k), so the shallows get the range.
|
||
float depth = seaLevel - height;
|
||
float t = depth / (depth + SeaCompression);
|
||
return Sample(dusk ? DuskSea : AtlasSea, t);
|
||
}
|
||
|
||
private static Color Sample((float h, Color c)[] stops, float v)
|
||
{
|
||
if (v <= stops[0].h) return stops[0].c;
|
||
for (int i = 1; i < stops.Length; i++)
|
||
{
|
||
if (v <= stops[i].h)
|
||
{
|
||
float span = stops[i].h - stops[i - 1].h;
|
||
float local = span <= 0f ? 0f : (v - stops[i - 1].h) / span;
|
||
return stops[i - 1].c.Lerp(stops[i].c, local);
|
||
}
|
||
}
|
||
return stops[stops.Length - 1].c;
|
||
}
|
||
|
||
/// <summary>The land range the stops cover, for a run header / INDEX.</summary>
|
||
public static (float lo, float hi) LandAnchors => (AtlasLand[0].h, AtlasLand[AtlasLand.Length - 1].h);
|
||
}
|
||
}
|