This shows you the differences between two versions of the page.
gp:laboratoare:02 [2025/03/11 16:04] maria_anca.balutoiu [Implementare] |
gp:laboratoare:02 [2025/03/14 15:53] (current) maria_anca.balutoiu [Implementare] |
||
---|---|---|---|
Line 40: | Line 40: | ||
</code> | </code> | ||
- | Î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 înn 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. | + | Î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. |
<code> | <code> | ||
Line 61: | Line 61: | ||
</code> | </code> | ||
- | <hidden> | + | 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. |
- | În general, în implementările de zgomot Perlin, zgomotul se va „încheia” după fiecare multiplu de 256 (să numim acest număr w), adică se va repeta. Asta pentru că, pentru a oferi fiecărui punct al grilei un vector constant, în curând vom avea nevoie de ceva numit tabel de permutare. Este o matrice de dimensiune w care conține toate numerele întregi între 0 și w-1, dar amestecate (adică o permutare). Indicele pentru această matrice (valoarea dintre parantezele pătrate [ ]) este X sau Y (sau o valoare în apropierea acestora), deci trebuie să fie mai mic de 256. Zgomotul „se înfășoară” deoarece dacă, de exemplu, intrarea x este 256, X va fi egal cu 0. Acest 0 va fi folosit pentru a indexa tabelul de permutare și apoi pentru a genera un vector aleatoriu. Deoarece X este 0 la fiecare multiplu de 256, vectorul aleatoriu va fi același în toate acele puncte, așa că zgomotul se repetă. Puteți, dacă doriți, să aveți un tabel de permutare mai mare (să zicem, de dimensiunea 512) și, în acest caz, zgomotul s-ar încheia la fiecare multiplu de 512. | + | <code> |
+ | 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]; | ||
+ | </code> | ||
- | În continuare, avem nevoie de o valoare din acel 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 grilă care îl are ca colț conține valoarea de intrare. De exemplu, dacă colțul din dreapta sus al celulei grilei (0, 0) are o valoare de 42, atunci colțul din stânga sus al celulei grilei (1, 0) trebuie să aibă, de asemenea, aceeași valoare de 42. Este același punct al grilei, deci aceeași valoare indiferent de celula grilăi este calculată: | + | 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: |
- | Modul în care am selectat valorile pentru colțuri din codul de mai sus respectă această restricție. Dacă ne aflăm în celula grilă (0, 0), „valueBottomRight” va fi egal cu P[P[0+1]+0] = P[P[1]+0]. În timp ce în celula grilei (1, 0), „valueBottomLeft” va fi egal cu P[P[1]+0]. „valueBottomRight” și „valueBottomLeft” sunt aceleași. Restrictia este respectata. | + | <code> |
+ | def getConstantVector(v): | ||
+ | h = v % 4 | ||
- | De asemenea, dorim să dublăm tabelul pentru ca zgomotul să se înfășoare la fiecare multiplu de 256. Dacă calculăm P[X+1] și X este 255 (deci X+1 este 256), am obține un depășire dacă nu am dubla matricea, deoarece indicele maxim al unei matrice de dimensiunea 256 este 255. Ceea ce este important este că trebuie să dublăm și apoi să nu dublăm matricea. În schimb, trebuie să o amestecăm și apoi să o dublem. În exemplul lui P[X+1] unde X este 255, dorim ca P[X+1] să aibă aceeași valoare ca P[0], astfel încât zgomotul să se poată încheia. | + | 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) | ||
+ | </code> | ||
- | Acum este momentul să obținem acești vectori constanți. Implementarea originală a lui Ken Perlin a folosit o funcție ciudată numită „grad” care calcula produsul punctual pentru fiecare colț în mod direct. Vom simplifica lucrurile prin crearea unei funcții care returnează doar vectorul constant având o anumită valoare din tabelul de permutare și vom calcula produsul punctual mai târziu. | + | 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. |
- | De asemenea, deoarece este mai ușor să le generezi, acei vectori constanți pot fi 1 din 4 vectori diferiți: (1.0, 1.0), (1.0, -1.0), (-1.0, -1.0) și (-1.0, 1.0). | + | <code> |
+ | def lerp(t, a1, a2): | ||
+ | return a1 + t * (a2 - a1) | ||
+ | </code> | ||
- | Deoarece v este între 0 și 255 și avem 4 vectori posibili, putem face a & 3 (echivalent cu % 4) pentru a obține 4 valori posibile ale lui h (0, 1, 2 și 3). În funcție de această valoare, returnăm unul dintre vectorii posibili. | + | 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ă: |
- | Acum că trebuie să punctăm un produs pentru fiecare colț, trebuie să le amestecăm cumva pentru a obține o singură valoare. Pentru aceasta, vom folosi interpolarea. Interpolarea este o modalitate de a afla ce valoare se află între alte 2 valori (să zicem, a1 și a2), având în vedere o altă valoare t între 0,0 și 1,0 (un procent, practic, unde 0,0 este 0% și 1,0 este 100%). De exemplu: dacă a1 este 10, a2 este 20 și t este 0,5 (deci 50%), valoarea interpolată ar fi 15 deoarece este la jumătatea distanței între 10 și 20 (50% sau 0,5). Un alt exemplu: a1=50, a2=100 și t=0,4. Atunci valoarea interpolată ar fi la 40% din distanță între 50 și 100, adică 70. Aceasta se numește interpolare liniară deoarece valorile interpolate sunt într-o curbă liniară. | + | {{ :gp:laboratoare:perlin_noise_liniar_interpolation.png?500 |}} |
- | Acum avem 4 valori pe care trebuie să le interpolăm, dar putem interpola doar 2 valori la un moment dat. Deci, modul în care folosim interpolarea pentru zgomotul Perlin este că interpolăm valorile din stânga sus și din stânga jos împreună pentru a obține o valoare pe care o vom numi v1. După aceea, facem același lucru pentru sus-dreapta și jos-dreapta pentru a obține v2. Apoi, în sfârșit, interpolăm între v1 și v2 pentru a obține o valoare finală. Aceasta este valoarea pe care dorim să o returneze funcția noastră de zgomot. | + | În schimb, ne-am dori un rezultat de genul: |
- | Rețineți că dacă schimbăm puțin punctul de intrare, vectorii dintre fiecare colț și punctul de intrare se vor schimba puțin și ei, în timp ce vectorul constant nu se va schimba deloc. Produsele punctiforme se vor schimba, de asemenea, doar puțin, la fel și valoarea finală va reveni de către funcția de zgomot. Chiar dacă intrarea schimbă pătratul grilei, cum ar fi de la (3.01, 2.01) la (2.99, 1.99), valorile finale vor fi totuși foarte apropiate, deoarece chiar dacă 2 (sau 3) dintre colțuri se schimbă, celelalte 2 (sau 1) nu s-ar face și deoarece cu ambele intrări suntem aproape de colț(e), interpolarea va face ca valoarea finală a colțului să fie într-adevăr aproape de acea valoare. Deoarece cu ambele intrări acel colț va avea aceeași valoare, rezultatele finale vor fi foarte apropiate. | + | {{ :gp:laboratoare:perlin_noise_nonlinear_interpolation.png?500 |}} |
- | Am putea folosi interpolarea liniară, dar aceasta nu ar da rezultate excelente, deoarece s-ar simți nenatural, ca în această imagine care arată interpolarea liniară 1 dimensională: | + | 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: |
- | După cum puteți vedea, schimbarea dintre ceea ce este inferior lui 1 și ceea ce este superior lui 1 este bruscă. Ceea ce ne dorim este ceva mai lin, ca acesta: | + | <code> |
- | + | def fade(t): | |
- | Cu interpolarea liniară, am folosi xf ca valoare de interpolare (t). În schimb, vom transforma xf și yf în u și v. O vom face astfel încât, având în vedere o valoare a t între 0,0 și 0,5 (exclus), valoarea transformată va fi ceva mai mică (dar plafonată la 0,0). De asemenea, având în vedere o valoare a t între 0,5 (exclus) și 1,0, valoarea transformată ar fi puțin mai mare (dar plafonată la 1,0). Pentru 0,5, valoarea transformată ar trebui să fie 0,5. Acest lucru va avea ca rezultat o tranziție curbată, ca în figurile 5 și 6. | + | return ((6 * t - 15) * t + 10) * t * t * t |
- | + | </code> | |
- | Pentru a face acest lucru, avem nevoie de ceva numit curbă de ușurință: este doar o curbă matematică care arată astfel: | + | |
- | Dacă te uiți cu atenție, poți vedea că pentru o intrare (xf sau yf, axa x) între 0,0 și 0,5, ieșirea (u sau v, axa y) este puțin mai aproape de 0,0. Și pentru o valoare între 0,5 și 1,0, ieșirea este puțin mai aproape de 1,0. Pentru x=0,5, y=0,5. Asta va face treaba perfect. | + | ==== Tasks ==== |
+ | - Implementați algoritmul Perlin Noise. Salvați rezultatul într-o imagine PNG. | ||
+ | - Într-un proiect Unity, generați un teren cu denivelări, folosind Perlin Noise. | ||
+ | - **Bonus 1.** Adăugați încă o octavă în implementarea algoritmului. | ||
+ | - **Bonus 2.** Implementați efectul de plasmă specific Perlin Noise. Exemplu: | ||
+ | <html> | ||
+ | <p style="text-align:center;margin:auto;"> | ||
+ | <iframe width="560" height="315" src="https://www.youtube.com/embed/9B89kwHvTN4?si=McVA2s62UZGbUNTF&start=242" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe> | ||
+ | </p> | ||
+ | </html> | ||
- | Curba de mai sus este funcția de ușurință folosită de Ken Perlin în implementarea lui Perlin Noise. Ecuația este 6t5-15t4+10t3. Aceasta se mai numește și funcție de estompare. În cod, arată așa:</hidden> | + | <note tip>În Unity C# există funcția **float Mathf.PerlinNoise(float x, float y)**</note> |
+ | ==== Resurse ==== | ||
+ | * [[https://www.youtube.com/watch?v=9B89kwHvTN4]] | ||
+ | * [[https://rtouti.github.io/graphics/perlin-noise-algorithm]] | ||
+ | * [[https://www.youtube.com/watch?v=vFvwyu_ZKfU]] |