Згенеруйте гірський ландшафт у браузері за допомогою фрактального
броунівського руху, а потім запустіть симуляцію гідравлічної ерозії на
основі крапель, щоб вирізьбити реалістичні долини, річки та осипні конуси
— і все це без сервера чи кроку збірки.
1Генеруємо карту висот за допомогою fBm
Фрактальний броунівський рух (fBm) накладає кілька частот шуму Перліна
(їх називають октавами). Кожна октава масштабується
коефіцієнтом стійкості (persistence) та множником
лакунарності (lacunarity):
// Мінімальний 2D-шум у стилі Перліна (для стислості — value noise) function
hash(x, y) { const n = Math.sin(x * 127.1 + y * 311.7) * 43758.5453;
return n - Math.floor(n); } function smoothNoise(x, y) { const ix =
Math.floor(x), iy = Math.floor(y); const fx = x - ix, fy = y - iy;
const ux = fx * fx * (3 - 2 * fx); const uy = fy * fy * (3 - 2 * fy);
const a = hash(ix, iy), b = hash(ix+1, iy); const c = hash(ix, iy+1),
d = hash(ix+1, iy+1); return a + (b-a)*ux + (c-a)*uy +
(b-a+a-b-c+d)*ux*uy; } function fbm(x, y, octaves = 6, persistence =
0.5, lacunarity = 2.0) { let value = 0, amplitude = 1, frequency = 1,
maxValue = 0; for (let i = 0; i < octaves; i++) { value += amplitude *
smoothNoise(x * frequency, y * frequency); maxValue += amplitude;
amplitude *= persistence; frequency *= lacunarity; } return value /
maxValue; // нормалізовано 0…1 } // Будуємо пласку карту висот Float32Array
const N = 512; const heightMap = new Float32Array(N * N); for (let y =
0; y < N; y++) for (let x = 0; x < N; x++) heightMap[y * N + x] =
fbm(x / 128, y / 128);
Порада: Для природнішого вигляду застосуйте
викривлення області (domain warping) перед семплюванням fBm:
fbm(x + fbm(x+0.1, y+1.3), y + fbm(x+5.2, y+1.7)). Це
надає рельєфу «скрученого» вигляду — чудово підходить для безплідних
земель та вулканічних ландшафтів.
2Будуємо меш рельєфу у Three.js
Використайте PlaneGeometry та оновіть його буфер вершин,
щоб відобразити карту висот. Зміщуйте кожну вершину вздовж осі Y:
import * as THREE from
'https://cdn.jsdelivr.net/npm/three@0.160/build/three.module.js';
const SEGMENTS = 511; // N-1 для сітки з N точок const SIZE = 100; const
HEIGHT = 20; // коефіцієнт вертикального масштабу const geometry = new
THREE.PlaneGeometry(SIZE, SIZE, SEGMENTS, SEGMENTS);
geometry.rotateX(-Math.PI / 2); // робимо площину горизонтальною const positions
= geometry.attributes.position; for (let i = 0; i <
positions.count; i++) { const h = heightMap[i]; // вже нормалізовано
0…1 positions.setY(i, h * HEIGHT); } positions.needsUpdate = true;
geometry.computeVertexNormals(); const mesh = new THREE.Mesh(geometry,
new THREE.MeshStandardMaterial({ vertexColors: true, wireframe: false
})); scene.add(mesh);
3Розфарбовуємо за висотою
Додайте до геометрії атрибут кольорів. Зіставте діапазони висот із
біомами рельєфу — глибока вода, мілкий пляж, трава, скелі, сніг:
Скиньте віртуальну краплю води у випадковій позиції. Вона ковзає вниз
по найкрутішому градієнту, підхоплює осад і відкладає його, коли
сповільнюється. Повторіть для 50 000–150 000 крапель:
function erode(heightMap, N, numDroplets = 80000) { for (let d = 0; d
< numDroplets; d++) { let px = Math.random() * (N - 2) + 1; let py
= Math.random() * (N - 2) + 1; let vx = 0, vy = 0; // швидкість let
water = 1, sediment = 0; const inertia = 0.05, capacity = 8, erosion =
0.3, deposition = 0.3, gravity = 4, evaporation = 0.01; for (let step
= 0; step < 128; step++) { const ix = Math.floor(px), iy =
Math.floor(py); if (ix < 1 || ix >= N-1 || iy < 1 || iy >= N-1)
break; // Білінійна висота та градієнт const fx = px - ix, fy = py -
iy; const h00 = heightMap[iy*N+ix], h10 = heightMap[iy*N+ix+1]; const
h01 = heightMap[(iy+1)*N+ix], h11 = heightMap[(iy+1)*N+ix+1]; const gx
= (h10-h00)*(1-fy) + (h11-h01)*fy; const gy = (h01-h00)*(1-fx) +
(h11-h10)*fx; vx = vx * inertia - gx * (1 - inertia); vy = vy *
inertia - gy * (1 - inertia); const speed = Math.sqrt(vx*vx + vy*vy);
if (speed < 1e-6) break; vx /= speed; vy /= speed; const sedCap =
Math.max(-( gx*vx + gy*vy ), 0.01) * speed * water * capacity; if
(sediment > sedCap) { // відкладення const dep = (sediment - sedCap) *
deposition; sediment -= dep; heightMap[iy*N+ix] += dep *
(1-fx)*(1-fy); heightMap[iy*N+ix+1] += dep * fx*(1-fy);
heightMap[(iy+1)*N+ix] += dep * (1-fx)*fy; heightMap[(iy+1)*N+ix+1] +=
dep * fx*fy; } else { // ерозія const er = Math.min((sedCap - sediment)
* erosion, 0.05); sediment += er; heightMap[iy*N+ix] -= er *
(1-fx)*(1-fy); heightMap[iy*N+ix+1] -= er * fx*(1-fy);
heightMap[(iy+1)*N+ix] -= er * (1-fx)*fy; heightMap[(iy+1)*N+ix+1] -=
er * fx*fy; } px += vx; py += vy; water *= (1 - evaporation); } } }
Продуктивність: Обробка 80 000 крапель на CPU займає
200–500 мс. Перенесіть цикл у Web Worker, щоб потік UI
залишався чутливим під час обчислення ерозії. Передавайте буфер
Float32Array через
postMessage(..., [buffer]) для передачі без копіювання.
5Додаємо водну поверхню та освітлення
// Пласка напівпрозора водна поверхня на рівні моря const waterGeo = new
THREE.PlaneGeometry(SIZE, SIZE); waterGeo.rotateX(-Math.PI / 2); const
waterMat = new THREE.MeshStandardMaterial({ color: 0x1a6fa8,
transparent: true, opacity: 0.75, roughness: 0.05 }); const water =
new THREE.Mesh(waterGeo, waterMat); water.position.y = HEIGHT * 0.27;
// 27% від макс. висоти = рівень моря scene.add(water); // Направлене
(сонячне) світло з тінями const sun = new
THREE.DirectionalLight(0xfff5e0, 2.5); sun.position.set(60, 80, 40);
sun.castShadow = true; sun.shadow.mapSize.width =
sun.shadow.mapSize.height = 2048; sun.shadow.camera.near = 0.5;
sun.shadow.camera.far = 300; sun.shadow.camera.left =
sun.shadow.camera.bottom = -80; sun.shadow.camera.right =
sun.shadow.camera.top = 80; scene.add(sun); scene.add(new
THREE.AmbientLight(0x8090a0, 0.4)); renderer.shadowMap.enabled = true;
6Продуктивність та нотатки про LOD
Розмір сітки: 512×512 = 262 144 вершини — працює на
60 FPS на середньому за потужністю обладнанні. Для 1024×1024
розгляньте варіант вимкнення тіней або використання інстансованого
шару трави лише поблизу камери.
LOD:THREE.LOD дозволяє підставляти
меші нижчої роздільності на відстані. Заздалегідь запечіть 3 рівні
роздільності (512, 256, 128) і перемикайте їх на 50 м / 150 м.
Карта нормалей: Для додаткової деталізації без
збільшення геометрії запечіть карту нормалей із карти висот високої
роздільності за допомогою стандартних інструментів і застосуйте її до
грубішого меша.
Ерозія в реальному часі на GPU: Закодуйте висоту в
float-рендертаргеті, запустіть ядро ерозії як фрагментний шейдер GLSL
та використовуйте ping-pong-фреймбуфери. Це дає 10-кратне прискорення
для великих рельєфів.