Task 1 of 6
A heightmap is a grid of numbers, and nobody can judge a landscape from numbers. Before we start moving rock around we need eyes: a picture in which a valley looks like a valley. The standard one is a hillshade — pretend the terrain is a solid surface, work out which way each cell faces, and see how squarely it faces the sun.
Which way a cell faces is its slope, and slope on a grid is a
central difference: the neighbour to the right minus the neighbour to the
left, halved because that step spans two cells. (The same two-tap derivative the Sobel
filters use — this is just the one place it gets to be lighting instead of edges.) With
gx and gy in hand the surface normal is
(−gx, −gy, 1); normalise it, dot it with the light
ℓ = (−1, −1, 1)/√3, and the whole thing collapses to
lit = 0.5774 * (1 + gx + gy)
/ Math.sqrt(gx * gx + gy * gy + 1)
The grid wraps at the edges, the way Reaction–Diffusion's did: column 0's left neighbour is the last column. No special cases, no missing pixels.
terrain — central differences for gx and gy, then
the lit value above, clamped at zero.gx and gy are central differences × 0.5 × this.constants.reliefMath.sqrt(gx * gx + gy * gy + 1) — an un-normalised normal is not a normallit at 0 so ground facing away from the light goes black, not negativeThe wrapped indexes are already computed for you:
const gx = (height[y][xr] - height[y][xl])
* 0.5 * this.constants.relief;
const gy = (height[yu][x] - height[yd][x])
* 0.5 * this.constants.relief;
relief is only a vertical exaggeration — terrain this shallow would be almost
invisible at a true aspect ratio.
const inv = 1 / Math.sqrt(gx * gx + gy * gy + 1);
let lit = this.constants.light * (1 + gx + gy) * inv;
if (lit < 0) lit = 0;
The palette line underneath is already written; it only wants lit.
This page is an interactive exercise — the editor, the GPU runner and your saved progress need JavaScript. The text above is the full brief.