Skip to main content

procedural-landscapes

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".

Source facts

Repository
CK42BB/procedural-landscapes-threejs
Last source activity
February 8, 2026 at 21:32
Detected SKILL.md language
English
Stars
11
Forks
2

Install options

The review-first prompt is selected by default. You can switch to a direct command or download a local copy.

Review the source files

Read SKILL.md and any companion files shown by SkillsMP before deciding whether to install.

Showing SKILL.md

SKILL.md
Source instructions · Read-only preview
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. ```javascript 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+)**: ```html <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. ```javascript 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); const t0 = (i + j) * G2; const x0 = x - (i - t0), y0 = y - (j - t0); const i1 = x0 > y0 ? 1 : 0, j1 = x0 > y0 ? 0 : 1; const x1 = x0 - i1 + G2, y1 = y0 - j1 + G2; const x2 = x0 - 1 + 2 * G2, y2 = y0 - 1 + 2 * G2; const ii = i & 255, jj = j & 255; let n0 = 0, n1 = 0, n2 = 0; let t = 0.5 - x0*x0 - y0*y0; if (t > 0) { const g = grad[perm[ii + perm[jj]] & 7]; n0 = t*t*t*t * (g[0]*x0 + g[1]*y0); } t = 0.5 - x1*x1 - y1*y1; if (t > 0) { const g = grad[perm[ii+i1 + perm[jj+j1]] & 7]; n1 = t*t*t*t * (g[0]*x1 + g[1]*y1); } t = 0.5 - x2*x2 - y2*y2; if (t > 0) { const g = grad[perm[ii+1 + perm[jj+1]] & 7]; n2 = t*t*t*t * (g[0]*x2 + g[1]*y2); } return 70 * (n0 + n1 + n2); }; } ``` ### FBM and Terrain Noise Functions ```javascript 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, 6); } ``` ### 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 ```javascript 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. ```javascript 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); const radius = Math.ceil(this.viewDistance / this.chunkSize); const activeKeys = new Set(); for (let x = cx - radius; x <= cx + radius; x++) { for (let z = cz - radius; z <= cz + radius; z++) { const key = `${x},${z}`; activeKeys.add(key); const worldX = x * this.chunkSize; const worldZ = z * this.chunkSize; const dist = Math.hypot(cameraPosition.x - worldX, cameraPosition.z - worldZ); if (dist > this.viewDistance) continue; const lod = this.lodLevels.find(l => dist < l.distance); const existing = this.chunks.get(key); if (!existing || existing.lod !== lod.segments) { if (existing) { this.scene.remove(existing.mesh); existing.mesh.geometry.dispose(); } const mesh = this._createChunk(x, z, lod.segments); this.chunks.set(key, { mesh, lod: lod.segments }); this.scene.add(mesh); } } } for (const [key, chunk] of this.chunks) { if (!activeKeys.has(key)) { this.scene.remove(chunk.mesh); chunk.mesh.geometry.dispose(); this.chunks.delete(key); } } } _createChunk(cx, cz, segments) { const geo = createTerrainGeometry( this.chunkSize, segments, (nx, nz) => { const wx = (cx + nx) * this.chunkSize / 500; const wz = (cz + nz) * this.chunkSize / 500; return this.heightFn(wx, wz); }, this.maxHeight ); const mesh = new THREE.Mesh(geo, createTerrainMaterial()); mesh.position.set(cx * this.chunkSize, 0, cz * this.chunkSize); mesh.receiveShadow = true; return mesh; } dispose() { for (const [, chunk] of this.chunks) { this.scene.remove(chunk.mesh); chunk.mesh.geometry.dispose(); } this.chunks.clear(); } } ``` ## Terrain Materials ### Slope + Height Shader (WebGL) ```javascript 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); } `, fragmentShader: ` uniform float waterLevel, snowLevel, maxHeight; uniform vec3 grassColor, rockColor, sandColor, snowColor, sunDir; varying vec3 vWorldPos; varying vec3 vNormal; void main() { float h = clamp(vWorldPos.y / maxHeight, 0.0, 1.0); float slope = 1.0 - dot(vNormal, vec3(0, 1, 0)); vec3 color = grassColor; if (h < waterLevel + 0.05) color = mix(sandColor, grassColor, smoothstep(waterLevel, waterLevel + 0.05, h)); if (h > snowLevel) color = mix(color, snowColor, smoothstep(snowLevel, snowLevel + 0.1, h)); color = mix(color, rockColor, smoothstep(0.3, 0.6, slope)); float light = max(dot(vNormal, sunDir), 0.0) * 0.7 + 0.3; gl_FragColor = vec4(color * light, 1.0); } `, }); } ``` ### Node Material (WebGPU TSL) When using WebGPURenderer, prefer Three.js Shading Language (TSL) node materials: ```javascript 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, smoothstep(0.3, 0.6, slope)); material.colorNode = c; material.roughnessNode = mix(tslFloat(0.9), tslFloat(0.5), slope); return material; } ``` ## Water ```javascript 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.setY(i, Math.sin(x * 0.05 + time) * 0.3 + Math.cos(z * 0.08 + time * 0.7) * 0.2); } pos.needsUpdate = true; geometry.computeVertexNormals(); }; return water; } ```
View on GitHub
This SKILL.md is very large, so SkillsMP previews the first section here. View on GitHub