În cadrul acestei teme veți implementa o scenă virtuală ce face referire la faimoasele baloane cu aer cald cappadociene (puteți vizualiza aici o imagine de referință) - se vor studia astfel conceptele de iluminare și texturare. Elementele vizuale principale ce compun scena vor fi un teren texturat și modificat în înălțime printr-un shader, baloanele cu aer cald, precum și un soare la asfințit.
Puteți studia în următorul videoclip o posibilă implementare a cerințelor.
Scena va fi compusă dintr-o serie de modele 3D, anume:
Camera va fi de tip perspectivă și trebuie plasată astfel încât să includă în cadru toate elementele de interes din scenă (terenul, baloanele și soarele). Nu este necesar să permiteți controlul poziției sau rotației camerei în timpul execuției aplicației, aceasta putând rămâne fixă.
Baloanele din scenă sunt construite folosind o serie de primitive. Varianta recomandată, care este utilizată și în demo, are următoarele componente:
Pentru a obține un rezultat apropiat de aspectul real al corpului unui balon cu aer cald, este necesar să aplicăm o deformare simplă de alungire unei sfere. Procesul presupune deplasamentul programatic, în Vertex Shader, al vârfurilor din jumătatea inferioară a sferei pe verticală. O reprezentare vizuală a rezultatului pe care îl veți obține este evidențiată mai jos:
Veți face această deformare raportându-vă la coordonatele obiect ale sferei. O simplă verificare a modelului disponibil în framework (sphere.obj) vă va arăta că acesta este definit în origine și are rază 1. Vârfurile care sunt mutate au coordonata y intre [0, -0.5] în coordonate obiect. Observați că referința realizează un deplasament diferit în funcție de această valoare 😊.
Fiecare element constitutiv al unui balon se va textura. Pentru nacelă și conectoare puteți să folosiți aceeași textură pentru toate baloanele. Corpul trebuie să fie colorat prin alegere aleatorie dintr-un set de texturi. Este obligatoriu ca acesta să fie texturat diferit față de restul componentelor și ca alegerea texturii să se facă dintr-un set de minim 5. Un exemplu este următorul:
Baloanele din scenă trebuie să se rotească pe traiectorii concentrice. Traiectoriile sunt sub formă de cercuri paralele cu planul \(XOZ\) și cu centrul comun. Pentru acest lucru trebuie să alegeți un punct din planul \(XOZ\), \(\mathbf{C}\) (de exemplu \((0, 0)\)), pentru a desemna centrul cercurilor.
Pentru fiecare balon trebuie să se aleagă (de preferat aleator) o înălțime \(\mathbf{H}\) la care să se rotească (astfel centrul traiectoriei va fi \((C.x, H, C.y)\)), o rază \(\mathbf{R}\) a cercului care descrie traiectoria și o viteză de rotație \(\omega\). Rezultatul va fi o serie de traiectorii concentrice paralele cu \(XOZ\) și paralele între ele, la înălțimi diferite față de pământ.
În plus, pentru un efect mai realistic, fiecare balon va avea o oscilație pe axa \(OY\) de forma \(\Delta y = A \cdot \sin(\omega \cdot \Delta t)\) (unde \(A\) și \(\omega\) se vor alege astfel încât efectul vizual să pară realistic).
Terenul este inițial plat pentru ca mai apoi înălțimea vârfurilor sale să fie modificată pe GPU, cum este explicat în secțiunea de mai jos. Pentru ca această modificare să fie posibilă, este necesar ca acesta să fie subdivizat.
Veți realiza deformarea în Vertex Shader luând informația de înălțime din textură, la care veți aplica un factor de intensificare constant în funcție de rezultatul dorit.
// GLSL code const float Y_OFFSET = 0.5; // Get vertex height from the height map new_position.y = Y_OFFSET * texture(texture_1, v_texture_coord).r;
Rezultatul pe care îl veți obține este următorul:
Terenul, reprezentat de planul subdivizat, se va textura folosind coordonatele sale de textură. Pentru a obține un efect interesant vă veți folosi de două texturi de culoare, împreună cu textura de adâncime. Fiecare fragment se colorează în funcție de înălțime. Pentru o trecere uniformă este necesară interpolarea liniară în zonele de înălțime medie. Ideea este evidențiată de diagrama de mai jos:
Funcția care indică gradul de interpolare trebuie să normalizeze intervalul de mijloc \([ \text{LOW_LIMIT}, \text{HIGH_LIMIT} ]\) în \([0, 1]\). Aceasta poate să arate așa:
\[ f(\text{height}) = \frac{\text{height} - \text{LOW_LIMIT}}{\text{HIGH_LIMIT} - \text{LOW_LIMIT}} \]
Rezultatul pe care trebuie să îl obțineți este următorul:
Deformarea aplicată în vertex shader se aplică asupra poziției fiecărui vertex, urmărind metodolgia prezentată mai sus. Totuși, amintiți-vă de formulele de la calculele de iluminare - acestea se bazează pe vectorul normală la suprafață (\(\vec{N}\)), care a rămas neschimbat în urma deplasării vertecșilor, ceea ce conduce la un rezultat eronat al iluminării.
În imaginea următoare este desenat terenul deformat și o lumină punctiformă - observați diferențele între imagini, in partea stângă normalele planului sunt toate orientate către \(\text{(0,1,0)}\), iar imaginea din dreapta ilustrează rezultatul după recalcularea normalelor.
La nivel conceptual, ne vom folosi de metoda diferențelor finite pentru a aproxima aceste normale. Pentru coordonatele de texturare ale fiecărui vertex vom eșantiona harta de înălțimi, unde ne vom folosi de texelii vecini pentru a determina aceste normale.
Pentru a calcula \(\text{texelSize}\), presupunem că harta de înălțimi este o imagine pătratică, iar dimensiunea sa (rezoluția) este \(\text{dim}\). Texelul reprezintă o unitate discretă a texturii, iar dimensiunea sa relativă în coordonate UV se poate calcula astfel:
\[ \text{texelSize} = \frac{1}{\text{dim}} \]
Se eșantionează textura suport (harta de înălțimi) pentru a determina “înălțimea” texelilor (în esență, se identifică valoarea de luminozitate a texelului, pe care o notăm cu (\(h\))). Se folosesc coordonatele de textură (\(v_{\text{texCoord}}\)) pentru a extrage valoarea înălțimii corespunzătoare (\(h\)), dar și valorile de înălțime ale vecinilor acestuia, de-a lungul axelor X și Z.
\[ h = texture(\text{heightMap}, v_{\text{texCoord}}).r \\ h_{\text{right}} = texture(\text{heightMap}, v_{\text{texCoord}} + \vec{2}(\text{texelSize}, 0)).r \\ h_{\text{up}} = texture(\text{heightMap}, v_{\text{texCoord}} + \vec{2}(0, \text{texelSize})).r \]
Următorul pas este calcularea gradientelor pe direcțiile X și Z. Factorul de scalare pe verticală \((y_{\text{offset}})\) reprezintă valoarea utilizată anterior pentru ajustarea înălțimii terenului pe baza hărții de înălțimi:
\[ \Delta x = (h_{\text{right}} - h)\,y_{\text{offset}} \\ \Delta z = (h_{\text{up}} - h)\,y_{\text{offset}} \]
Pe baza acestor variații, construim doi vectori, tangenta și bitangenta în spațiul obiectului:
\[ \vec{T}_{OS} = \vec{3}(\text{texelSize}, \Delta x, 0) \\ \vec{B}_{OS} = \vec{3}(0, \Delta z, \text{texelSize}) \]
Aplicăm matricea de modelare pentru a obține vectorii în spațiul lumii:
\[ \vec{T}_{WS} = (Model \cdot \vec{4}(\vec{T}_{OS}, 0))_{xyz} \\ \vec{B}_{WS} = (Model \cdot \vec{4}(\vec{B}_{OS}, 0))_{xyz} \]
Prin trecerea de la \((\vec{T}_{OS}, \vec{B}_{OS})\) la \((\vec{T}_{WS}, \vec{B}_{WS})\), garantăm că direcțiile rezultate țin cont de toate transformările de modelare aplicate terenului, asigurându-ne astfel că normala finală \((\vec{N})\), obținută prin produsul vectorial dintre \(\vec{B}_{WS}\) și \(\vec{T}_{WS}\), reflectă corect forma terenului în spațiul lumii.
\[ \vec{N} = \text{normalize}(\text{cross}(\vec{B}_{WS}, \vec{T}_{WS})) \]
Imaginea următoare oferă suport vizual pentru vectori și rezultatul produsului vectorial.
În final, animația de mai jos ilustrează cum sunt normalele recalculate în funcție de înălțimea terenului, precum și rezultatul obținut în urma adăugării unei lumini direcționale.
Iluminarea scenei se va realiza folosind cel puțin 2 tipuri de surse de lumină: punctiformă și direcțională. Fiecare sursă de lumină (indiferent de tipul acesteia) o să aibă o culoare specifică și trebuie să se țină cont de această culoare în calculele de iluminare.
Lumina punctiformă: Aceasta este reprezentată de sursele care emit lumina cu aceeași intensitate in toate direcțiile. Acest tip de lumină trebuie implementat pentru fiecare balon cu aer cald: Lumina trebuiă să ramână la o poziție fixă relativ la balon și să aibă o culoare și intensitate luminoasă aleatorie.
În următoarea imagine au fost activate numai luminile punctiforme:
Lumina direcțională: aceasta va ilumina toate obiectele din scenă cu aceeași intensitate. Specific luminii de tip direcțional este faptul că vectorul luminii incidente $L$ nu depinde de poziția luminii sau a fragmentului care trebuie iluminat (precum în cazul luminilor de tip point și spot). Așadar, pentru fiecare fragment, iluminarea va fi calculată folosind același vector $L$ (corespunzător direcției luminii). Astfel, pentru o sursă de lumină de tip direcțional este nevoie să se definească direcția acesteia și culoarea luminii emise. În cadrul acestei teme vom considera soarele ca `sursa` acestei lumini direcționale, așadar, va trebui să setați direcția acestei lumini în mod corespunzător.
În următoarea imagine a fost activată numai lumina direcțională:
Pentru întrebări vom folosi forumurile de pe Moodle. Orice nu este menționat în temă este la latitudinea fiecărui student!
Baremul este orientativ. Fiecare asistent are o anumită libertate în evaluarea temelor (de exemplu, să dea punctaj parțial pentru implementarea incompletă a unei funcționalități sau să scadă pentru hard coding). Același lucru este valabil atât pentru funcționalitățile obligatorii, cât și pentru bonusuri.
Tema va fi implementată în OpenGL și C++. Este indicat să folosiți framework-ul și Visual Studio.