The exact math behind every terrain-derivative visualization mode
Every formula on this page is transcribed directly from its lib/*-protocol.ts source, not re-derived — see that file for the full derivation/attribution comments. For how these values get from a formula to a rendered layer, see Terrain Analysis Rendering Pipeline, LRM, and Lighting Effects. Sun/light-position astronomy (declination, hour angle, azimuth/elevation) is covered separately on Sun Position, since it drives Phong's light direction and the shadow calculator rather than being a terrain derivative itself.
The Horn 3×3 gradient (hornGradient(), ported from GDAL's GDALSlopeHornAlg), the shared building block for Slope, Aspect, TRI*, Curvature, and (per-sub-cell) Blobness. The window is named a0..a8 row-major — GDAL's own afWin[0..8] convention, kept for line-by-line parity with the ported code — laid out on the tile grid as (x = column = east, y = row = south):
The Horn 3×3 gradient window — ported from GDAL's GDALSlopeHornAlg
west (col −1)
center col
east (col +1)
north (row −1)
a0
a1
a2
center row
a3
a4 (center)
a5
south (row +1)
a6
a7
a8
With ground resolution L at the tile's center latitude:
∂x∂z=8Lcos(lat)a0+2a3+a6−a2−2a5−a8∂y∂z=8Lcos(lat)a6+2a7+a8−a0−2a1−a2*TRI/TPI/Roughness don't use the gradient directly — they're plain 3×3 aggregations of elevation itself, see below.
Four sub-modes share one Zevenbergen & Thorne (1987) second-order fit over the 3×3 window (p,q = first partials; r,t,s = second partials; L = ground spacing):
Combined — discrete Laplacian, ×100 (no single canonical GDAL algorithm to port from, unlike slope/aspect/TRI):
∇2z=L2a1+a3+a5+a7−4a4combined=100⋅∇2z
Profile (curvature along steepest descent — flow acceleration) / Plan (curvature across contours — flow convergence/divergence, ≡div(∇z/∣∇z∣)), both ×100, undefined (→0) on flat ground:
Det-Hessian — fxxfyy−fxy2, a blob/saddle detector (positive at bowl/dome extrema, negative at saddles, ~0 on a uniform slope or straight ridge), ×10,000:
detHessian=(rt−s2)⋅10000
Casorati / Shape Index — both derived from the principal curvatures (eigenvalues of the Hessian [rsst]), the small-slope approximation:
tr=r+tdisc=(r−t)2+4s2κ1=2tr+discκ2=2tr−discCasorati (Koch 1993)=2κ12+κ22⋅100— always≥0, magnitude onlyShapeIndex (Koenderink & van Doorn 1992)=π2atan2(κ1+κ2,κ1−κ2)— always in [−1,1]
(+1 dome/peak, +0.5 ridge, 0 saddle, −0.5 valley, −1 pit/bowl.)
All modes ×CURVATURE_ENCODE_SCALE = 1000 at the wire-encoding step only (undone on read), to spread the small near-zero-heavy range across more of Terrarium's discrete levels. lib/curvature-protocol.ts.
A Förstner/Harris-style structure tensor J=[IxxIxyIxyIyy], where each entry is the 3×3-box average of gx2, gy2, gxgy — the Horn gradient computed at each of the window's 9 sub-cells (needs a 5×5 halo, since each sub-cell's own gradient needs its own 3×3 neighborhood):
Blobness=tr(J)det(J)⋅64100 — the 64100 corrects for the Horn kernel's 8× gradient inflation (det is degree-4 in the gradient, so 82=64) and rescales to roughly match TRI/Roughness's visual range. High where gradient direction varies in every direction (peaks/pits/saddles/knolls); near zero on a uniform slope or straight ridge, however steep.
Eigen-Ratio=λmaxλmin⋅100 — shape only, independent of magnitude: 0 on a perfectly coherent edge (slope/ridge/valley), 100 on a perfectly isotropic blob (peak/pit/saddle). The 8× inflation cancels in this ratio, so no extra scale correction is needed.
Orientation — dominant eigenvector's axis, folded to 0–180° (a line has no inherent direction, only an axis):
lib/horizon-angle.ts — for each of 8 compass directions, march outward pixel-by-pixel (up to a user-set search radius) and find the steepest elevation angle to any point along the ray:
Each angle is clamped to ≥0 first (a ray dipping downhill still leaves the entire sky visible in that direction — SVF can't exceed "fully open"), then:
Mean angular distance from zenith to the horizon, not clamped to ≥0 (so it can read above/below what SVF calls "fully open" — e.g. standing on a summit looking outward gives a negative angle in every direction). Negative Openness is Positive Openness computed on the terrain's mirror image (elevation differences × −1), turning pits into the "peaks" the same formula highlights.
Places a virtual observer of eye height oh=1.6m at each pixel and averages the downward-looking angle to the surrounding terrain across several compass directions and distances (Hesse, 2016):
averaged over sampled directions × distances. Positive/high on local highs that look down on their surroundings (mounds, ridges, tells); low/negative in enclosed depressions. Distances beyond a near-field threshold are sampled from pyramid ancestor tiles (one ring per octave) rather than at native resolution — the same trick LRM uses, valid here because this quantity is a mean (a coarse per-octave sample is a legitimate low-pass proxy), unlike SVF/Openness's max. lib/local-dominance-protocol.ts.
(signs empirically pinned against MapLibre's native hillshade shader — see Lighting Effects). total is then split into a multiply-darken regime (total≤1) and a screen-brighten regime (total>1) for compositing — see that page for the exact alpha encoding. lib/phong-protocol.ts.