# Seeing the Ground

*Task 1 of 6 · [Hydraulic Erosion: Carving Terrain by Accumulation](https://gpu.rocks/learn/hydraulic-erosion-07165ca1.md) · GPU.js Learn*

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

```js
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.

## Figures

- **two neighbours make a slope, a slope makes a normal, a normal catches the light**

## Goal

**Goal:** finish the graphical kernel so it hillshades
`terrain` — central differences for `gx` and `gy`, then
the lit value above, clamped at zero.

## Requirements

- `gx` and `gy` are central differences × `0.5` × `this.constants.relief`
- Divide by `Math.sqrt(gx * gx + gy * gy + 1)` — an un-normalised normal is not a normal
- Clamp `lit` at `0` so ground facing away from the light goes black, not negative

## Hint 1 — the two slopes

The wrapped indexes are already computed for you:

```js
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.

## Hint 2 — the rest of it

```js
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`.

## Same idea elsewhere

Hillshading is a fragment shader that has escaped its renderer: one texture read
per neighbour, one dot product, one write. Every terrain engine — WebGPU, Metal, Unreal's
landscape — does exactly this, usually with the normals baked into a texture beside the
heights so the fetch is one sample instead of four.

## Starter code

```js
// A heightmap you cannot see is a heightmap you cannot debug.
const gpu = new GPU({ mode });

const hillshade = gpu.createKernel(function (height) {
  const x = this.thread.x;
  const y = this.thread.y;
  let xl = x - 1; if (xl < 0) xl = this.constants.size - 1;
  let xr = x + 1; if (xr > this.constants.size - 1) xr = 0;
  let yd = y - 1; if (yd < 0) yd = this.constants.size - 1;
  let yu = y + 1; if (yu > this.constants.size - 1) yu = 0;

  // TODO: central differences, scaled by this.constants.relief
  const gx = 0;
  const gy = 0;
  // TODO: normalise, dot with the light, clamp at zero
  let lit = 1;

  const rock = Math.min(1, height[y][x] * 1.15);
  this.color(lit * (0.3 + 0.62 * rock), lit * (0.44 + 0.4 * rock), lit * (0.54 + 0.2 * rock), 1);
}, {
  output: [64, 64],
  graphical: true,
  constants: { size: 64, relief: 12, light: 0.5774 },
});

await hillshade(terrain);
render(hillshade.canvas);
```

---

Interactive version: https://gpu.rocks/learn/hydraulic-erosion-07165ca1/1

[Next task](https://gpu.rocks/learn/hydraulic-erosion-07165ca1/2.md)
