Математика · Процедурна генерація
📅 Березень 2026 ⏱ ≈ 13 хв читання 🎯 Середній рівень

Шум Перліна та fBm — як математика малює природу

Гори, хмари, дно океану, вогонь, мармур — усе це генерується з однієї елегантної функції. Кен Перлін винайшов градієнтний шум, працюючи над фільмом Tron (1982). Чотири десятиліття потому він залишається стандартним інструментом для процедурної генерації контенту.

1. Типи шуму: значення проти градієнта

Усі функції процедурного шуму відображають неперервну координату на псевдовипадкове значення, що змінюється плавно — на відміну від Math.random(), який є розривним. Ключова властивість: близькі вхідні дані дають близькі вихідні (просторова когерентність).

Чому градієнтний шум виглядає краще: інтерполяція значень дає широкі пласкі ділянки з різкими переходами на перетинах сітки. Інтерполяція скалярних добутків градієнтів гарантує, що похідна теж неперервна, даючи плавні хвилі без артефакту «гребенів, вирівняних за сіткою».

2. Таблиця перестановок і хеш

Критичний інгредієнт для «випадкової, але відтворюваної» вибірки — це таблиця перестановок — масив P[256], що містить перемішану послідовність цілих чисел 0…255. Таблицю подвоюють (P[512]), щоб уникнути взяття за модулем під час індексації від’ємними значеннями.

hash(x, y) = P[ P[x & 255] + (y & 255) ]

P — це фіксована перестановка 0..255, повторена двічі.
Для 3D: hash(x,y,z) = P[ P[ P[x&255] + (y&255) ] + (z&255) ]

Це дає 256 унікальних хеш-значень на комбінацію вимірів, створюючи ґратку, що повторюється кожні 256 одиниць — і цього достатньо для більшості випадків. Оригінальна таблиця Перліна фіксована (не випадкова), даючи однакові результати на всіх платформах:

// Оригінальна таблиця перестановок Кена Перліна
const PERM = [
  151,160,137, 91, 90, 15,131, 13,201, 95, 96,
  53,194,233,  7,225,140, 36,103, 30, 69,142,
  8, 99, 37,240, 21, 10, 23,190,  6,148,247,
  /* ... усього 256 значень ... */
];
// Подвоюємо, щоб уникнути загортання індексу
const P = [...PERM, ...PERM];

3. Градієнтні вектори

Кожній хешованій точці ґратки присвоюється градієнтний вектор. У 2D Перлін використовує 8 одиничних векторів з інтервалом 45°. У 3D оригінальна реалізація використовує 12 векторів, спрямованих від центра куба до кожного з його 12 ребер.

2D-градієнти (8 напрямків):
( 1, 1), (-1, 1), ( 1,-1), (-1,-1)
( 1, 0), (-1, 0), ( 0, 1), ( 0,-1)

3D-градієнти (12 напрямків):
( 1, 1, 0), (-1, 1, 0), ( 1,-1, 0), (-1,-1, 0)
( 1, 0, 1), (-1, 0, 1), ( 1, 0,-1), (-1, 0,-1)
( 0, 1, 1), ( 0,-1, 1), ( 0, 1,-1), ( 0,-1,-1)

Значення шуму в позиції (x,y) — це скалярний добуток градієнта кута сітки на вектор відстані від цього кута до точки запиту. Кути з градієнтами, спрямованими до точки запиту, дають додатні значення; кути, спрямовані геть, дають від’ємні значення.

// Скалярний добуток 2D-градієнта
function grad2(hash, x, y) {
  const h = hash & 7;  // один з 8 градієнтів
  const u = h < 4 ? x : y;
  const v = h < 4 ? y : x;
  return ((h & 1) ? -u : u) + ((h & 2) ? -v : v);
}

4. Функція fade та інтерполяція

Наївна лінійна інтерполяція між чотирма скалярними добутками кутів дає розривні перші похідні (видимі складки на межах сітки). Перлін використовує квінтичну згладжувальну криву «fade» — також звану smoothstep6 — у якої нульові перша і друга похідні при t=0 та t=1:

Лінійна інтерполяція: lerp(t, a, b) = a + t·(b − a)

Кубічний fade (Ерміта): f(t) = 3t² − 2t³ ← гладкість C¹
Квінтичний fade (Perlin2002): f(t) = 6t⁵ − 15t⁴ + 10t³ ← гладкість C²

Обидва: f(0) = 0, f(1) = 1, f'(0) = f'(1) = 0

Оригінальна стаття Перліна 1985 року використовувала кубічний fade. Покращена версія 2002 року перейшла на квінтичний, усунувши непомітні «лінійні артефакти», що з’являються з кубічним, коли шум використовується для карт нормалей чи оцінки похідних.

// Квінтичний fade (Perlin 2002)
function fade(t) {
  return t * t * t * (t * (t * 6 - 15) + 10);
}

function lerp(t, a, b) {
  return a + t * (b - a);
}

5. Класичний 2D-шум Перліна: повна реалізація

Зберемо все докупи: знаходимо одиничний квадрат, що містить точку, обчислюємо дробові частини, застосовуємо fade до дробових частин, хешуємо всі чотири кути, обчислюємо скалярні добутки градієнтів і білінійно інтерполюємо.

function noise2D(x, y) {
  // Одиничний квадрат
  const X = Math.floor(x) & 255;
  const Y = Math.floor(y) & 255;

  // Дробова частина
  x -= Math.floor(x);
  y -= Math.floor(y);

  // Криві fade
  const u = fade(x);
  const v = fade(y);

  // Хешуємо кути
  const a  = P[X]   + Y;
  const aa = P[a],  ab = P[a + 1];
  const b  = P[X+1] + Y;
  const ba = P[b],  bb = P[b + 1];

  // Білінійне змішування скалярних добутків градієнтів
  return lerp(v,
    lerp(u, grad2(P[aa], x,   y  ),
             grad2(P[ba], x-1, y  )),
    lerp(u, grad2(P[ab], x,   y-1),
             grad2(P[bb], x-1, y-1))
  );
}

Діапазон виходу у 2D становить приблизно −0.707 … +0.707 (а не −1…1, як часто вважають). Щоб нормалізувати до 0…1: v = noise2D(x, y) * 0.707 + 0.5.

Поширена помилка: вважати, що діапазон виходу точно −1…+1. Це не так — екстремальні значення досягаються лише точно під 45° вздовж градієнтного вектора. Завжди вимірюйте реальний діапазон і нормалізуйте відповідно.

6. Дробовий броунівський рух (fBm)

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

fBm(x, y) = Σᵢ amplitudeᵢ · noise(x · frequencyᵢ, y · frequencyᵢ)

frequencyᵢ = lacunarity^i (lacunarity ≈ 2.0)
amplitudeᵢ = gain^i (gain ≈ 0.5, також зване persistence)
Октава 1
50%
Низька частота, висока амплітуда. Пагорби континентального масштабу.
Октава 2
25%
Гірські хребти й гребені.
Октава 3
12.5%
Скелі й уступи.
Октава 4+
6%…
Валуни, галька, дрібна текстура.
function fbm(x, y, octaves = 6, lacunarity = 2.0, gain = 0.5) {
  let value = 0;
  let amplitude = 0.5;
  let frequency = 1.0;
  let max = 0;  // для нормалізації

  for (let i = 0; i < octaves; i++) {
    value     += amplitude * noise2D(x * frequency, y * frequency);
    max       += amplitude;
    amplitude *= gain;
    frequency *= lacunarity;
  }

  return value / max;  // нормалізуємо приблизно до −1..1
}

// Приклад рельєфу: карта висот 512×512
for (let y = 0; y < 512; y++) {
  for (let x = 0; x < 512; x++) {
    const scale = 0.004;  // віддалення
    heightmap[y * 512 + x] =
      fbm(x * scale, y * scale, 8, 2.0, 0.5);
  }
}

Варіанти

// Деформація домену — у стилі Iñigo Quílez
function warpedFbm(x, y) {
  // q зміщує на одне поле fBm
  const qx = fbm(x,       y,       4);
  const qy = fbm(x + 5.2, y + 1.3, 4);

  // r зміщує на q
  const rx = fbm(x + 4.0*qx + 1.7,  y + 4.0*qy + 9.2,  4);
  const ry = fbm(x + 4.0*qx + 8.3,  y + 4.0*qy + 2.8,  4);

  return fbm(x + 4.0*rx, y + 4.0*ry, 5);
}

7. Реалізація на GLSL

На GPU таблиця перестановок стає 1D-текстурою на 256 пікселів (або вбудовується inline). Ось компактна й широко використовувана реалізація GLSL-хешу + 2D-шуму Перліна від Ian McEwan / Ashima (під ліцензією MIT), яка працює у фрагментному шейдері без жодних звернень до текстур:

// GLSL — компактний 2D-шум Перліна (хеш через арифметику)
vec2 fade2(vec2 t) {
  return t*t*t*(t*(t*6.0-15.0)+10.0);
}

vec4 permute4(vec4 x) {
  return mod(x*x*34.0+x, 289.0);
}

float cnoise2D(vec2 P) {
  vec4 Pi = floor(P.xyxy) + vec4(0,0,1,1);
  vec4 Pf = fract(P.xyxy) - vec4(0,0,1,1);
  Pi = mod(Pi, 289.0);
  vec4 ix = Pi.xzxz, iy = Pi.yyww;
  vec4 fx = Pf.xzxz, fy = Pf.yyww;
  vec4 i  = permute4(permute4(ix) + iy);
  vec4 gx = 2.0*fract(i/41.0) - 1.0;
  vec4 gy = abs(gx) - 0.5;
  gx = gx - floor(gx + 0.5);
  vec2 g00 = vec2(gx.x, gy.x);
  vec2 g10 = vec2(gx.y, gy.y);
  vec2 g01 = vec2(gx.z, gy.z);
  vec2 g11 = vec2(gx.w, gy.w);
  vec4 norm = 1.79284291400159 - 0.85373472095314*vec4(
    dot(g00,g00), dot(g01,g01), dot(g10,g10), dot(g11,g11));
  g00 *= norm.x; g01 *= norm.y; g10 *= norm.z; g11 *= norm.w;
  float n00 = dot(g00, fx.xy);
  float n10 = dot(g10, fx.zy);
  float n01 = dot(g01, fy.xw);
  float n11 = dot(g11, fy.zw);
  vec2 fade_xy = fade2(fract(P));
  vec2 n_x = mix(vec2(n00,n01), vec2(n10,n11), fade_xy.x);
  return 2.3 * mix(n_x.x, n_x.y, fade_xy.y);
}
fBm на GLSL у 4 рядки: щойно маєте cnoise2D, fBm — це просто цикл. У GLSL розгорніть перші 4 октави вручну заради продуктивності на старіших мобільних GPU.
// fBm у фрагментному шейдері GLSL
float fbm(vec2 p) {
  float value = 0.0;
  float amplitude = 0.5;
  for (int i = 0; i < 6; i++) {
    value += amplitude * cnoise2D(p);
    p *= 2.0;
    amplitude *= 0.5;
  }
  return value;
}

// Використання в main():
float h = fbm(vUv * 4.0 + u_time * 0.05);
vec3 color = mix(vec3(0.0,0.1,0.4), vec3(0.9,0.95,1.0), h*0.5+0.5);

8. Симплекс-шум

Ревізія Кена Перліна 2001 року замінила квадратну/кубічну сітку симплекс-ґраткою — найпростішим багатогранником у кожному вимірі (відрізок у 1D, трикутник у 2D, тетраедр у 3D). Це дає:

Властивість Класичний Перлін Симплекс
Кутів на обчислення (2D) 4 3
Кутів на обчислення (3D) 8 4
Артефакти сітки Видимі на кратних 90° Немає (поворотна симетрія)
Неперервність похідної C² (квінтичний fade) C¹ (але без артефактів)
Складність GLSL Середня Трохи нижча
Патентний статус Суспільне надбання Був запатентований (US6867776) — закінчився 2021
Практичний вибір: після закінчення патенту на симплекс обидва тепер повністю вільні. Для шейдерів, що мають обчислювати шум у 3D+ для кожного пікселя, симплекс швидший. Для попередньо обчислених карт висот у JS класичний Перлін простіший у реалізації.

9. Застосування: рельєф, хмари, текстури

Карта висот рельєфу

fBm дає базову карту висот. Додайте степеневу криву (h = h^2.5), щоб загострити піки, зберігаючи долини рівними. Помножте на «маску континентальності» (інший низькочастотний шум), щоб створити острови, розділені океаном:

function getHeight(x, y) {
  let h = fbm(x * 0.003, y * 0.003, 8) * 0.5 + 0.5;
  const continent =
    fbm(x*0.0008, y*0.0008, 2) * 0.5 + 0.5;
  h = h * continent;
  h = Math.pow(h, 2.2);  // загострюємо гори
  return h;
}

Анімовані хмари

Тривимірний шум, обчислений у (x, y, time * speed), дає повільно рухомі хмари. Застосуйте поріг до значення для чітких країв хмар або плавну рампу для розмитих країв:

// Фрагментний шейдер — анімований шар хмар
float t = u_time * 0.07;
float c = fbm(vUv * 3.0 + vec2(t, t*0.4));
float cloud = smoothstep(0.45, 0.65, c * 0.5 + 0.5);

Текстури мармуру та дерева

Класичний мармур: використовуйте sin(x * freq + turbulence(x,y) * scale). Турбулентність спотворює синусоїдальні смуги у природні на вигляд прожилки.

// Візерунок мармурових прожилок
float marble(vec2 p) {
  float n = fbm(p);
  return sin(p.x * 6.0 + n * 8.0) * 0.5 + 0.5;
}

// Кільця деревини
float wood(vec2 p) {
  float r = length(p) * 12.0;
  r += fbm(p * 2.0) * 4.0;
  return fract(r);
}

📐 Математика та фрактали

Генератор процедурного рельєфу та дослідник фракталів незабаром з’являться в категорії «Математика» — з використанням fBm і шуму Перліна саме так, як описано тут.

Категорія «Математика» →