Шум Перліна та fBm — як математика малює природу
Гори, хмари, дно океану, вогонь, мармур — усе це генерується з однієї елегантної функції. Кен Перлін винайшов градієнтний шум, працюючи над фільмом Tron (1982). Чотири десятиліття потому він залишається стандартним інструментом для процедурної генерації контенту.
1. Типи шуму: значення проти градієнта
Усі функції процедурного шуму відображають неперервну координату на
псевдовипадкове значення, що змінюється плавно — на відміну
від Math.random(), який є розривним. Ключова властивість:
близькі вхідні дані дають близькі вихідні (просторова когерентність).
- Шум значень (value noise): присвоюємо випадкове значення кожній цілочисловій точці сітки, а потім інтерполюємо між ними. Просто, але дає «розмиту» низькочастотну картину.
- Градієнтний шум (Перліна): присвоюємо випадковий градієнтний вектор кожній точці сітки. Значення шуму в будь-якій позиції — це скалярний добуток градієнта на вектор відстані. Дає багатші, природніші на вигляд візерунки.
- Симплекс-шум: удосконалення Кена Перліна 2001 року — використовує симплекс-сітку (трикутники у 2D, тетраедри у 3D) замість квадратної сітки, зменшуючи обчислення з 2ⁿ до n+1 кутів і усуваючи артефакт, вирівняний за осями.
2. Таблиця перестановок і хеш
Критичний інгредієнт для «випадкової, але відтворюваної» вибірки — це
таблиця перестановок — масив
P[256], що містить перемішану послідовність цілих чисел
0…255. Таблицю подвоюють (P[512]), щоб уникнути взяття за
модулем під час індексації від’ємними значеннями.
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 ребер.
( 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:
Кубічний 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.
6. Дробовий броунівський рух (fBm)
Одна октава шуму Перліна дає плавні, м’яко хвилясті пагорби. Реальний рельєф має дрібномасштабну шорсткість поверх великомасштабної структури — це моделюється дробовим броунівським рухом: підсумовуванням кількох октав шуму, кожна з подвоєною частотою та вдвічі меншою амплітудою.
frequencyᵢ = lacunarity^i (lacunarity ≈ 2.0)
amplitudeᵢ = gain^i (gain ≈ 0.5, також зване persistence)
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);
}
}
Варіанти
-
turbulence(x,y) — сума
|noise|замістьnoise. Дає схожі на хмари спалахи та мармурові прожилки. -
ridged multifractal —
1 − |noise|на октаву, з піднесенням у степінь. Дає різкі гірські гребені. - деформація домену (domain warping) — використовуємо один виклик fBm, щоб деформувати вхідні координати іншого. Створює драматично закручені, схожі на ерозію візерунки (техніка Iñigo Quílez).
// Деформація домену — у стилі 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);
}
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). Це дає:
- Менше кутів сітки для обчислення: n+1 замість 2ⁿ (4 кути → 3 у 2D; 8 → 4 у 3D).
- Жодних спрямованих артефактів, вирівняних за осями, від кутів квадратної сітки.
- Неперервна похідна всюди (корисно для генерації карт нормалей).
| Властивість | Класичний Перлін | Симплекс |
|---|---|---|
| Кутів на обчислення (2D) | 4 | 3 |
| Кутів на обчислення (3D) | 8 | 4 |
| Артефакти сітки | Видимі на кратних 90° | Немає (поворотна симетрія) |
| Неперервність похідної | C² (квінтичний fade) | C¹ (але без артефактів) |
| Складність GLSL | Середня | Трохи нижча |
| Патентний статус | Суспільне надбання | Був запатентований (US6867776) — закінчився 2021 |
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 і шуму Перліна саме так, як описано тут.