Урок · Процедурна генерація · Three.js · WebGL
📅 Липень 2026 ⏱ ≈ 60 хв 🎯 Середній рівень

Процедурний острів за одну годину

Об'єднайте все, що потрібно хорошому острову — гірський силует на шумі, узбережжя, що справді спадає, розмиті ерозією долини та пляжі, кольори біомів за висотою, і плавно анімовану воду — в один самодостатній генератор Three.js, який можна вставити в будь-який проєкт.

1. Базовий heightmap на шумі fBm

Почніть із фракційного броунівського руху (fBm): підсумуйте кілька октав шуму Перліна/simplex, кожна вищої частоти й нижчої амплітуди за попередню. Нижні октави формують широкі гори; верхні додають скелясту деталізацію поверх них.

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;       // кожна октава слабша
    frequency *= lacunarity; // кожна октава дрібніша
  }
  return sum / norm; // нормалізовано до [-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| замість сирого шуму в кожну октаву для гострих гірських хребтів замість плавних пагорбів — корисно для центрального піку вулканічного острова.

2. Радіальна маска спаду — перетворення рельєфу на острів

Сирий шум fBm нескінченно тайлиться у всіх напрямках — він ніколи не «вирішує» стати островом. Помножте його на радіальну маску спаду, яка дорівнює 1 у центрі й плавно спадає до 0 на краю мапи, тож висота спадає до рівня моря незалежно від того, що каже шум під нею.

function falloff(nx, ny) {
  // nx, ny в [-1, 1], відстань від центру
  const d = Math.sqrt(nx*nx + ny*ny);
  const a = 3, b = 2.2; // константи налаштування форми
  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; // значення тепер у [0, 1], центр високий, краї 0
}
Квадратні проти круглих островів: Ця маска дає заокруглене, природне узбережжя. Для архіпелагу згенеруйте маску в низькій роздільності з власним полем шуму замість чистої радіальної формули, потім перемножте.

3. Прохід гідравлічної ерозії

Сирий heightmap із маскою досі виглядає однорідно горбистим. Прожене пакет симульованих крапель по ньому (повний виведення формул — у статті про гідравлічну ерозію) — кожна розмиває круті схили та осаджує наноси, де сповільнюється, вирізаючи долини й нарощуючи пласкі пляжні шельфи біля берега.

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); // розмити/осадити вздовж шляху
  }
  return map;
}
Бюджет на 60 хвилин: На heightmap 256×256 20 000–50 000 крапель дають видимо еродований острів менш ніж за секунду в основному потоці — GPU compute pass для острова масштабу уроку не потрібен.

4. Генерація сітки та нормалей

Подайте еродований heightmap у Three.js PlaneGeometry, зміщуючи Z кожної вершини на відповідне значення висоти, потім перерахуйте нормалі, щоб освітлення реагувало на новий рельєф, а не на початкову нормаль пласкої площини.

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); // площина XY, зміщення вздовж локального Z
  }

  geo.rotateX(-Math.PI / 2); // покласти горизонтально: Z стає світовим Y (вгору)
  geo.computeVertexNormals();
  return new THREE.Mesh(geo, terrainMaterial);
}

5. Розфарбування біомів за висотою та нахилом

Розфарбуйте кожну вершину у фрагментному шейдері (або через буфер кольорів вершин) на основі висоти — пляж біля рівня моря, трава на середньому схилі, скеля вище, сніг на піку — із перевагою нахилу, щоб круті грані читалися як гола скеля незалежно від висоти:

// GLSL фрагментний шейдер, спрощено
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)); // круто = скеля

  gl_FragColor = vec4(color, 1.0);
}

6. Вода узбережжя та хвилі

Додайте пласку напівпрозору площину на рівні моря (висота 0). Дешева брижа у вершинному шейдері плюс член Френеля для піни там, де вода зустрічає мілководний пляж, продає ілюзію без повної симуляції океану:

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);
    }`
});
Лінія піни: Одноразово відрендерте висоту рельєфу в render target, потім у шейдері води семпліть висоту рельєфу за тими самими XY і освітлюйте пікселі, де terrainHeight у межах невеликого порогу від рівня моря — простий спосіб імітувати смугу піни без додаткової геометрії.

7. Складання все разом

Загорніть кожен крок в одну функцію-генератор із зерном, щоб те саме зерно завжди відтворювало той самий острів — важливо для обміну зерном або збереження/завантаження сцени:

function generateIsland(seed, size = 256) {
  seedNoise(seed);                       // детермінована таблиця перестановок Перліна
  let map = buildHeightmap(size);       // 1. fBm
  map = applyIslandMask(map, size);      // 2. радіальний спад
  map = erode(map, size);               // 3. гідравлічна ерозія
  const terrain = buildMesh(map, size); // 4. сітка + нормалі
  terrain.material = biomeMaterial;      // 5. шейдер біомів
  const water = buildWaterPlane(size, waterMat); // 6. узбережжя
  return { terrain, water };
}

const { terrain, water } = generateIsland(1337);
scene.add(terrain, water);
Куди рухатись далі: Додайте дерева через instanced meshes, розкидані по вершинах трав'яного біому, мерехтіння піску на пляжі, або прив'яжіть зерно до параметра URL, щоб острови можна було ділитись посиланням. Добре доповнюють цей урок статті про гідравлічну ерозію та шум Перліна для базової математики.

Часті запитання

Чого я навчуся в цьому уроці?

Створіть повноцінний процедурний острів у Three.js за одну годину: heightmap на шумі Перліна, радіальна маска спаду, прохід гідравлічної ерозії, розфарбування біомів і анімовані хвилі узбережжя.

Які теми розглядаються в цьому уроці?

Цей урок охоплює такі теми: Базовий heightmap на шумі fBm, Радіальна маска спаду — рельєф в острів, Прохід гідравлічної ерозії, Генерація сітки та нормалей, Розфарбування біомів за висотою й нахилом, Вода узбережжя та хвилі, Складання все разом.

Скільки часу займає цей урок?

Цей урок займає приблизно 60 хв.

Які попередні знання потрібні?

Це урок рівня «Середній рівень» — окрема попередня підготовка, крім базового JavaScript, не потрібна.