conrads.website / vibechecks / sea cliff math

Sea Cliff: Layer by Layer

Every block in this coast is decided by a formula. Nothing is simulated, and no block ever looks at its neighbour.

Scroll down and we will rebuild it from nothing, adding one piece of arithmetic at a time. Drag the view whenever you want a closer look.

Last updated 2026-10-02 02:46 UTC · made on claude.ai

Model, effort and cost weren't recorded for this one: it was made on claude.ai, which doesn't keep them with the page.

Distance to the shore

Start with one number for every column of blocks: how far it is from the shoreline. Call it c. It is positive inland, negative out to sea, and zero on the waterline.

Every later layer is written in terms of c, so the whole coast is really a picture of this one field.

c(x, z) = z − z0
function coast(x, z) {
  return z - P.coastZ;
}

Look for the rule that makes rock: a column is land wherever c is above zero. So far that gives a straight wall.

Layer 1 of 13

Bend the shoreline

Add smooth noise to c and the zero line wanders. A slow, strong noise B makes headlands and bays. A faster, weaker one S adds coves and points.

Keep hold of B. Its sign says whether a column sits on a headland or in a bay, and we will need that soon.

c = (z − z0) + 47·B(x/85, z/85) + 6·S(x/22, z/22)
const big   = fbm(x / 85, z / 85, seed + 11, 3) * 1.8;
const small = fbm(x / 22, z / 22, seed + 23, 2);
return (z - P.coastZ) + big * 26 + small * 26 * 0.22;

Look for stripes that are no longer evenly spaced. After bending, c is only roughly a distance. That turns out to be good enough.

Layer 2 of 13

Turn distance into height

A cliff is a rule for how high the ground is c blocks inland. Use a curve that is steep at the foot and rounds over at the top, reaching full height H after w blocks. With w = 3 the wall is close to sheer.

The seabed is the same idea pointed downward. A block at height y is rock if y is at or below the curve.

h(c) = sea + H·(1 − (1 − c/w)2)   for 0 < c < w
bed(c) = sea − 1.5 + c/2   for c < 0
function landTop(col, c) {
  if (c <= 0) return -1;
  const t = Math.min(1, c / P.slopeW);
  const r = 1 - (1 - t) * (1 - t);
  return SEA - 1 + (col.top - (SEA - 1)) * r;
}

Look for the cut edge of the world, where the profile shows in section. This is still a plain heightmap: one height per column.

Layer 3 of 13

Which rock takes the hit

Erosion should bite hardest where waves strike squarely. The gradient of c points inland, straight through the cliff face. Compare it with the direction the swell travels, d. Their dot product is 1 for a head-on hit and 0 for a glancing one.

Then scale by the headland noise B from before. Headlands focus wave energy and bays spread it out.

facing = max(0, ∇c · d / |∇c|)
e = (0.15 + 0.85·facing) · (0.78 + 0.3·B)
const gx = coast(x + 3, z) - coast(x - 3, z);
const gz = coast(x, z + 3) - coast(x, z - 3);
const facing = Math.max(0,
  (gx * swx + gz * swz) / Math.hypot(gx, gz));
let e = clamp((0.15 + 0.85 * facing)
            * (0.78 + 0.3 * headland), 0, 1);

Look for red points and blue bays. Nothing has moved yet. Exposure e is the one number every later layer reads.

Layer 4 of 13

Bedding, and the trick the rest depends on

Limestone is laid down in beds. Slice the rock into tilted layers and give each a hardness from a hash of its layer number.

Now the key move. Do not carve blocks. Instead, at each height, pull c back by a recession R before applying the cliff curve. Soft beds get a larger R and sit further back. Hard beds stand out as ledges.

layer = ⌊(y + dip·x) / t⌋    hard = hash(layer)
R(y) = ledge · (1 − hard)1.5
rock if  y ≤ h(c − R(y))
const layer = Math.floor((y + col.dipOff) / P.thick);
const hard  = hash1(layer, seed + 300);
R += P.ledge * Math.pow(1 - hard, 1.5);
...
const cp = col.c - R;
if (cp > 0 && y <= landTop(col, cp)) block = ROCK;

Look for ledges stepping in and out. Because R changes with height, a column can now be rock, then air, then rock. It has stopped being a heightmap.

Layer 5 of 13

The wave notch

Waves work on a narrow band around the waterline. Add a second recession term shaped like a bell, centred on sea level, scaled by exposure and resisted by hard beds.

Rock above the notch loses its support and falls, so everything from the waterline up also retreats, by 40% of the notch depth. What used to stand there survives below the water as a flat bench, the wave-cut platform.

band = exp(−((y − sea) / 2.6)2)
R(y) += N·e·(band·(0.55 + 0.45·(1 − hard)) + 0.4)
platform width = 1.5 + 1.4·N·e
const d = (y - SEA) / 2.6;
const band = Math.exp(-d * d);
R += P.notch * col.e * (
  band * (0.55 + 0.45 * (1 - hard))
  + (y >= SEA - 1 ? 0.4 : 0));

Look for the undercut at the waterline with rock hanging over it, and a pale shelf just under the surface.

Layer 6 of 13

Joints, caves and clefts

Limestone cracks along near-vertical planes, and the sea widens them. Lay down two sets of parallel planes at different angles. Jitter each plane within its slot and give it a random strength.

For every block, measure the distance to the nearest plane. Remove the block if it lies inside a half-ellipse around that plane at the waterline. The opening tapers inland and ends at a length L set by strength and exposure. The strongest planes are opened to the sky.

u = x·cos a + z·sin a    dist = |u − pi|
taper = 1 − c/L
carve if  dist < w·√taper · √(1 − ŷ2)
const u   = x * Math.cos(a) + z * Math.sin(a);
const ci  = Math.floor(u / spacing);
const pos = (ci + 0.25 + 0.5 * hash1(ci, s1)) * spacing;
const dist = Math.abs(u - pos);
...
const yy = (y - (SEA - 2)) / height;
if (dist < halfWidth * Math.sqrt(1 - yy * yy)) carve();

Look for a cave mouth. Where a cave is longer than its headland is wide it comes out the far side. Arches are not a feature here, they are a consequence.

Layer 7 of 13

Sea stacks

A stack is a piece of cliff the retreat left behind. Divide the map into cells 26 blocks across. In each cell a hash decides whether there is a candidate, where it stands, and how wide and tall it is. Keep it only if the coast field at that spot says it is just offshore.

Then merge it into c with a max. The stack becomes part of the same field, so it picks up bedding, the notch and joints with no extra code.

c ← max(c,  r − ‖(x, z) − pcell‖)
if (hash2(ix, iz, seed + 900) > P.stacks) continue;
const px = (ix + 0.2 + 0.6 * hash2(ix, iz, s1)) * 26;
const pz = (iz + 0.2 + 0.6 * hash2(ix, iz, s2)) * 26;
const cm = coast(px, pz);
if (cm > -5 || cm < -32) continue;   // not offshore
c = Math.max(c, r - Math.hypot(x - px, z - pz));

Look for the narrow waist at the waterline. The notch applies to stacks too, capped at half the radius so it can never cut one loose.

Layer 8 of 13

What the sea puts back

Erosion has a counterpart. Where exposure is low, sand settles. Fill each sheltered column with sand up to a ramp that rises with c, scaled by how sheltered it is.

Where exposure is high, fallen blocks collect at the cliff foot instead. A fine-grained noise picks where the boulders sit.

shelter = 1 − e
a = 0.8 · smoothstep(0.42, 0.75, shelter)
sand top = sea − 3 + 0.4·(c + 12)·(0.4 + 0.6·a)
const amt = P.beach * smooth(0.42, 0.75, 1 - e);
if (amt > 0.12 && c > -12 && c < 12)
  sandTop = Math.floor(SEA - 3
    + (c + 12) * 0.4 * (0.4 + 0.6 * amt));

Look for pocket beaches in the bays and bare rock on the points. It is the same exposure field, read the other way round.

Layer 9 of 13

One Gerstner wave

Now the water. A plain sine wave moves each point straight up and down, which gives round crests. A Gerstner wave also slides each point sideways, toward the crest as it rises. Crests sharpen and troughs flatten, the way real swell looks.

Speed is not a free choice. In deep water it follows from the wavelength λ.

k = 2π/λ    ω = √(g·k)    φ = k·(D · p) − ωt
pxz += Q·A·D·cos φ
py  += A·sin φ
float ph = k * dot(D, p.xz) - sqrt(9.81 * k) * uTime;
d.xz += D * QA * cos(ph);
d.y  += A * sin(ph);

Look for one wave, 38 blocks long, with about five seconds between crests. That timing comes from the formula, not from a setting.

Layer 10 of 13

Five waves make a sea

A single wave looks like a machine. Add five, each shorter and smaller than the last, fanned out around the swell direction.

The sideways slide is shared out among them through a steepness s, so that crests can never fold over on themselves.

p += Σi (QiAi·Di·cos φi,  Ai·sin φi)
QiAi = s / (5·ki)
λi = λ·(1, 0.61, 0.39, 0.26, 0.17)
for (int i = 0; i < 5; i++) {
  vec2 D = uWave[i].xy; float k = uWave[i].z;
  float A = uWave[i].w;
  float ph = k * dot(D, p.xz) - sqrt(9.81 * k) * uTime;
  float QA = uSteep * A / max(k * A * 5.0, 0.4);
  d.xz += D * QA * cos(ph);  d.y += A * sin(ph);
}

Look for the direction the swell runs. It is the same d that set exposure earlier, so the sea and the erosion agree.

Layer 11 of 13

Where water meets rock

Three additions tie the sea to the coast. Wave height is damped over shallow rock. Foam is drawn where a crest is sharp, and in bands that run toward the shore, using a map of distance to the nearest rock.

Spray is thrown wherever a rising crest reaches a rock face, in proportion to that face's exposure.

A′ = A · mix(0.45, 1, smoothstep(0, 6, depth))
foam ∝ (1 − smoothstep(0.3, 5, shore)) · sin(1.5·shore + 1.7·t)
float att = mix(0.45, 1.0, smoothstep(0.0, 6.0, depth));
...
float band = 0.5 + 0.5 * sin(shore * 1.5 + uTime * 1.7);
float shoreFoam = (1.0 - smoothstep(0.3, 5.0, shore))
                * (0.4 + 0.6 * band) * surge;

Look for spray on the faces that were red in the exposure layer, and almost none in the bays.

Layer 12 of 13

Why this works chunk by chunk

Look back over the layers. Each one answered the question "is this block rock?" using only its own x, y, z and the seed. No layer read a neighbouring block, and nothing was repeated until it settled.

So any chunk can be built alone, in any order, on any thread, and the borders line up. The worlds in this page were built that way, a chunk at a time, while you were reading.

The one shared input is the coast field. Here it is a formula. In a game it would come from the ocean map, worked out per region before any chunk is built.

The price is that nothing is simulated. No sediment is conserved and nothing emerges from flow. It is a careful drawing of erosion, not erosion.

Look for the seams. There are none.

Layer 13 of 13

Finished coast Open full page →