using System;
namespace IslaApocalypse.Tools
{
///
/// Classic cartographic SHADED RELIEF from a height field — the slope+aspect view.
///
/// ⚠ PRESENTATION ONLY. → `Design - Rendering - Roughness Is Presentation.md`. Nothing here
/// touches how height is GENERATED; it changes only how height is SHOWN. A hillshade parameter
/// is a look dial, never a claim about the world.
///
/// ═══ THE METHOD ═══
///
/// HORN'S 3x3 method — the standard for DEM shading, and more robust than a plain central
/// difference because it weights the four edge-adjacent neighbours double, so single-pixel noise
/// does not dominate the normal. Cell spacing is 1: one column per pixel, 1 m per column
/// (D-002, 1:1 scale).
///
/// a b c dz/dx = ((c + 2f + i) - (a + 2d + g)) / 8
/// d e f dz/dy = ((g + 2h + i) - (a + 2b + c)) / 8
/// g h i
///
/// ⚠ y INCREASES SOUTHWARD, matching the height field and the rendered image. So dz/dy is a
/// north→south gradient, and the light's north component points toward -y.
///
/// ⚠ EDGES CLAMP, THEY DO NOT WRAP. Sampling is replicated at the border. Wrapping would fold
/// the Trench's outer wall against the opposite edge and smear a hard, deliberate boundary into
/// a false slope — the Trench border is a wall, not a seam.
///
/// ═══ ⚠⚠ WHY VERTICAL EXAGGERATION IS NOT OPTIONAL HERE ═══
///
/// The height field is in RAW NOISE UNITS (sea 0.15, peaks ~1.45) over a 2048–10240 px map, so
/// the true per-pixel gradient is tiny. Measured on seed 1063685222 at 2048, over 135k land
/// samples: MEDIAN |grad| = 0.0038, i.e. a slope of **0.22°**. Un-exaggerated, the entire island
/// shades as a flat plane. That is not a defect in the terrain; it is what a 1:1 metre grid
/// looks like.
///
/// So is a required look dial. Measured slopes at
/// candidate values (median / p90 / p99 of land):
///
/// zex 25 → 5.5° / 9.9° / 14.2° too flat to read
/// zex 75 → 16.0° / 27.6° / 37.1° natural, atlas-like ← default
/// zex 120 → 25.3° / 42.0° / 52.0° dramatic, shape-forward
/// zex 300 → 49.0° / 64.5° / 71.7° cartoon; the tint is lost
///
/// ⚠ IT IS A LOOK DIAL, NOT A PHYSICAL CLAIM. The raw units are not metres. The metres-per-unit
/// conversion belongs to Phase 2's elevation profile, not to this file, and no code should read
/// ZExaggeration as if it meant something about the world.
///
public static class Hillshade
{
///
/// Compute the hillshade factor at (x, y): the cosine of the angle between the surface
/// normal and the light, clamped to [0, 1]. 0 = fully shadowed, 1 = face-on to the light.
///
/// On PERFECTLY FLAT ground this returns sin(altitude) — not 1. That value is the
/// neutral point the blend divides by, so flat ground keeps its true hypsometric tint.
/// → .
///
public static float At(float[,] h, int n, int x, int y, float zExaggeration,
float lightX, float lightY, float lightZ)
{
// Clamped 3x3 window — replicate at the border, never wrap.
int xm = x > 0 ? x - 1 : 0, xp = x < n - 1 ? x + 1 : n - 1;
int ym = y > 0 ? y - 1 : 0, yp = y < n - 1 ? y + 1 : n - 1;
float hA = h[xm, ym], hB = h[x, ym], hC = h[xp, ym];
float hD = h[xm, y], hF = h[xp, y];
float hG = h[xm, yp], hH = h[x, yp], hI = h[xp, yp];
float dzdx = ((hC + 2f * hF + hI) - (hA + 2f * hD + hG)) / 8f;
float dzdy = ((hG + 2f * hH + hI) - (hA + 2f * hB + hC)) / 8f;
// Surface normal of z = height * zExaggeration, before normalization.
float nx = -dzdx * zExaggeration;
float ny = -dzdy * zExaggeration;
const float nz = 1f;
float inv = 1f / MathF.Sqrt(nx * nx + ny * ny + nz * nz);
float dot = (nx * lightX + ny * lightY + nz * lightZ) * inv;
return dot < 0f ? 0f : (dot > 1f ? 1f : dot);
}
///
/// The unit vector pointing FROM the surface TOWARD the light.
///
/// Azimuth is compass degrees, measured CLOCKWISE FROM NORTH — 315° is the cartographic
/// standard, light from the north-west. Altitude is degrees above the horizon; 45° standard.
///
/// North is -y in image space (y increases southward), which is why the y component is
/// negated. Get this wrong and the relief inverts: ridges read as valleys, which the eye
/// notices instantly even when it cannot say why.
///
public static (float x, float y, float z) LightVector(float azimuthDegrees, float altitudeDegrees)
{
float az = azimuthDegrees * MathF.PI / 180f;
float alt = altitudeDegrees * MathF.PI / 180f;
float cosAlt = MathF.Cos(alt);
return (cosAlt * MathF.Sin(az), -cosAlt * MathF.Cos(az), MathF.Sin(alt));
}
///
/// The hillshade value flat ground returns — sin(altitude). The blend normalizes by
/// this so that flat terrain is neither darkened nor lightened, and only actual SLOPE moves
/// the colour away from its hypsometric tint.
///
public static float Neutral(float altitudeDegrees) => MathF.Sin(altitudeDegrees * MathF.PI / 180f);
}
}