🏔️ Szum Perlina: algorytm stojący za naturalną losowością

Czysty szum losowy wygląda jak śnieg telewizyjny — każdy piksel jest niezależny, brak struktury, brak spójności. Zjawiska naturalne są inne: chmury mają wielkoskalowe kłęby i drobnoskalowe strzępy. Teren ma pasma górskie i skaliste detale. Algorytm Kena Perlina z 1983 roku generuje szum o właśnie takim wieloskalowym, spójnym charakterze — i przyniósł mu on Techniczną Nagrodę Akademii Filmowej.

Problem z szumem białym

Szum biały przypisuje każdemu punktowi w przestrzeni całkowicie niezależną wartość losową. Jego widmo mocy jest płaskie — wszystkie częstotliwości wnoszą jednakowy wkład. Naturalne tekstury są zupełnie inne: większość ich mocy przypada na niskie częstotliwości (cechy wielkoskalowe), a moc maleje przy wyższych częstotliwościach (drobne detale). Widmo mocy naturalnych tekstur przybliżenie podąża za wzorem P(f) ~ 1/f² — cechą charakterystyczną struktur fraktalnych, samopodobnych.

Interpolowany szum wartości — rozmieszczanie losowych wartości na siatce i interpolowanie między nimi — zapewnia spójność w skali siatki, ale wykazuje wyraźne artefakty wyrównane do siatki, gdy częstotliwość odpowiada rozstawowi siatki. Szum Perlina rozwiązuje ten problem, używając losowych gradientów zamiast losowych wartości.

Szum gradientowy: intuicja Perlina

Klasyczny szum Perlina przypisuje każdemu całkowitoliczbowemu punktowi siatki losowy jednostkowy wektor gradientu. Wartość szumu w punkcie P jest obliczana poprzez:

  1. Znalezienie otaczających narożników siatki (4 w 2D, 8 w 3D)
  2. Dla każdego narożnika obliczenie iloczynu skalarnego gradientu tego narożnika z wektorem od narożnika do P
  3. Płynną interpolację tych iloczynów skalarnych za pomocą funkcji wygładzającej (fade)
// Ulepszona funkcja wygładzająca (fade) Kena Perlina (2002)
// 6t^5 - 15t^4 + 10t^3 — zerowa pierwsza i druga pochodna w 0 i 1
function fade(t) {
  return t * t * t * (t * (t * 6 - 15) + 10);
}

// Iloczyn skalarny gradientu z wektorem przesunięcia
function grad(hash, x, y) {
  const h = hash & 3;
  const u = h < 2 ? x : y;
  const v = h < 2 ? y : x;
  return ((h & 1) ? -u : u) + ((h & 2) ? -v : v);
}

Funkcja wygładzająca jest kluczowa. Oryginalny szum Perlina wykorzystywał funkcję sześcienną 3t² − 2t³ (smoothstep), która ma zerową pierwszą pochodną w 0 i 1, ale niezerową drugą pochodną — co tworzyło widoczne nieciągłości na mapach normalnych i mapach wypukłości (bump maps). Funkcja piątego stopnia z 2002 roku, 6t⁵ − 15t⁴ + 10t³, ma zerową pierwszą i drugą pochodną na krańcach, zapewniając ciągłość klasy C² i eliminując te artefakty.

Użycie gradientów zamiast wartości eliminuje pasmowanie wyrównane do siatki, które dręczy szum wartości. Konstrukcja oparta na iloczynie skalarnym gwarantuje, że szum płynnie przechodzi przez zero w każdym punkcie siatki (ponieważ wektor przesunięcia ma tam zerową długość), tworząc przyjemną właściwość „przejść przez zero w węzłach kraty", która unika problemów obciążenia typowych dla interpolowanego szumu wartości.

Oktawy, persistence i szum fraktalny

Pojedyncza oktawa szumu Perlina ma jedną charakterystyczną częstotliwość — określoną przez rozstaw siatki. Aby stworzyć wieloskalową, fraktalnopodobną teksturę, sumujemy wiele oktaw o rosnącej częstotliwości i malejącej amplitudzie:

function fbm(x, y, octaves, persistence, lacunarity) {
  let value = 0;
  let amplitude = 1.0;
  let frequency = 1.0;
  let maxValue = 0;

  for (let i = 0; i < octaves; i++) {
    value += perlin(x * frequency, y * frequency) * amplitude;
    maxValue += amplitude;
    amplitude *= persistence;   // zwykle 0.5
    frequency *= lacunarity;    // zwykle 2.0
  }
  return value / maxValue;  // normalizacja do [-1, 1]
}

Przy persistence = 0,5 i lacunarity = 2,0 każda oktawa podwaja częstotliwość i zmniejsza amplitudę o połowę. Po N oktawach najdrobniejsze detale są 2^N razy mniejsze niż najgrubsze. Widmo mocy podąża za wzorem P(f) ~ f^(-2·log₂(1/persistence)) — dla persistence = 0,5 daje to P(f) ~ f⁻², co odpowiada statystykom naturalnych tekstur.

Zmiana persistence zmienia charakter tekstury: wysoka persistence (0,7–0,9) daje wyraziste, kontrastowe tekstury z wyeksponowanymi drobnymi detalami. Niska persistence (0,2–0,4) daje gładkie, łagodnie faliste powierzchnie, gdzie dominują grube cechy. Turbulencja wykorzystuje wartość bezwzględną każdej oktawy — |noise(x,y)| — tworząc ostre grzbiety i kształty przypominające kłębiące się chmury.

Szum simpleksowy: ulepszenie Perlina z 2001 roku

Szum simpleksowy Perlina z 2001 roku eliminuje dwie słabości oryginalnego algorytmu: złożoność obliczeniową (O(2^N) narożników siatki w N wymiarach staje się kosztowne dla 4D i więcej) oraz artefakty kierunkowe (siatka kwadratowa/sześcienna ma kierunki uprzywilejowane).

Szum simpleksowy wykorzystuje kratę simpleksową — najprostszy kształt geometryczny w N wymiarach. W 2D simpleksy to trójkąty równoboczne (3 narożniki) zamiast kwadratów (4 narożniki). W 3D — czworościany (4 narożniki) zamiast sześcianów (8 narożników). W N wymiarach — (N+1) narożników zamiast 2^N. Oszczędności złożoności dla szumu wysokowymiarowego są dramatyczne: 4D szum simpleksowy wymaga 5 obliczeń gradientu w porównaniu z 16 w oryginalnym algorytmie.

Krata simpleksowa ma też lepszą izotropię — odległość od dowolnego punktu wewnętrznego do najbliższego narożnika zmienia się mniej niż w kracie kwadratowej, co zmniejsza obciążenie kierunkowe wyniku. Szum simpleksowy stał się standardem dla wysokowydajnych shaderów GPU i silników gier.

Zastosowania: teren, chmury i systemy cząstek

Generowanie terenu nakłada oktawy z domain warping — podając wynik szumu z powrotem jako przesunięcie wejścia: height(p) = fbm(p + fbm(p)). Ten „zniekształcony" szum fraktalny tworzy meandrujące doliny i nawisające urwiska charakterystyczne dla realistycznych gór. Symulacja erozji (hydraulicznej i termicznej) dodatkowo dopracowuje teren, przenosząc materiał z miejsc o wysokim gradiencie do miejsc o niskim gradiencie, wyostrzając grzbiety i wygładzając doliny.

Animowany szum napędza wiele efektów w symulacjach na tej stronie. Turbulencja płynu wykorzystuje szum 3D lub 4D (dodając czas jako wymiar), by generować przestrzennie spójne zaburzenia prędkości. Przemieszczanie ziaren piasku wykorzystuje szum 2D przewijany w kierunku wiatru. Efekt zorzy polarnej wykorzystuje szum ze zniekształceniem dziedziny (domain warping) dla charakterystycznych kształtów kurtyn.

Symulacja terenu wykorzystuje fraktalny szum Perlina z erozją do generowania realistycznych krajobrazów w czasie rzeczywistym. Symulacja piasku wykorzystuje sterowane szumem pola wiatru do kontrolowania wzorców ruchu cząstek.