Процедурний острів за одну годину
Об'єднайте все, що потрібно хорошому острову — гірський силует на шумі, узбережжя, що справді спадає, розмиті ерозією долини та пляжі, кольори біомів за висотою, і плавно анімовану воду — в один самодостатній генератор 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;
}
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);
}`
});
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);
Часті запитання
Чого я навчуся в цьому уроці?
Створіть повноцінний процедурний острів у Three.js за одну годину: heightmap на шумі Перліна, радіальна маска спаду, прохід гідравлічної ерозії, розфарбування біомів і анімовані хвилі узбережжя.
Які теми розглядаються в цьому уроці?
Цей урок охоплює такі теми: Базовий heightmap на шумі fBm, Радіальна маска спаду — рельєф в острів, Прохід гідравлічної ерозії, Генерація сітки та нормалей, Розфарбування біомів за висотою й нахилом, Вода узбережжя та хвилі, Складання все разом.
Скільки часу займає цей урок?
Цей урок займає приблизно 60 хв.
Які попередні знання потрібні?
Це урок рівня «Середній рівень» — окрема попередня підготовка, крім базового JavaScript, не потрібна.