Procedural terrain generation in WebGL

Motivation

I wanted a terrain renderer that could run in the browser without any dependencies. No Three.js, no Babylon — just raw WebGL, GLSL, and pain.

The goal: generate a heightmap using layered Perlin noise (octaves of fBm), apply a simplified hydraulic erosion pass, and render the result with a custom lighting model that approximates the look of Swiss topo maps from the 1800s.

Heightmap generation

The terrain starts as a 1024×1024 float texture. Each texel is a height value generated by summing six octaves of Perlin noise:

float h = 0.0;
float amp = 1.0;
float freq = 0.005;
for (int i = 0; i < 6; i++) {
    h += amp * noise(pos * freq);
    amp *= 0.5;
    freq *= 2.0;
}

This gives you the classic rolling hills, but it looks synthetic. Real terrain has ridges, valleys, drainage patterns. That’s where erosion comes in.

Hydraulic erosion

I’m using a particle-based approach loosely based on Hans Theobald Beyer’s implementation. The idea:

  1. Drop a virtual water particle at a random location
  2. Compute the gradient of the heightmap at that point
  3. Move the particle downhill, picking up sediment proportional to velocity
  4. When the particle slows (flatter terrain), deposit sediment
  5. Repeat 200,000 times

The result is dramatically more realistic. You get dendritic drainage networks, alluvial fans, and the kind of asymmetric ridgelines you see in actual DEMs.

Rendering

The renderer uses a two-pass approach:

Pass 1 — Normal computation: Finite differences on the heightmap to get surface normals. I do this in a separate shader because the normals are reused for both lighting and slope-dependent coloring.

Pass 2 — Composite: Lambertian diffusion with a sun direction I can rotate, plus ambient occlusion approximated by sampling the heightmap in a small radius around each fragment. Steeper slopes get a rock texture (just noise), flatter areas get grass-colored.

The whole thing runs at 60fps on integrated graphics. The erosion step is the bottleneck — currently runs on the CPU. Moving it to a compute shader (via WebGPU, eventually) would let me erode in real time.

What’s next

  • WebGPU compute shader for real-time erosion
  • Biome generation based on altitude + moisture
  • Level-of-detail system for rendering larger terrains
  • Water simulation (at least a basic shallow-water equation)

Source code (WIP)