Build a Procedural Island From Scratch in One Hour
Combine everything a good island needs — a noise-based mountain silhouette, a coastline that actually tapers off, eroded valleys and beaches, height-driven biome colours, and gently animated water — into one self-contained Three.js generator you can drop into any project.
1. Base Heightmap with fBm Noise
Start with fractional Brownian motion (fBm): sum several octaves of Perlin/simplex noise, each one higher frequency and lower amplitude than the last. Low octaves shape broad mountains; high octaves add rocky detail on top of them.
function fbm(x, y, octaves = 6, lacunarity = 2.0, gain = 0.5) {
let amplitude = 1, frequency = 1, sum = 0, norm = 0;
for (let o = 0; o < octaves; o++) {
sum += amplitude * perlin2D(x * frequency, y * frequency);
norm += amplitude;
amplitude *= gain; // each octave weaker
frequency *= lacunarity; // each octave finer
}
return sum / norm; // normalised to [-1, 1]
}
function buildHeightmap(size) {
const map = new Float32Array(size * size);
for (let y = 0; y < size; y++)
for (let x = 0; x < size; x++)
map[y * size + x] = fbm(x / size * 4, y / size * 4);
return map;
}
1 − |noise|
instead of raw noise into each octave for sharp mountain ridgelines
instead of rolling hills — useful for a volcanic island's central
peak.
2. Radial Falloff — Turning Terrain into an Island
Raw fBm noise tiles endlessly in every direction — it never decides to become an island. Multiply it by a radial falloff mask that is 1 at the centre and smoothly drops to 0 at the map edge, so height fades to sea level no matter what the noise underneath says.
function falloff(nx, ny) {
// nx, ny in [-1, 1], distance from centre
const d = Math.sqrt(nx*nx + ny*ny);
const a = 3, b = 2.2; // shape tuning constants
return Math.pow(d, a) / (Math.pow(d, a) + Math.pow(b - b * d, a));
}
function applyIslandMask(map, size) {
for (let y = 0; y < size; y++)
for (let x = 0; x < size; x++) {
const nx = (x / size) * 2 - 1, ny = (y / size) * 2 - 1;
const i = y * size + x;
map[i] = (map[i] + 1) / 2 * (1 - falloff(nx, ny));
}
return map; // values now in [0, 1], centre high, edges 0
}
3. Hydraulic Erosion Pass
A raw masked heightmap still looks uniformly bumpy. Run a batch of simulated droplets over it (see the hydraulic erosion article for the full derivation) — each one erodes steep slopes and deposits sediment where it slows down, carving valleys and building up flat beach shelves near the shore.
function erode(map, size, dropletCount = 50000) {
for (let i = 0; i < dropletCount; i++) {
const pos = { x: Math.random() * size, y: Math.random() * size };
simulateDroplet(map, size, pos); // erode/deposit along its path
}
return map;
}
4. Mesh Generation and Normals
Feed the eroded heightmap into a Three.js
PlaneGeometry, displacing each vertex's Z by the
corresponding height sample, then recompute normals so lighting
responds to the new slopes instead of the flat plane's original
normal.
function buildMesh(heightmap, size, worldSize = 100, maxHeight = 18) {
const geo = new THREE.PlaneGeometry(worldSize, worldSize, size - 1, size - 1);
const pos = geo.attributes.position;
for (let i = 0; i < pos.count; i++) {
const h = heightmap[i] * maxHeight;
pos.setZ(i, h); // plane is XY, displace along local Z
}
geo.rotateX(-Math.PI / 2); // lay flat: Z becomes world Y (up)
geo.computeVertexNormals();
return new THREE.Mesh(geo, terrainMaterial);
}
5. Biome Colouring by Height and Slope
Colour every vertex in a fragment shader (or per-vertex colour buffer) based on height — beach near sea level, grass mid-slope, rock higher up, snow at the peak — with a slope override so cliffs read as bare rock regardless of altitude:
// GLSL fragment shader, simplified
varying float vHeight;
varying vec3 vNormal;
void main() {
float slope = 1.0 - dot(normalize(vNormal), vec3(0.0, 1.0, 0.0));
vec3 beach = vec3(0.85, 0.77, 0.55);
vec3 grass = vec3(0.25, 0.5, 0.2);
vec3 rock = vec3(0.4, 0.37, 0.33);
vec3 snow = vec3(0.95, 0.95, 0.97);
vec3 color = mix(beach, grass, smoothstep(0.05, 0.15, vHeight));
color = mix(color, rock, smoothstep(0.45, 0.6, vHeight));
color = mix(color, snow, smoothstep(0.8, 0.92, vHeight));
color = mix(color, rock, smoothstep(0.35, 0.6, slope)); // steep = rock
gl_FragColor = vec4(color, 1.0);
}
6. Shoreline Water and Waves
Add a flat, semi-transparent plane at sea level (height 0). A cheap vertex-shader ripple plus a Fresnel term for foam where the water meets the shallow beach sells the illusion without a full ocean simulation:
const waterMat = new THREE.ShaderMaterial({
transparent: true,
uniforms: { uTime: { value: 0 } },
vertexShader: `
uniform float uTime;
varying vec3 vPos;
void main() {
vec3 p = position;
p.z += sin(p.x * 0.3 + uTime) * 0.06 + cos(p.y * 0.25 + uTime * 1.3) * 0.05;
vPos = p;
gl_Position = projectionMatrix * modelViewMatrix * vec4(p, 1.0);
}`,
fragmentShader: `
varying vec3 vPos;
void main() {
vec3 shallow = vec3(0.25, 0.65, 0.7);
vec3 deep = vec3(0.03, 0.15, 0.3);
gl_FragColor = vec4(mix(shallow, deep, 0.6), 0.85);
}`
});
terrainHeight is within a small threshold of sea
level — an easy way to fake a foam band without extra geometry.
7. Putting It Together
Wrap every step into one seeded, reusable generator so the same seed always reproduces the same island — essential for sharing a seed or saving/loading a scene:
function generateIsland(seed, size = 256) {
seedNoise(seed); // deterministic Perlin permutation table
let map = buildHeightmap(size); // 1. fBm
map = applyIslandMask(map, size); // 2. radial falloff
map = erode(map, size); // 3. hydraulic erosion
const terrain = buildMesh(map, size); // 4. mesh + normals
terrain.material = biomeMaterial; // 5. biome shader
const water = buildWaterPlane(size, waterMat); // 6. shoreline
return { terrain, water };
}
const { terrain, water } = generateIsland(1337);
scene.add(terrain, water);
Frequently Asked Questions
What will I learn in this tutorial?
Build a complete procedural island in Three.js in one hour: Perlin-noise heightmap, radial falloff mask, hydraulic erosion pass, biome colour grading, and animated shoreline waves.
What topics are covered in this tutorial?
This tutorial covers: Base Heightmap with fBm Noise, Radial Falloff — Terrain Into an Island, Hydraulic Erosion Pass, Mesh Generation and Normals, Biome Colouring by Height and Slope, Shoreline Water and Waves, Putting It Together.
How long does this tutorial take?
This tutorial takes approximately 60 minutes to complete.
What prerequisites do I need before starting?
This is a Intermediate-level tutorial — no special preparation beyond basic JavaScript is assumed.