| name | procedural-landscapes |
| description | Generate procedural 3D landscapes and terrain in Three.js using WebGPU compute shaders with automatic WebGL2 fallback. Covers heightmap generation via FBM noise, ridged multifractal, and domain warping; chunked LOD terrain meshes; slope-based multi-material texturing; procedural water planes; atmospheric sky rendering; and instanced vegetation scattering. Use when building terrain generators, open-world environments, landscape visualizers, flight simulators, or any 3D scene requiring procedural ground surfaces. Triggers: "procedural terrain", "landscape generator", "heightmap", "terrain mesh", "procedural world", "open world", "terrain LOD", "terrain chunks", "noise terrain", "3D landscape".
|
Procedural Landscapes
Generate performant, visually rich procedural terrain in Three.js with a WebGPU-first
architecture and automatic WebGL2 fallback.
Architecture Overview
┌─────────────────────────────────────────────────┐
│ Renderer Init │
│ WebGPU available? ──yes──► WebGPURenderer │
│ │no │
│ └──────────────► WebGLRenderer │
├─────────────────────────────────────────────────┤
│ Terrain Pipeline │
│ 1. Noise Generation (GPU compute or CPU) │
│ 2. Heightmap → Geometry (chunked grid) │
│ 3. Normal Computation (per-vertex) │
│ 4. Material Assignment (slope + height rules) │
│ 5. LOD Management (camera-distance based) │
├─────────────────────────────────────────────────┤
│ Environment Layers │
│ Water plane ─ Sky dome ─ Fog ─ Vegetation │
└─────────────────────────────────────────────────┘
Renderer Setup with Dual Backend
Always attempt WebGPU first, fall back to WebGL2 gracefully.
import * as THREE from 'three';
import WebGPU from 'three/addons/capabilities/WebGPU.js';
import WebGPURenderer from 'three/addons/renderers/webgpu/WebGPURenderer.js';
async function createRenderer(canvas) {
let renderer;
let gpuAvailable = false;
if (WebGPU.isAvailable()) {
renderer = new WebGPURenderer({ canvas, antialias: true });
await renderer.init();
gpuAvailable = true;
} else {
renderer = new THREE.WebGLRenderer({ canvas, antialias: true });
renderer.toneMapping = THREE.ACESFilmicToneMapping;
renderer.toneMappingExposure = 1.0;
}
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
return { renderer, gpuAvailable };
}
CDN usage (r170+):
<script type="importmap">
{
"imports": {
"three": "https://cdn.jsdelivr.net/npm/three@0.170.0/build/three.module.js",
"three/addons/": "https://cdn.jsdelivr.net/npm/three@0.170.0/examples/jsm/"
}
}
</script>
Noise Generation
CPU Path (WebGL fallback)
Implement FBM noise on CPU when GPU compute is unavailable. Self-contained simplex noise
avoids external dependencies.
function createNoise2D(seed = 0) {
const perm = new Uint8Array(512);
let s = seed;
for (let i = 0; i < 256; i++) {
s = (s * 16807 + 0) % 2147483647;
perm[i] = perm[i + 256] = s & 255;
}
const G2 = (3 - Math.sqrt(3)) / 6;
const grad = [[1,1],[-1,1],[1,-1],[-1,-1],[1,0],[-1,0],[0,1],[0,-1]];
return function(x, y) {
const s0 = (x + y) * 0.5 * (Math.sqrt(3) - 1);
const i = Math.floor(x + s0), j = Math.floor(y + s0);
t0 = (i + j) * ;
x0 = x - (i - t0), y0 = y - (j - t0);
i1 = x0 > y0 ? : , j1 = x0 > y0 ? : ;
x1 = x0 - i1 + , y1 = y0 - j1 + ;
x2 = x0 - + * , y2 = y0 - + * ;
ii = i & , jj = j & ;
n0 = , n1 = , n2 = ;
t = - x0*x0 - y0*y0;
(t > ) { g = grad[perm[ii + perm[jj]] & ]; n0 = t*t*t*t * (g[]*x0 + g[]*y0); }
t = - x1*x1 - y1*y1;
(t > ) { g = grad[perm[ii+i1 + perm[jj+j1]] & ]; n1 = t*t*t*t * (g[]*x1 + g[]*y1); }
t = - x2*x2 - y2*y2;
(t > ) { g = grad[perm[ii+ + perm[jj+]] & ]; n2 = t*t*t*t * (g[]*x2 + g[]*y2); }
* (n0 + n1 + n2);
};
}
FBM and Terrain Noise Functions
function fbm(noise, x, y, octaves = 6, lacunarity = 2.0, gain = 0.5) {
let sum = 0, amp = 1, freq = 1, maxAmp = 0;
for (let i = 0; i < octaves; i++) {
sum += noise(x * freq, y * freq) * amp;
maxAmp += amp;
amp *= gain;
freq *= lacunarity;
}
return sum / maxAmp;
}
function ridgedMultifractal(noise, x, y, octaves = 6, lacunarity = 2.0, gain = 0.5) {
let sum = 0, amp = 1, freq = 1, prev = 1;
for (let i = 0; i < octaves; i++) {
let n = 1 - Math.abs(noise(x * freq, y * freq));
n = n * n * prev;
sum += n * amp;
prev = n;
amp *= gain;
freq *= lacunarity;
}
return sum;
}
function domainWarp(noise, x, y, strength = 0.3) {
const qx = fbm(noise, x, y, 4);
const qy = fbm(noise, x + 5.2, y + 1.3, 4);
return fbm(noise, x + strength * qx, y + strength * qy, );
}
GPU Compute Path (WebGPU)
For GPU-accelerated heightmap generation via WGSL compute shaders, see
references/wgsl-shaders.md. The compute path generates a heightmap texture
on the GPU, then samples it in vertex shaders or reads it back for collision.
Terrain Geometry
Single-Chunk Terrain
function createTerrainGeometry(size, segments, heightFn, maxHeight = 50) {
const geometry = new THREE.PlaneGeometry(size, size, segments, segments);
geometry.rotateX(-Math.PI / 2);
const position = geometry.attributes.position;
const vertex = new THREE.Vector3();
for (let i = 0; i < position.count; i++) {
vertex.fromBufferAttribute(position, i);
const nx = vertex.x / size + 0.5;
const nz = vertex.z / size + 0.5;
position.setY(i, heightFn(nx, nz) * maxHeight);
}
geometry.computeVertexNormals();
position.needsUpdate = true;
return geometry;
}
Chunked Terrain with LOD
For larger worlds, divide terrain into chunks with distance-based LOD.
class TerrainChunkManager {
constructor(scene, chunkSize, viewDistance, heightFn, maxHeight) {
this.scene = scene;
this.chunkSize = chunkSize;
this.viewDistance = viewDistance;
this.heightFn = heightFn;
this.maxHeight = maxHeight;
this.chunks = new Map();
this.lodLevels = [
{ distance: chunkSize * 2, segments: 64 },
{ distance: chunkSize * 5, segments: 32 },
{ distance: chunkSize * 10, segments: 16 },
{ distance: Infinity, segments: 8 },
];
}
update(cameraPosition) {
const cx = Math.floor(cameraPosition.x / this.chunkSize);
const cz = Math.floor(cameraPosition.z / this.chunkSize);
radius = .(. / .);
activeKeys = ();
( x = cx - radius; x <= cx + radius; x++) {
( z = cz - radius; z <= cz + radius; z++) {
key = ;
activeKeys.(key);
worldX = x * .;
worldZ = z * .;
dist = .(cameraPosition. - worldX, cameraPosition. - worldZ);
(dist > .) ;
lod = ..( dist < l.);
existing = ..(key);
(!existing || existing. !== lod.) {
(existing) { ..(existing.); existing...(); }
mesh = .(x, z, lod.);
..(key, { mesh, : lod. });
..(mesh);
}
}
}
( [key, chunk] .) {
(!activeKeys.(key)) {
..(chunk.);
chunk...();
..(key);
}
}
}
() {
geo = (
., segments,
{
wx = (cx + nx) * . / ;
wz = (cz + nz) * . / ;
.(wx, wz);
},
.
);
mesh = .(geo, ());
mesh..(cx * ., , cz * .);
mesh. = ;
mesh;
}
() {
( [, chunk] .) {
..(chunk.);
chunk...();
}
..();
}
}
Terrain Materials
Slope + Height Shader (WebGL)
function createTerrainMaterial() {
return new THREE.ShaderMaterial({
uniforms: {
waterLevel: { value: 0.05 },
snowLevel: { value: 0.75 },
grassColor: { value: new THREE.Color(0x4a7c3f) },
rockColor: { value: new THREE.Color(0x8b8680) },
sandColor: { value: new THREE.Color(0xc2b280) },
snowColor: { value: new THREE.Color(0xf0f0f5) },
sunDir: { value: new THREE.Vector3(0.5, 0.8, 0.3).normalize() },
maxHeight: { value: 50.0 },
},
vertexShader: `
varying vec3 vWorldPos;
varying vec3 vNormal;
void main() {
vWorldPos = (modelMatrix * vec4(position, 1.0)).xyz;
vNormal = normalize(normalMatrix * normal);
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
`,
: ,
});
}
Node Material (WebGPU TSL)
When using WebGPURenderer, prefer Three.js Shading Language (TSL) node materials:
import { color, normalWorld, positionWorld, mix, smoothstep,
dot, vec3, float as tslFloat, MeshStandardNodeMaterial } from 'three/tsl';
function createTerrainNodeMaterial(maxHeight = 50) {
const material = new MeshStandardNodeMaterial();
const h = positionWorld.y.div(tslFloat(maxHeight)).clamp(0, 1);
const slope = tslFloat(1).sub(dot(normalWorld, vec3(0, 1, 0)));
const grass = color(0x4a7c3f);
const rock = color(0x8b8680);
const sand = color(0xc2b280);
const snow = color(0xf0f0f5);
let c = mix(sand, grass, smoothstep(0.0, 0.1, h));
c = mix(c, snow, smoothstep(0.75, 0.85, h));
c = mix(c, rock, (, , slope));
material. = c;
material. = ((), (), slope);
material;
}
Water
function createWater(size, waterLevel = 2.5) {
const geometry = new THREE.PlaneGeometry(size, size, 128, 128);
geometry.rotateX(-Math.PI / 2);
const material = new THREE.MeshPhysicalMaterial({
color: 0x006994, transparent: true, opacity: 0.7,
roughness: 0.1, metalness: 0.1, transmission: 0.3, thickness: 2.0,
});
const water = new THREE.Mesh(geometry, material);
water.position.y = waterLevel;
water.userData.animate = (time) => {
const pos = geometry.attributes.position;
for (let i = 0; i < pos.count; i++) {
const x = pos.getX(i), z = pos.getZ(i);
pos.(i, .(x * + time) * + .(z * + time * ) * );
}
pos. = ;
geometry.();
};
water;
}
Sky & Atmosphere
function createSky() {
const geo = new THREE.SphereGeometry(500, 32, 16);
const mat = new THREE.ShaderMaterial({
side: THREE.BackSide, depthWrite: false,
uniforms: {
topColor: { value: new THREE.Color(0x0077be) },
bottomColor: { value: new THREE.Color(0xffeebb) },
sunDir: { value: new THREE.Vector3(0.3, 0.5, 0.4).normalize() },
},
vertexShader: `
varying vec3 vDir;
void main() {
vDir = normalize(position);
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
`,
fragmentShader: `
uniform vec3 topColor, bottomColor, sunDir;
varying vec3 vDir;
void main() {
float y = vDir.y * 0.5 + 0.5;
vec3 sky = mix(bottomColor, topColor, pow(y, 0.6));
float sun = smoothstep(0.97, 1.0, dot(vDir, sunDir));
sky += vec3(1.0, 0.95, 0.8) * sun * 0.8;
gl_FragColor = vec4(sky, 1.0);
}
`,
});
.(geo, mat);
}
Vegetation Scattering
Place instanced vegetation using height and slope constraints.
function scatterVegetation(heightFn, terrainSize, maxHeight, count = 5000) {
const trunkGeo = new THREE.CylinderGeometry(0.1, 0.15, 1.5, 6);
const canopyGeo = new THREE.ConeGeometry(0.8, 2.0, 6);
canopyGeo.translate(0, 2.0, 0);
const merged = mergeBufferGeometries(trunkGeo, canopyGeo);
const material = new THREE.MeshStandardMaterial({ color: 0x2d5a27, flatShading: true });
const mesh = new THREE.InstancedMesh(merged, material, count);
mesh.castShadow = true;
const dummy = new THREE.Object3D();
const noise = createNoise2D(42);
let placed = 0;
for (let i = 0; i < count * 3 && placed < count; i++) {
x = (.() - ) * terrainSize;
z = (.() - ) * terrainSize;
h = (x / terrainSize + , z / terrainSize + ) * maxHeight;
nh = h / maxHeight;
(nh < || nh > ) ;
((x * , z * ) < ) ;
dummy..(x, h, z);
dummy.. = .() * . * ;
dummy..( + .());
dummy.();
mesh.(placed++, dummy.);
}
mesh. = placed;
mesh.. = ;
mesh;
}
() {
na = a.(), nb = b.();
pA = na..., pB = nb...;
nA = na..., nB = nb...;
pos = (pA. + pB.);
nor = (nA. + nB.);
pos.(pA); pos.(pB, pA.);
nor.(nA); nor.(nB, nA.);
geo = .();
geo.(, .(pos, ));
geo.(, .(nor, ));
geo;
}
Performance Guidelines
- Geometry budget: 64×64 for distant chunks, 256×256 for close. Never exceed 512×512.
- Instanced rendering: Always use
InstancedMesh for repeated objects. One draw call for 10K instances beats 10K meshes by ~100×.
- Dispose aggressively:
.dispose() geometry, materials, textures when removing chunks.
- Shadow optimization: One shadow-casting directional light. Use
CSM addon for large terrains.
- Vertex totals: Mobile < 500K, Desktop < 2M across visible scene.
- WebGPU compute: 10–50× faster than CPU for 1024² heightmaps. Use for real-time sculpting.
Noise Selection Guide
| Noise Type | Character | Best For |
|---|
| FBM | Smooth rolling hills | Meadows, plains |
| Ridged Multifractal | Sharp ridges/valleys | Mountains, canyons |
| Domain Warping | Organic twisted forms | Fantasy, alien terrain |
| Terraced FBM | Stepped plateaus | Mesas, rice paddies |
Combine multiplicatively for complex terrain:
function complexTerrain(noise, x, y) {
const base = fbm(noise, x * 0.5, y * 0.5, 4) * 0.5 + 0.5;
const mountains = ridgedMultifractal(noise, x, y, 6) * 0.4;
const detail = domainWarp(noise, x * 2, y * 2, 0.2) * 0.1;
return Math.max(base * 0.5 + mountains * base + detail, 0);
}
Common Pitfalls
- Normals not recomputed after vertex modification → flat/unlit terrain. Always call
geometry.computeVertexNormals().
- Chunk seams → sample identical world-space noise at shared edges.
- Z-fighting on water → use
polygonOffset or small Y offset.
- Memory leaks → dispose geometries on chunk removal. Monitor with
renderer.info.
- WebGPU silent failure → always gate behind
WebGPU.isAvailable().
References
references/wgsl-shaders.md — Complete WGSL compute shaders for GPU heightmap generation, erosion simulation, and normal computation.
references/noise-algorithms.md — Mathematical foundations and advanced noise variants (Voronoi, analytical derivatives, curl noise).