Laboratorul 02. Perlin Noise

Perlin noise, dezvoltat de Ken Perlin în 1983, este o funcție de zgomot gradient utilizată pe scară largă în grafica computerizată pentru a crea texturi, terenuri și alte conținuturi procedurale cu aspect natural. Spre deosebire de zgomotul aleatoriu simplu, Perlin noise produce modele mai netede și mai coerente care imită neregularitățile întâlnite în natură.

Cum funcționează?

  1. Definirea unui grid: Spațiul este împărțit într-un grid în care fiecărui punct de intersecție (nod) i se atribuie un vector gradient pseudo-aleatoriu
  2. Determinarea celulei: Pentru orice punct dat în spațiu unde este necesară valoarea zgomotului, se identifică celula gridului în care se află. Această celulă este definită de nodurile înconjurătoare
  3. Vectori de offset: Se calculează vectorii de offset de la fiecare dintre nodurile colțului celulei până la punctul în cauză. Acești vectori reprezintă poziția relativă a punctului în cadrul celulei
  4. Produse scalare: Se calculează produsul scalar între fiecare vector gradient al nodului și vectorul său de offset corespunzător. Acest pas măsoară influența gradientului fiecărui colț asupra punctului
  5. Interpolare: Se amestecă aceste rezultate ale produsului scalar folosind o funcție de interpolare, adesea o funcție de atenuare, pentru a asigura tranziții fără întreruperi între valori. Această interpolare asigură că zgomotul se schimbă treptat, producând netezimea caracteristică a Perlin noise

Implementare

Pentru a genera o textură, Perlin noise primește ca input 2 parametri, x și y, coordonatele pixelilor din textură înmulțite cu un număr mic (frecvența).

for y in range(height):
    for x in range(width):
        n = perlin(x * frequency, y * frequency)

În implementare, se definește un grid, pe care va fi definită textura. Inpututul algoritmului se va afla într-o celulă a acestui grid. Pentru fiecare colț al fiecărei celule din grid se va genera un vector gradient constant. Outputul algoritmului va fi o interporlare între 4 valori, calculate în funcție de vectorul gradient al fiecărui colț și de datele de input, x și y.

Diferența dintre Perlin noise și value noise este modul în care sunt obținute aceste 4 valori. În timp ce value noise folosește un generator de numere pseudo-aleatoare, Perlin noise folosește un produs scalar între 2 vectori.

Pentru a calcula valorile necesare interpolării, se vor calcula 4 produse scalare. Pentru fiecare produs scalar, primul vector va fi generat pornind de la colțul curent la punctul de input, iar al doilea vector este vectorul gradient asignat fiecărui colț al gridului.

xf = x - floor(x)
yf = y - floor(y)

topRight = Vector2(xf - 1.0, yf - 1.0)
topLeft = Vector2(xf, yf - 1.0)
bottomRight = Vector2(xf - 1.0, yf)
bottomLeft = Vector2(xf, yf)

dotTopRight = dot(topRight, gradientTopRight)
dotTopLeft = dot(topLeft, gradientTopLeft)
dotBottomRight = dot(bottomRight, gradientBottomRight)
dotBottomLeft = dot(bottomLeft, gradientBottomLeft)

În implementarea originală a algoritmului, Ken Perlin a folosit un tabel de permutări cu 256 de intrări pentru a determina ce vector gradient este asociat fiecărei intersecții a gridului. Astfel, vectorii gradient ajung să se repete în funcție de dimensiunea texturii pe care o generăm. Tot în implementarea originală, Ken Perlin a folosit următorul tabel de permutări, însă în practică se poate folosi orice alt tabel de permutări.

int permutation[] = { 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, 120, 234,  75,   0,  26, 197,  62,  94, 252, 219, 203, 117,  35,  11,  32,
                       57, 177,  33,  88, 237, 149,  56,  87, 174,  20, 125, 136, 171, 168,  68, 175,
                       74, 165,  71, 134, 139,  48,  27, 166,  77, 146, 158, 231,  83, 111, 229, 122,
                       60, 211, 133, 230, 220, 105,  92,  41,  55,  46, 245,  40, 244, 102, 143,  54,
                       65,  25,  63, 161,   1, 216,  80,  73, 209,  76, 132, 187, 208,  89,  18, 169,
                      200, 196, 135, 130, 116, 188, 159,  86, 164, 100, 109, 198, 173, 186,   3,  64,
                       52, 217, 226, 250, 124, 123,   5, 202,  38, 147, 118, 126, 255,  82,  85, 212,
                      207, 206,  59, 227,  47,  16,  58,  17, 182, 189,  28,  42, 223, 183, 170, 213,
                      119, 248, 152,   2,  44, 154, 163,  70, 221, 153, 101, 155, 167,  43, 172,   9,
                      129,  22,  39, 253,  19,  98, 108, 110,  79, 113, 224, 232, 178, 185, 112, 104,
                      218, 246,  97, 228, 251,  34, 242, 193, 238, 210, 144,  12, 191, 179, 162, 241,
                       81,  51, 145, 235, 249,  14, 239, 107,  49, 192, 214,  31, 181, 199, 106, 157,
                      184,  84, 204, 176, 115, 121,  50,  45, 127,   4, 150, 254, 138, 236, 205,  93,
                      222, 114,  67,  29,  24,  72, 243, 141, 128, 195,  78,  66, 215,  61, 156, 180 };

Odată generat tabelul de permutări, este nevoie de o valoare din acest tabel pentru fiecare dintre colțuri. Există însă o restricție: un colț trebuie să primească întotdeauna aceeași valoare, indiferent care dintre cele 4 celule de grid care îl pot avea drept colț conține valoarea de input. De exemplu, dacă colțul din dreapta sus al celulei gridului (0, 0) are o valoare de 42, atunci colțul din stânga sus al celulei gridului (1, 0) trebuie să aibă, de asemenea, aceeași valoare de 42. Este același punct al gridului, deci aceeași valoare indiferent de celula gridului.

X = floor(x) & 255
Y = floor(y) & 255
    
valueTopRight = permutations[permutations[X+1]+Y+1];
valueTopLeft = permutations[permutations[X]+Y+1];
valueBottomRight = permutations[permutations[X+1]+Y];
valueBottomLeft = permutations[permutations[X]+Y];

Odată asignată colțului valoarea din tabelul de permutări, trebuie calculat vectorul gradient constant al colțului. Pentru a simplifica lucrurile, putem folosi următoarea funcție:

def getConstantVector(v):
    h = v % 4

    if h == 0:
        return Vector2(1.0, 1.0)
    elif h == 1:
        return Vector2(-1.0, 1.0)
    elif h == 2:
        return Vector2(-1.0, -1.0)
    elif h == 3:
        return Vector2(1.0, -1.0)

Odată calculate cele 4 produse scalare pentru cele 4 colțuri, acestea se vor interpola pentru a obține valoarea finală. Însă, la un moment dat se pot interpola doar 2 valori. Astfel, se pot interpola mai întâi valorile din stânga celului și cele din dreapta, iar apoi se va realiza o interpolare între cele 2 rezultate sau se pot interpola mai întâi valori din partea de jos a celulei și cele din partea de sus, iar apoi se vor interpola cele 2 valori rezultate.

def lerp(t, a1, a2):
    return a1 + t * (a2 - a1)

Dacă am folosi interpolarea liniară, aceasta nu ar da rezultate excelente, deoarece s-ar simți nenatural, trecerea de la o celulă la alta ar fi foarte bruscă:

În schimb, ne-am dori un rezultat de genul:

Pentru a obține de acest rezultat, Ken Perlin s-a folosit de funcția fade pentru a uniformiza valorile de input ale interpolării:

def fade(t):
    return ((6 * t - 15) * t + 10) * t * t * t

Tasks

  1. Implementați algoritmul Perlin Noise. Salvați rezultatul într-o imagine PNG.
  2. Într-un proiect Unity, generați un teren cu denivelări, folosind Perlin Noise.
  3. Bonus 1. Adăugați încă o octavă în implementarea algoritmului.
  4. Bonus 2. Implementați efectul de plasmă specific Perlin Noise. Exemplu:

În Unity C# există funcția float Mathf.PerlinNoise(float x, float y)

Resurse

gp/laboratoare/02.txt · Last modified: 2025/03/14 15:53 by maria_anca.balutoiu
CC Attribution-Share Alike 3.0 Unported
www.chimeric.de Valid CSS Driven by DokuWiki do yourself a favour and use a real browser - get firefox!! Recent changes RSS feed Valid XHTML 1.0