Procedural terrain

절차적 지형

Layering noise functions to automatically generate natural-looking hills and mountains.

Also known as: HeightmapfBm noiseSimplexNoise
···
js
import * as THREE from 'three';
import { SimplexNoise } from 'three/addons/math/SimplexNoise.js';
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setPixelRatio(Math.min(devicePixelRatio, 2));
document.body.appendChild(renderer.domElement);
const scene = new THREE.Scene();
const skyColor = 0x0e1730;
scene.background = new THREE.Color(skyColor);
scene.fog = new THREE.Fog(skyColor, 4, 13);
const camera = new THREE.PerspectiveCamera(50, 1, 0.1, 60);

scene.add(new THREE.AmbientLight(0x33406a, 1.1));
const sun = new THREE.DirectionalLight(0xffe3b0, 1.3);
sun.position.set(4, 6, 2);
scene.add(sun);

const SIZE = 8, SEG = 90;
const geo = new THREE.PlaneGeometry(SIZE, SIZE, SEG, SEG);
geo.rotateX(-Math.PI / 2);
const simplex = new SimplexNoise();
const pos = geo.attributes.position;
const colors = [];
const low = new THREE.Color(0x1c5a3c), mid = new THREE.Color(0x6b7a4a), high = new THREE.Color(0xf4f1e6);
for (let i = 0; i < pos.count; i++) {
  const x = pos.getX(i), z = pos.getZ(i);
  let h = 0, amp = 1, freq = 0.35, norm = 0;
  for (let o = 0; o < 4; o++) {
    h += simplex.noise(x * freq, z * freq) * amp;
    norm += amp;
    amp *= 0.5;
    freq *= 2.1;
  }
  h = (h / norm) * 1.4;
  pos.setY(i, h);
  const tone = h < 0.15
    ? low.clone().lerp(mid, THREE.MathUtils.clamp((h + 0.3) / 0.45, 0, 1))
    : mid.clone().lerp(high, THREE.MathUtils.clamp((h - 0.15) / 0.55, 0, 1));
  colors.push(tone.r, tone.g, tone.b);
}
geo.setAttribute('color', new THREE.Float32BufferAttribute(colors, 3));
geo.computeVertexNormals();
const terrain = new THREE.Mesh(geo, new THREE.MeshStandardMaterial({ vertexColors: true, roughness: 0.95 }));
scene.add(terrain);

function resize() { renderer.setSize(innerWidth, innerHeight); camera.aspect = innerWidth / innerHeight; camera.updateProjectionMatrix(); }
addEventListener('resize', resize); resize();
renderer.setAnimationLoop((t) => {
  const time = t * 0.00012;
  camera.position.set(Math.sin(time) * 4.2, 2.1, Math.cos(time) * 4.2 + 1.2);
  camera.lookAt(0, 0, 0);
  renderer.render(scene, camera);
});

A single layer of noise is too smooth and cloud-like to read as a mountain. To look like real terrain, you sum multiple octaves — fBm (fractal Brownian motion) roughly doubles the frequency and halves the amplitude each pass, adding the noise together repeatedly. Low frequencies shape the big rolling hills; high frequencies add rippling detail.

This demo calls three.js’s THREE.SimplexNoise addon on the CPU, four octaves per vertex of a PlaneGeometry, to set each vertex’s height (y), then interpolates vertex color by elevation band — green (lowland) → grayish-brown (midslope) → white (snowcap). Calling geometry.computeVertexNormals() afterward to recompute normals for lighting is essential too.

The difference from vertex-displacement is where the math runs: that entry computes noise inside the shader every frame to animate a moving surface (waves, say); here it’s computed once on the CPU when the geometry is built, producing a static terrain. If nothing needs to move in real time, computing it once like this is far cheaper.

When to use

Game worlds, map-visualization backgrounds, any static terrain that needs "natural randomness."