Sunteți pe pagina 1din 66

Laboratorul 08

Modelarea reflexiei luminii


Va reamintim formulele pentru calculul culorii intr-un punct al unei suprafețe:

culoarea = ce + ca + cd + cs

Emisiva: ce = Ke

Ambientala: ca = Ia ⋅ Ka

Difuza: ⃗  ⃗ 
cd = Kd ⋅ Isursă ⋅ max(N ⋅ L, 0)

Speculara: ⃗  ⃗ 
cs = Ks ⋅ Isursă ⋅ lum ⋅ (max(N ⋅ H , 0)
n
, unde ⃗  ⃗ 
lum = (N ⋅ L > 0)?1 : 0

Dacă introducem mai multe lumini în scenă și ținem cont și de factorul de atenuare, atunci culoarea
intr-un punct al unei suprafețe este:

→ →
⃗  ⃗  n
culoarea = Ke + Ia ⋅ Ka + ∑ fati ⋅ Isursă (Kd ⋅ max(N ⋅ Li , 0) + Ks ⋅ lumi ⋅ (max(N ⋅ H i , 0) )
i

La laboratorul de saptamana trecuta, pentru ușurința implementării, am considerat mai multe


simplificări:

am considerat că toate constantele de material Ke , Ka , Kd , Ks , sunt variable de tip float


(un singur canal)
deoarece constantele de material au fost considerate pe un singur canal, s-a introdus
variabila uniformă object_color, o variabilă de tip vec3 care a modelat culoarea obiectului
am considerat că intensitatea sursei de lumină este o constantă float (cu valoarea 1)
am ignorat culoarea emisă
am înlocuit constanta de material Ka cu constanta Kd (pentru a trimite mai putine
uniforme)
am considerat intensitatea luminii ambientale o constantă float (cu valoarea 0.25)

Totuși, trebuie să menționăm că modelul complet urmărește formula de mai sus, unde constantele
de material Ke , Ka , Kd , Ks sunt diferite și au 3 canale (R, G, B) , iar intensitatea luminii
ambientale și intensitatea sursei de lumină au de asemenea 3 canale. Expresia luminii se evaluează
separat pentru cele trei canale.

Iluminare Phong in Fragment Shader


Modeulul de iluminare aplicat in cazul implementarii in fragment shader este acelasi cu cel studiat in
Laboratorul 07. Totusi, exista o diferenta majora intre cele doua implementari prin faptul ca
iluminarea nu se mai aplica la nivelul fiecarul vertex ci la nivel de fragment. Rezultatul final este
superior calitativ intrucat iluminarea fiecarui fragment nu se va mai calcula pe baza interpolarii
luminii calculate la nivel de vertex ci pe baza normalei si pozitiei in spatiu a fiecarui fragment.

Valorile de intrare primite de Fragment Shader sunt interpolate linar intre valorile vertexilor ce
compun primitiva utilizata la desenare.
Imaginea de mai sus este obtinuta prin desenarea unui triunghi avand cele 3 varfuri de culori
diferite: rosu, verde, albastru

Prin transmiterea culorii de la Vertex Shader la Fragment Shader culoarea fiecarui fragment
de pe suprafata triunghiului este calculata ca o interpolare linara intre culorile vertexilor ce
compun primitiva specificata (in acest caz, un triunghi).

Acelasi procedeu se aplica pentru orice alta proprietate, cum ar fi:

pozitia in spatiul lume a unui fragment (daca trimitem pozitiile vertexilor)


normala in spatiul lume a unui fragment (daca trimitem normalele vertexilor)
orice alta valoarea transmisa de la vertex shader la fragment shader
etc

Modelul de interpolarea implicit utilizat (smooth) calculeaza interpolarea tinand cont si de


perspectiva (se face o interpolare perspectiva).
API-ul OpenGL permite specificarea modelului de interpolare prin utilizarea unor termeni specifici in
cadrul Fragment Shaderului:

flat - valoarea nu va fi interpolata


smooth - interpolare perspectiva (implicita)
noperspective - interpolare liniara in spatiu fereastra

Mai multe detalii despre modelele de interpolare se pot gasi accesind urmatoarele resurse:

OpenGL Type Qualifier


[https://www.opengl.org/wiki/Type_Qualifier_(GLSL)#Interpolation_qualifiers]
OpenGL Interpolation Qualifiers Tutorial [http://www.geeks3d.com/20130514/opengl-
interpolation-qualifiers-glsl-tutorial/]

Astfel, utilizand valorile interpolate de pozitie si normala (in spatiul lume) putem sa calculam
modeul de iluminare Phong pentru fiecare fragment al unei primitive rasterizate, rezultatul final
fiind mult superior intrucat prin interpolarea normalelor se obtine o trecere lina intre suprafete
adiacente (sunt interpolate normalele de pe muchii), deci si iluminarea finala va oferi impresia unei
suprafete netede. Astfel poligoanele componente ale obiectelor nu vor mai aparea vizibil in imagine.

Detalii de implementare

1. Se calculeaza world_position si world_normal in Vertex Shader ca in Laboratorul 07


2. Se transmit cele 2 valori catre Fragment Shader
3. Se aplica calculul luminii (componenta ambientala, difuza, speculara) in Fragment Shader

Pentru a primi valoarea unei variabile de tip uniform este suficient sa declarati respectiva variabila
in shaderul in care este necesara. Deci, NU trimiteti valoarea unei variabile de la Vertex
Shader la Fragment Shader

// Vertex Shader
uniform vec3 light_position;

// Fragment Shader
uniform vec3 light_position;

Iluminare Spot-light
Nu toate sursele de lumina sunt punctiforme. Daca dorim sa implementam iluminarea folosind o
sursa de lumina de tip spot trebuie sa tinem cont de o serie de constrangeri

Asa cum se poate vedea si in poza pentru a implementa o sursa de lumina de tip spot avem nevoie
de urmatorii parametri aditionali:

orientarea spotului (directia luminii)


unghiul de cut-off al spotului ce controleaza deschiderea conului de lumina
un model de atenuare unghiular al luminii ce tine cont valoarea de cut-off a spot-ului

Astfel, punctul P se afla in conul de lumina (primeste lumina) daca conditia urmatoare este
indepilita:

float cut_off = radians(30);


float spot_light = dot(-L, light_direction);
if (spot_light > cos(cut_off))
{
// fragmentul este iluminat de spot, deci se calculeaza valoarea luminii conform modelului Phong
// se calculeaza atenuarea luminii
}

Pentru a simula corect iluminarea de tip spot este nevoie sa tratam si atenuarea luminii
corespunzatoare apropierii unghiului de cut-off. Putem astfel sa utilizam un model de atenuare
patratica ce ofera un rezultat convingator.

float cut_off = radians(30);


float spot_light = dot(-L, light_direction);
float spot_light_limit = cos(cut_off);

// Quadratic attenuation
float linear_att = (spot_light - spot_light_limit) / (1 - spot_light_limit);
float light_att_factor = pow(linear_att, 2);

Cerinte laborator
tasta F5 - reincarca shaderele in timpul rularii aplicatiei.

In cazul in care ati modificat doar sursele shader nu este nevoie sa opriti aplicatia intrucat
shaderele sunt compilate si rulate de catre placa video si nu au legatura cu codul sursa C++ propriu
zis, iar framework-ul ofera suport pentru reincarcarea acestora la runtime.

1. Descarcati framework-ul de laborator [https://github.com/UPB-Graphics/Framework-


EGC/archive/master.zip]
2. Sa se implementeze iluminarea de tip Phong in Fragment Shader
3. Atunci cand se apasa tasta F sa se treaca in modul de iluminare Spot-light
Directia de ilumiare este transmisa ca uniform vec3 light_direction
Nu uitati sa aplicati un model de atenuare al luminii in functie de apropierea
fragmentelor de unghiul de cut-off

[Bonus]

Sa se modifice directia si unghiul de cut-off al luminii spotlight de la tastatura


logica in OnInputUpdate
rotirea spotului: sus, jos, stanga, dreapta
2 taste pentru a creste/micsora unghiul de iluminare al spot-ului

egc/laboratoare/08.txt · Last modified: 2020/11/24 15:18 by anca.morar


Laboratorul 09

Introducere
Am invatat in laboratoarele trecute ca, pentru a adauga detalii obiectului nostru, putem folosi culori asociate fiecarui
vertex. Pentru a putea crea obiecte detaliate, asta ar insemna sa avem foarte multi vertecsi astfel incat sa putem specifica
o gama cat mai variata de culori si ar ocupa foarte multa memorie. Pentru a rezolva aceasta problema se folosesc texturi.

Pentru scopul acestui laborator, o textura este o imagine 2D (exista si texturi 1D si 3D


[https://gamedev.stackexchange.com/a/9674]) care este folosita pentru a adauga detalii obiectului. Ganditi-va la textura ca la
o bucata de hartie (cu un desen pe ea) care este impaturita peste obiectul 3D. Pentru ca putem adauga oricate detalii
vrem intr-o singura imagine, putem da iluzia ca obiectul este foarte detaliat fara sa adaugam vertecsi in plus.

Diferenta intre un obiect texturat si acelasi obiect netexturat este remarcabil de mare din punct de vedere al acuratetei
reprezentarii obiectului respectiv:

Maparea texturilor
Pentru a mapa (impacheta) o textura peste un triunghi, trebuie sa specificam in ce parte din textura corespunde fiecare
vertex. Asadar, fiecare vertex ar trebui sa aiba asociata un set de coordonate de textura (2D adica glm::vec2) care
specifica partea din textura unde isi are locul. Coordonatele de textura se afla in intervalul [0, 1] pentru axele x si y (in
cazul 2D). Coordonatele texturii incep din punctul (0, 0) pentru coltul din stanga jos a imaginii pana la punctul (1, 1) care
se afla in coltul din dreapta sus.

Un punct de pe imagine si care este in spatiul [0, 1] × [0, 1] se numeste texel, numele venind de la texture element. Dupa
cum se poate vedea in imaginea de mai jos, in functie de coordonatele fiecarui vertex al triunghiului, partea din textura
care este mapata peste triunghi poate fi diferita:

Adaugarea unei texturi


Pentru a construi o textura in OpenGL avem nevoie in primul rand de pixelii imaginii ce va fi folosita ca textura. Pixelii
trebuie fie generati functional, fie incarcati dintr-o imagine, iar acest pas este independent de OpenGL. In laborator, pentru
a citi texturi, se poate folosi clasa Texture2D:

Texture2D::Load2D(const char* fileName, GLenum wrapping_mode)

unde wrapping_mode poate fi:

GL_REPEAT: textura se repeta pe toata suprafata obiectului


GL_MIRRORED_REPEAT: textura se repeta dar va fi vazuta in oglinda pentru repetarile impare
GL_CLAMP_TO_EDGE: coordonatele vor fi intre 0 si 1
GL_CLAMP_TO_BORDER: asemanator cu clamp to edge, doar ca ceea ce se afla dincolo de marginea imaginii nu mai
este texturat.

Pentru a seta manual modul de wrapping al texturii se pot folosi:

glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, wrapping_mode); // modul de wrapping pe orizontala


glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, wrapping_mode); // modul de wrapping pe verticala

Dupa ce avem pixelii imaginii incarcate putem genera un obiect de tip textura de OpenGL folosind comanda:

unsigned int gl_texture_object;

glGenTextures(1, &gl_texture_object);

Similar cu toate celelalte procese din OpenGL, nu lucram direct cu textura ci trebuie sa o asociem unui punct de legare.
Mai mult, la randul lor, punctele de legare pentru texturi sunt dependente de unitatile de texturare
[https://en.wikipedia.org/wiki/Texture_mapping_unit]. O unitate de texturare e foarte similara ca si concept cu pipe-urile pe
care trimitem atribute. Setam unitatea de texturare folosind comanda (o singura unitate de texturare poate fi activa):

glActiveTexture(GL_TEXTURE0 + nr_unitatii_de_texturare_dorite);

Iar pentru a lega obiectul de tip textura generat anterior la unitatea de textura activa folosim punctul de legare
GL_TEXTURE_2D:

glBindTexture(GL_TEXTURE_2D, gl_texture_object);

Pentru a incarca datele efective in textura folosim comanda:

glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, data);

Primul argument specifica tipul de textura. Daca punem GL_TEXTURE_2D, inseamna ca aceasta functie va asocia
obiectului de tip textura (trecut anterior prin bind) o textura 2D (deci daca avem legate un GL_TEXTURE_1D sau
GL_TEXTURE_3D, acestea nu vor fi afectate).
Al 2-lea argument specifica nivelul de mipmap [https://en.wikipedia.org/wiki/Mipmap] pentru care vrem sa cream
imaginea. Vom explica ce este un mipmap pe parcusul laboratorului. Pentru moment, putem sa lasam valoarea
aceasta 0.
Al 3-lea argument specifica formatul in care vrem sa fie stocata imaginea. In cazul nostru este RGB.
Al 4-lea si al 5-lea argument seteaza marimea imaginii.
Urmatorul argument ar trebui sa fie mereu 0 (legacy stuff)
Argumentele 7 si 8 specifica formatul si tipul de date al imaginii sursa.
Ultimul argument il reprezinta vectorul de date al imaginii.

Asadar, glTextImage2D incarca o imagine definita prin datele efective, adica un array de unsigned chars, pe obiectul
de tip textura legat la punctul de legare GL_TEXTURE_2D al unitatii de texturare active la momentul curent, nivelul 0 (o sa
luam aceasta constanta ca atare pentru moment), cu formatul intern GL_RGB cu lungimea width si cu inaltimea height, din
formatul GL_RGB. Datele citite sunt de tip GL_UNSIGNED_BYTE (adica unsigned char) si sunt citite de la adresa data.

Utilizarea texturii
Pentru a folosi o textura in shader trebuie urmat acest proces:

glActiveTexture(GL_TEXTURE0);

glBindTexture(GL_TEXTURE_2D, texture1->GetTextureID());

glUniform1i(glGetUniformLocation(shader->program, "texture_1"), 0);

glActiveTexture(GL_TEXTURE1);

glBindTexture(GL_TEXTURE_2D, texture2->GetTextureID());

glUniform1i(glGetUniformLocation(shader->program, "texture_2"), 1);

Unitatea de texturare este folositoare in momentul in care vrem sa atribuim textura unei variabile uniforme din shader.
Scopul acestui mecanism este de a ne permite sa folosim mai mult de 1 textura in shaderele noastre. Prin folosirea
unitatilor de texturare, putem face bind la multiple texturi, atat timp cat le setam ca fiind active.

OpenGl are minim 16 unitati de texturare care pot fi activate folosind GL_TEXTURE0 pana la GL_TEXTURE15. Nu este
nevoie sa se specifice manual numarul, devreme ce unitatea de texturare cu numarul X poate fi activata folosind
GL_TEXTURE0 + X.

Urmatorul cod este un exemplu de shader ce poate folosi legarea precedenta, unde texcoord reprezinta coordonatele de
texturare primite ca atribute in vertex shader si apoi pasate catre rasterizer pentru interpolare:

#version 330

uniform sampler2D texture_1;

in vec2 texcoord;

layout(location = 0) out vec4 out_color;

void main()
{
vec4 color = texture2D(texture_1, texcoord);
out_color = color;
}

Multitexturarea este folositare in momentul in care reprezentam o un obiect topologic complex, de exemplu frunze, cu o
textura ce are o topologie mult inferioara ca si complexitate (de exemplu un quad). Daca utilizam o asemenea reducere de
complexitate trebuie sa avem o metoda prin care sa putem elimina, la nivel de fragment, fragmentele ce nu sunt necesare
(evidentiate in imaginea urmatoare).
Puntru aceasta putem sa folosim o textura de opacitate (alpha) care ne spune care sunt fragmentele reale, vizibile, ale
obiectului. Combinatia de textura de opacitate si textura de culoare este suficienta pentru definirea acestui bambus:

Pentru a omite desenarea fragmentelor care nu sunt vizibile se foloseste directiva de shader discard.

if (alpha == 0) {
discard;
}

Filtrare

Coordonatele prin care se mapeaza vertecsii obiectului pe textura nu depind de rezolutia imaginii, ci sunt valori float in
intervalul [0, 1] , iar OpenGL trebuie sa-si dea seama ce texel (texture pixel) sa mapeze pentru coordonatele date. Pentru a
rezolva aceasta problema se foloste filtrarea, care este o metoda de esantionare si reconstructie a unui semnal.

Reconstructia reprezinta procesul prin care, utilizand acesti pixeli, putem obtine valori pentru oricare din pozitiile din
textura (adica nu neaparat exact la coordonatele din mijlocul pixelului, acolo unde a fost esantionata realitatea in spatiul
post proiectie).
Pentru a face acest proces mai usor, OpenGL are o serie de filtre care pot fi folosite pentru a obtine maparea dorita, iar
cele mai des utilizate sunt: GL_NEAREST si GL_LINEAR.

GL_NEAREST (care se mai numeste si nearest neighbor filtering) este filtrarea default pentru OpenGL. Cand este folosit
acest filtru, OpenGL selecteaza pixelul al carui centru este cel mai aproape de coordonatele de texturare. Mai jos se pot
vedea 4 pixeli unde crucea reprezinta exact coordonatele de texturare. Texelul din stanga sus are centrul cel mai aproape
de coordonata texturii si astfel este ales:

GL_LINEAR (cunoscut drept filtrare biliniara) ia valoarea interpolata din texelii vecini ai coordonatei de texturare,
aproximand astfel culoarea mai bine. Cu cat distanta de la coordonata de texturare pana la centrul texelului este mai mica,
cu atat contributia culorii acelui texel este mai mare. Mai jos putem vedea cum pixelul intors

Se pot folosi filtre diferite pentru cazul in care vrem sa marim imaginea si cand vrem sa o micsoram (upscaling si
downscaling). Filtrul se specifica folosind metoda glTextPameter:

glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);

glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);

Mipmaps
Imaginati-va cazul in care avem o camera plina cu obiecte ce folosesc aceeasi textura dar se afla in pozitii diferite. Cele
care sunt mai indepartate vor aparea mai mici fata de cele care sunt mai apropiate, dar toate vor avea aceeasi textura de
rezolutie mare.

Deoarece obiectele care se afla la departare vor folosi probabil doar cateva fragmente din imaginea de baza, OpenGL va
intampina dificultati in obtinerea culorii din textura de rezolutie mare deoarece trebuie sa aleaga culoarea care sa poata
reprezenta o portiune foarte mare din textura. Comprimarea unei portiuni mari din textura intr-un singur frament poate
duce la artefacte visible pentru obiectele mici, pe langa memoria irosita prin folosirea unei texturi mari pentru obiecte mici.

Pentru a rezolva aceasta problema, se foloseste un concept numit mipmap, care este de fapt o colectie de copii ale
aceleiasi imagini, unde fiecare copie este de doua ori mai mica decat copia anterioara. Ideea din spatele conceptului de
mipmap este destul de simpla: dupa un anumit prag de distanta, OpenGL va folosi o textura mipmap mai mica pentru acel
obiect. Fiindca obiectul este la departare, faptul ca rezolutia este mai mica nu va fi observat de utilizator. O textura
mipmap arata in felul urmator:

Crearea de texturi mipmaps este destul de anevoioasa de facut manual, asa ca OpenGL poate face tot acest proces in mod
automat, folosind functia glGenerateMipmap(GL_TEXTURE_2D); dupa ce am creat textura.

Cand privim un obiect dintr-un anumit unghi, se poate ca OpenGL sa faca schimbarea intre diferite niveluri de texturi
mipmap, ceea ce poate duce la artefacte asa cum se vede in imaginea de mai jos :

Exact ca filtrarea normala, este posibil sa folosim filtrare intre diferite niveluri de mipmaps folosind filtrare NEAREST si
LINEAR atunci cand se produce schimbarea intre niveluri. Pentru a specifica acest tip de filtru, inlocuim filtrarea anterioara
cu urmatoarele 4 optiuni :

GL_NEAREST_MIPMAP_NEAREST : foloseste cea mai apropiata textura mipmap si foloseste interpolare nearest
neighbor pentru a alege culoarea.
GL_LINEAR_MIPMAP_NEAREST : foloseste cea mai apropiata textura mipmap si foloseste interpolare liniara pentru
a obtine culoarea.
GL_NEAREST_MIPMAP_LINEAR : interpoleaza liniar intre cele mai apropiate doua texturi mipmap si si foloseste
interpolare nearest neighbor pentru a obtine culoarea
GL_LINEAR_MIPMAP_LINEAR : interpoleaza liniar intre cele mai apropiate doua texturi mipmap si foloseste
interpolare liniara pentru a obtine culoarea.

Exemplu de folosire:
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);

glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);

O greseala comuna este folosirea filtrului de mipmap pentru marimea imaginii. Acest filtru nu va avea niciun efect
deoarece texturile mipmap sunt folosite in principal atunci cand obiectele devin mai mici. Marirea texturii nu foloseste
mipmap si daca dam un astfel de filtru, vom primi o eroare de tipul GL_INVALID_ENUM.

Mai multe informatii si detalii despre filtrare se pot gasi pe pagina API-ului glTexParameter
[https://www.opengl.org/sdk/docs/man4/html/glTexParameter.xhtml]

Cerinte laborator

1. Descarcati framework-ul de laborator [https://github.com/UPB-Graphics/Framework-EGC/archive/master.zip]


2. Completati functia RenderSimpleMesh astfel inca sa trimiteti corect textura catre Shader
3. Completati coordonatele de textura pentru patrat
4. Completati shaderele astfel incat sa foloseasca coordonatele de textura
5. Sa se completeze shaderul astfel incat sa se faca alpha discard
In cadrul laboratorului nu se va folosi o imagine diferita pentru alpha discard ci se va testa componenta alpha
din cadrul texturii
Ex: Faceti alpha discard daca valoarea alpha este mai mica de 0.5f
6. Creati si incarcati pe GPU o textura random
completati functia Laborator9::CreateRandomTexture
! generati mipmaps : glGenerateMipmap(GL_TEXTURE_2D);
textura va fi folosita in cadrul randarii pe cubul din stanga
GL_TEXTURE_MIN_FILTER si GL_TEXTURE_MAG_FILTER si observati diferentele de afisare
7. Modificati filterele
GL_TEXTURE_MAG_FILTER aveti doar 2 valori posibile
GL_LINEAR
GL_NEAREST
GL_TEXTURE_MIN_FILTER aveti doar 6 valori posibile
GL_NEAREST
GL_LINEAR
GL_NEAREST_MIPMAP_NEAREST
GL_LINEAR_MIPMAP_NEAREST
GL_NEAREST_MIPMAP_LINEAR
GL_LINEAR_MIPMAP_LINEAR
8. Randati un quad folosind 2 texturi. Folositi functia mix() [https://www.opengl.org/sdk/docs/man/html/mix.xhtml] in
fragment shader pentru a obtine o combinatie intre cele doua texturi.

vec3 color = mix(color1, color2, 0.5f); // ultimul parametru reprezinta factorul de interpolare intre cele 2 culori, avand valoare intre 0 si

Bonus:

1. Sa se trimita timpul aplicatiei Engine::GetElapsedTime() catre fragment shader si sa se utilizeze pentru a cicla
prin textura de pe globul pamantesc (pe coordonata OY) (doar pentru acel obiect, deci e nevoie de si de o variabila
uniform pentru a testa obiectul randat)
2. Sa se roteasca spre directia camerei (doar pe OY) quadul cu textura de iarba astfel incat sa fie orientat tot timpul
catre camera.

egc/laboratoare/09.txt · Last modified: 2020/11/26 08:39 by florica.moldoveanu


Resurse bonus

Redare text în OpenGL


OpenGL este o bibliotecă ce pune la dispoziție funcționalități de nivel scăzut și nu conține capabilități de nivel ridicat
pentru redarea unui text pe ecran. Pentru a realiza acest lucru, dezvoltatorul trebuie să implementeze un sistem de
redare a textului ce utilizează functionalitățile low-level ale OpenGL.

Cea mai des utilizată metoda este aceea ca fiecare caracter ce se dorește a fi redat să aibă asociată o textură. Astfel
redarea propriu-zisă a unui caracter pe ecran presupune desenarea unui poligon de tip quad ce va avea mapat
textura corespunzătoare caracterului ce se dorește a fi afișat.

O primă variantă ar fi ca toate caracterele să se regăsească în cadrul unei singure texturi mari denumită “bitmap
font” și la redare să se selecteze din bitmap font pentru mapare coordonatele specifice porțiunii unde se află
caracterul dorit.

Figura 1. Bitmap font ce conține toate caracterele și simbolurile

Figura 2. Redare text “Hello World” folosind quad-uri și caracterele extrase din bitmap font

Cea de-a doua variantă (și cea care este folosită și pentru implementare în continuarea laboratorului) este ca pentru
fiecare caracter/simbol să se creeze câte o textură individuală ce va avea dimensiunea (lătime/înălțime)
caracterului/simbolului. Această metodă permite o mai bună flexibilitate în manipularea fiecărui caracter/simbol în
parte pentru poziționarea/scalarea acestuia.

Pentru a obtine imaginea (bitmapul) pentru fiecare caracter/simbol necesar dintr-un font dat, a fost folosită
biblioteca FreeType.

Utilizare FreeType
FreeType este o bibliotecă open-source care permite încărcarea de fonturi și redarea acestora în bitmapuri inclusiv la
nivelul fiecărui caracter/simbol individual.

FreeType poate fi descarcat de la (FreeType [https://www.freetype.org/index.html]).

Detalii despre modul de lucru cu FreeType, crearea și gestiunea bitmapurilor create pot fi găsite la ( Tutoriale
FreeType [https://www.freetype.org/freetype2/docs/tutorial/index.html]).

În cazul nostru, pentru primele 128 de caractere ASCII ale font-ului folosit, folosind FreeType vom crea câte o
textură astfel:

Încărcare font

// Then initialize and load the FreeType library


FT_Library ft;
// All functions return a value different than 0 whenever an error occurred
if (FT_Init_FreeType(&ft))
std::cout << "ERROR::FREETYPE: Could not init FreeType Library" << std::endl;
// Load font as face
FT_Face face;
if (FT_New_Face(ft, font.c_str(), 0, &face))
std::cout << "ERROR::FREETYPE: Failed to load font" << std::endl;
// Set size to load glyphs as
FT_Set_Pixel_Sizes(face, 0, fontSize);

Creare texturi pentru fiecare caracter/simbol

// for the first 128 ASCII characters,


// pre-load/compile their characters and store them
for (GLubyte c = 0; c < 128; c++)
{
// Load character glyph
if (FT_Load_Char(face, c, FT_LOAD_RENDER))
{
std::cout << "ERROR::FREETYTPE: Failed to load Glyph" << std::endl;
continue;
}
// Generate texture
GLuint texture;
glGenTextures(1, &texture);
glBindTexture(GL_TEXTURE_2D, texture);
glTexImage2D(
GL_TEXTURE_2D,
0,
GL_RED,
face->glyph->bitmap.width,
face->glyph->bitmap.rows,
0,
GL_RED,
GL_UNSIGNED_BYTE,
face->glyph->bitmap.buffer
);
// Set texture options
...

// Store character information (texture id, size, etc) for later use
...
}

Astfel pentru fiecare dintre primele 128 de caractere ASCII din font se crează câte o textură. Se folosește pentru
textura creată doar primul canal de culoare (GL_RED) unde se va memora valoarea de culoare din bitmap-ul generat
pentru fiecare caracter ca o imagine greyscale pe 8 biți.

Shadere
Pentru redarea fiecărui caracter se folosesc următoarele shadere:

Vertex shader

layout(location = 0) in vec4 vertex; // <x,y,u,v>


out vec2 TexCoords;

uniform mat4 projection;

void main()
{
gl_Position = projection * vec4(vertex.xy, 0.0, 1.0);
TexCoords = vertex.zw;
}

Pe coordonatele .xy se transmit coordonatele x,y din cadrul ferestrei de afișare care reprezintă poziția unde va fi
afișat caracterul.

Coordonata z va fi zero deoarece afișarea se va face direct pe fața cubului corespunzător volumului vizual canonic în
planul Z = 0.

Astfel acestea vor fi transformate in vertex shader doar cu matricea de proiecție.

Se va folosi o proiecție ortografică ce va avea near = far = 0 și pentru a considera colțul stânga sus ca fiind 0,0 va
avea bottom inversat cu top astfel:

glm::value_ptr(glm::ortho(0.0f, static_cast<GLfloat>(width), static_cast<GLfloat>(height), 0.0f))


unde width și height sunt rezoluția ferestrei de afișare

Pe coordonatele .zw vor veni coordonatele de textură care vor fi trimise mai departe în TexCoords

Fragment shader

in vec2 TexCoords;
out vec4 color;

uniform sampler2D text;


uniform vec3 textColor;

void main()
{
vec4 sampled = vec4(1.0, 1.0, 1.0, texture(text, TexCoords).r);
color = vec4(textColor, 1.0) * sampled;
}

Fragment shader-ul primește ca uniforme id-ul texturii unde a fost încărcat bitmap-ul monocolor al caracterului și
culoarea cu care se dorește a fi afișat textul. Astfel, deoarece am folosit doar canalul roșu pentru textură (GL_RED)
se face eșantionarea din textură doar de pe acesta (texture(text, TexCoords).r) și se stochează în culoarea finală pe
canalul de alpha (componenta a 4-a). În acest fel, considerând quad-ul pe care se afisează ca fiind transparent (in
bitmap acolo unde nu există pixeli pentru caracter, va fi intors 0 iar un alpha de 0 înseamnă perfect transparent)
textul va fi afișat iar restul conținutului quad-ului pe care a fost mapată textură va fi ignorat permițând astfel
combinarea naturală cu ce deja a fost afișat anterior în frame buffer. Ca ultim pas, se înmulțește culoarea obținută
cu culoarea textColor pentru a desena textul cu o culoare dorită.

Pentru a putea fi folosit acest mecanism de transparență trebuie activat și folosit mecanismul de blending din
OpenGL astfel:

glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);

Redarea textului
Pentru redarea unei linii de text se va folosi functia

void RenderText(std::string text, GLfloat x, GLfloat y, GLfloat scale, glm::vec3 color)

unde:

x,y: coordonatele (x,y) din fereastra de afișare de unde va fi afișată linia de text ( 0,0 corespunde colțului
stânga sus al ecranului)
scale: factor de scalare pentru text (dimensiunea font-ului ales la încărcare va fi înmulțită cu acest factor)
color: culoarea textului

Întreaga funcție:

void TextRenderer::RenderText(std::string text, GLfloat x, GLfloat y, GLfloat scale, glm::vec3 color)


{
// Activate corresponding render state
//this->TextShader.Use();
if (this->TextShader)
{
glUseProgram(this->TextShader->program);
CheckOpenGLError();
}

int loc_text_color = glGetUniformLocation(this->TextShader->program, "textColor");


glUniform3f(loc_text_color, color.r, color.g, color.b);
//this->TextShader.SetVector3f("textColor", color);
glActiveTexture(GL_TEXTURE0);
glBindVertexArray(this->VAO);

// Iterate through all characters


std::string::const_iterator c;
for (c = text.begin(); c != text.end(); c++)
{
Character ch = Characters[*c];
GLfloat xpos = x + ch.Bearing.x * scale;
GLfloat ypos = y + (this->Characters['H'].Bearing.y - ch.Bearing.y) * scale;

GLfloat w = ch.Size.x * scale;


GLfloat h = ch.Size.y * scale;
// Update VBO for each character
GLfloat vertices[6][4] = {
{ xpos, ypos + h, 0.0, 1.0 },
{ xpos + w, ypos, 1.0, 0.0 },
{ xpos, ypos, 0.0, 0.0 },

{ xpos, ypos + h, 0.0, 1.0 },


{ xpos + w, ypos + h, 1.0, 1.0 },
{ xpos + w, ypos, 1.0, 0.0 }
};
// Render glyph texture over quad
glBindTexture(GL_TEXTURE_2D, ch.TextureID);
// Update content of VBO memory
glBindBuffer(GL_ARRAY_BUFFER, this->VBO);
glBufferSubData(GL_ARRAY_BUFFER, 0, sizeof(vertices), vertices); // Be sure to use glBufferSubData and not glBufferData

glBindBuffer(GL_ARRAY_BUFFER, 0);
// Render quad
glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);
glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
glDrawArrays(GL_TRIANGLES, 0, 6);
glDisable(GL_BLEND);
// Now advance cursors for next glyph
x += (ch.Advance >> 6) * scale; // Bitshift by 6 to get value in pixels (1/64th times 2^6 = 64)
}
glBindVertexArray(0);
glBindTexture(GL_TEXTURE_2D, 0);
}

Astfel se procesează fiecare caracter din care este alcătuit quad-ul după cum urmează:

Pentru fiecare caracter se avansează pe ecran pe linia orizontală (variabila x) la poziția de început de unde
urmează a fi desenat (xpos, ypos)
Se crează quad-ul de dimensiunea lățimea și înălțimea texturii asociate caracterului
Quad-ul va fi desenat folosind două triunghiuri: 6 vertecși (câte 3 pentru fiecare triunghi) fiecare cu 4 valori:
x,y: coodonatele vârfului (după cum s-a explicat mai sus z va fi zero deoarece se desenează folosind o
proiecție ortografică în planul Z = 0 al volumului vizual canonic)
z,w: coordonatele de textură asociate vârfului
Se desenează quad-ul format din cele două triunghiuri cu textura corespunzătoare caracterului
(glBindTexture(GL_TEXTURE_2D, ch.TextureID)) și cu mecanismul de blending activat pentru a nu desena
decât pixelii caracterului și a ignora restul quad-ului care este transparent

Utilizare
1. Descărcați framework-ul de laborator [https://github.com/UPB-Graphics/Framework-EGC/archive/withrendertext.zip]
ce are implementat redarea textului
2. Rulați exemplul de redare a textului: clasa Laborator_BonusTextRenderer
3. Dacă doriți să vedeți în detaliu implementarea redării textului examinați:
Clasa TextRenderer din /TextRenderer
Shaderele VertexShaderText.glsl și FragmentShaderText.glsl din /TextRenderer/Shaders
egc/laboratoare/bonusrendertext.txt · Last modified: 2019/12/03 07:56 by alexandru.gradinaru
Laboratorul 01
Video Laborator 1: https://www.youtube.com/watch?v=YlzFkjJjh4Q
[https://www.youtube.com/watch?v=YlzFkjJjh4Q].
Autor: Anca Morar [mailto:anca.morar@cs.pub.ro]

Introducere
Grafica pe calculator este un subiect amplu utilizat într-un număr din ce în ce mai mare de
domenii. În acest laborator se vor prezenta conceptele ce stau la baza graficii cât și a utilizării
procesorului grafic pentru acest scop. Domeniul graficii computerizate necesită cunoștințe
variate: matematică, fizică, algoritmică, grafică digitală 2D & 3D, user experience design, etc.

Framework laborator
Întrucât scrierea unei aplicații simple OpenGL nu se poate realiza foarte ușor într-un timp
scurt, dar și pentru a putea prezenta mai simplu conceptele de bază ale graficii computerizate
moderne, în cadrul laboratoarelor se va lucra pe un framework ce oferă o serie de
funcționalități gata implementate. Framework-ul utilizat oferă toate funcționalitățile de bază ale
unui motor grafic minimal, precum:

Fereastra de desenare având la bază un context OpenGL 3.3+ (o să aflați ce înseamnă)


Suport pentru încărcarea de modele 3D (cunoscute și ca 3D meshes)
Suport pentru încărcarea de imagini pentru texturarea modelelor 3D
Suport pentru definirea și încărcarea de shadere OpenGL

De asemenea, pe langă funcționalitățile de bază, framework-ul implementează un model


generic pentru scrierea de aplicații OpenGL. Astfel, sunt oferite următoarele aspecte:

Control pentru fereastra de afișare


Management pentru input de la tastatură și mouse
Cameră de vizualizare cu input predefinit pentru a ușura deplasarea și vizualizarea
scenei
Model arhitectural al unei aplicații simple OpenGL, bazat pe toate aspectele prezentate

Funcționalitatea framework-ului este oferită prin intermediul mai multor biblioteci (libraries):

GLFW
Site oficial http://www.glfw.org [http://www.glfw.org] Github
https://github.com/glfw/glfw [https://github.com/glfw/glfw]
Oferă suportul de bază pentru API-ul OpenGL precum context, fereastră, input,
etc
GLEW
Site oficial http://glew.sourceforge.net [http://glew.sourceforge.net] Github
https://github.com/nigels-com/glew [https://github.com/nigels-com/glew]
Asigură suportul pentru extensiile de OpenGL suportate de procesorul grafic
GLM
Site oficial http://glm.g-truc.net [http://glm.g-truc.net] Github
https://github.com/g-truc/glm [https://github.com/g-truc/glm]
Funcționalități matematice bazate pe specificațiile limbajului GLSL (shadere
OpenGL)
Asigură interoperabilitate simplă cu API-ul OpenGL
ASSIMP
Site oficial http://www.assimp.org [http://www.assimp.org] Github
https://github.com/assimp/assimp [https://github.com/assimp/assimp]
Open Asset Import Library
Oferă suport pentru încărcarea de modele și scene 3D
Suportă majoritatea formatelor de stocare 3D utilizate în industrie
STB
Github https://github.com/nothings/stb [https://github.com/nothings/stb]
Oferă suport pentru încărcare/decodare de imagini JPG, PNG, TGA, BMP, PSD,
etc.
EGC-Components – closed source
Oferă o serie de funcționalități ce vor fi utilizate începând din primul laborator dar
care vor fi implementate și de către studenți pe parcursul laboratoarelor

Structura framework-ului
/libs
Bibliotecile utilizate în cadrul framework-ului
/Visual Studio
Proiect Visual Studio 2017 preconfigurat
/Resources
Resurse necesare rulării proiecutului
/Textures
Diverse imagini ce pot fi încărcate și utilizate ca texturi
/Shaders
Exemple de programe shader - Vertex Shader și Fragment Shader
/Models
Modele 3D ce pot fi încărcate în cadrul framework-ului
/Source
Surse C++
/include
O serie de headere predefinite pentru facilitarea accesului la biblioteci
gl.h
Adaugă suportul pentru API-ul OpenGL
glm.h
Adaugă majoritatea headerelor glm ce vor fi utilizate
Printare ușoara pentru glm::vec2, glm::vec3, glm::vec4
prin intermediul operatorului C++ supraîncărcat: operator«
math.h
Simple definiții preprocesor pentru MIN, MAX, conversie radiani ⇔
grade
utils.h
Simple definiții preprocesor pentru lucrul cu memoria și pe biți
/Components
Diverse implementări ce facilitează lucrul în cadrul laboratoarelor
SimpleScene.cpp
Model de bază al unei scene 3D utilizată ca bază a tuturor
laboratoarelor
CameraInput.cpp
Implementare a unui model simplu de control FPS al camerei de
vizualizare oferite de biblioteca EGC-Components
/Core
API-ul de bază al framwork-ului EGC
/GPU
GPUBuffers.cpp
Asigură suportul pentru definirea de buffere de date și
încărcarea de date (modele 3D) pe GPU
Mesh.cpp
Loader de modele 3D atât din fișier cât și din memorie
Shader.cpp
Loader de programe Shader pentru procesorul grafic
Texture2D.cpp
Loader de texturi 2D pe GPU
/Managers
ResourcePath.h
Locații predefinite pentru utilizarea la încărcarea resurselor
TextureManager.cpp
Asigură încărcare și management pentru texturile Texture2D
Încarcă o serie de texturi simple predefinite
/Window
WindowCallbacks.cpp
Asigură implementarea funcțiilor de callback necesare de
GLFW pentru un context OpenGL oarecare
Evenimentele GLFW sunt redirecționare către fereastra
definită de Engine
WindowObject.cpp
Oferă implementarea de fereastră de lucru, suport
predefinite definire pentru callbacks, dar și un model de
buffering pentru evenimente de input tastatură și mouse
InputController.cpp
Prin moștenire oferă suport pentru implementarea callback-
urilor de input/tastatură. Odată instanțiat, obiectul se va
atașa automat pe fereastra de lucru (pe care o obține de la
Engine) și va primi automat evenimentele de input pe care
le va executa conform implementării
În cadrul unui program pot exista oricâte astfel de obiecte.
Toate vor fi apelate în ordinea atașării lor, dar și a producerii
evenimentelor
Engine.cpp
Asigură inițializarea contextului OpenGL și a ferestrei de
lucru
World.cpp
Asigură implementarea modelului de funcționare al unei
aplicații OpenGL pe baza API-ului oferit de Framework
/Laboratoare
Implementările pentru fiecare laborator EGC
Fiecare laborator va pleca de la baza oferită de SimpleScene

Funcționarea unei aplicații grafice (OpenGL)


Orice aplicație trebuie să asigure funcționalitatea pe o anumită perioadă de timp. În funcție de
cerințe această perioadă poate fi :

deterministă - programul va executa un anumit task iar apoi se va închide


(majoritatea programelor create in cadrul facultății până în acest moment respectă
acest model)
continuă - rulează unul sau mai multe task-uri în mod continuu (până in momentul în
care utilizatorul sau un eveniment extern închide aplicația).

Aplicațiile grafice cu suport de vizualizare în timp real (de exemplu jocurile, sau de exemplu ce
vom face noi la EGC) se regăsesc în cel de-al doilea model și au la bază funcționarea pe baza
unei bucle (loop) de procesare.
În cadrul framework-ului EGC acest loop constă într-o serie de pași (vezi
World::LoopUpdate()):
1. Se interoghează evenimentele ferestrei OpenGL (input, resize, etc.)
Evenimentele sunt salvate pentru a fi procesate mai tarziu
2. Se estimează timpul de execuție pentru iterația actuală (timpul de execuție al iterației
precedente)
3. Se procesează evenimentele salvate anterior
4. Se procesează frame-ul actual (este indicat să se ia în considerare timpul de execuție în
cadrul modificărilor pentru a oferi actualizare independentă de timp)
5. Opțional: În cazul double sau triple buffering se interschimbă bufferele de imagine
6. Se trece la următorul frame (se revine la primul pas)
Cel mai simplu model de aplicație OpenGL va trata evenimentele de input (mouse, tastatură) la
momentul producerii lor. Acest model nu este indicat deoarece are numeroase dezavantaje:

nu oferă posibilitatea de a trata combinații de taste (Exemplu: utilizatorul apasa W și A


pentru a deplasa caracterul in diagonală)
nu oferă informații ce țin de starea continuă a unui eveniment
Exemplu: Un personaj dintr-un joc trebuie să se deplaseze în față atât timp cât
utilizatorul ține apasată tasta W.
Pentru a trata corespunzător o astfel de logică este necesar să menținem
starea tastei W iar atunci când se face deplasarea personajului, aceasta să
fie direct proporțională cu timpul trecut de la ultimul frame procesat
Același lucru se aplică și în cazul butoanelor de la mouse

De asemenea, un model bazat pe buffering al evenimentelor de input oferă posibilitatea de a


interoga starea input-ului în orice moment al unui frame, deci ofera și o flexibilitate generală
mai mare pentru a implementa noi comportamente/logici. Clasa WindowObject asigură
suportul pentru buffering, dar și pentru procesarea ulterioară a evenimentelor prin intermediul
obiectelor de tipul InputController.

Recomandăm să citiți documentația GLFW despre tratarea evenimentelor de input pentru a


înțelege mai bine conceptele prezentate: http://www.glfw.org/docs/latest/input_guide.html
[http://www.glfw.org/docs/latest/input_guide.html]

Multi-buffering
În general, aplicațiile grafice folosesc mai multe buffere de imagini separate pentru a evita
apariția artefactelor grafice prin modificarea directă a imaginii randate pe ecran. Astfel,
imaginea afișată la momentul T a fost procesată la momentul T-1, sau T-2 (în funcție de
dimensiunea bufferului).
Informații adiționale despre această tehnică multi-buffering pot fi obțiunute de pe wiki:

https://en.wikipedia.org/wiki/Multiple_buffering
[https://en.wikipedia.org/wiki/Multiple_buffering]
https://en.wikipedia.org/wiki/Multiple_buffering#Double_buffering_in_computer_graphics
[https://en.wikipedia.org/wiki/Multiple_buffering#Double_buffering_in_computer_graphics]
https://en.wikipedia.org/wiki/Multiple_buffering#Triple_buffering
[https://en.wikipedia.org/wiki/Multiple_buffering#Triple_buffering]

Modelul de funcționare al aplicației de laborator


În cadrul unui laborator modelul aplicației grafice prezentat mai sus este implementat de către
clasa World.
Pasul 2 este tratat de către instanțele InputController în timp ce pasul 4 este asigurat de
funcțiile FrameStart(), Update(float deltaTime), și FrameEnd() moștenite de la clasa
World. Clasa World extinde deja InputController pentru a ușura munca în cadrul
laboratorului.
Toate laboratoarele EGC vor fi implementate pe baza SimpleScene ce oferă următoarele
facilități:
scena 3D cu randarea unui sistem cartezian de referință în coordonate OpenGL
plan orizontal XOZ
evidențierea spațiului pozitiv (OX, OY, OZ)
camera predefinită pentru explorarea scenei
shadere predefinite pentru lucrul în primele laboratoare
management pentru stocarea shaderelor și modelelor nou create, pe baza unui nume
unic

Etapele rulării aplicației

1. Se definesc proprietățile pentru fereastra de lucru (Main.cpp)


2. Se inițializează Engine-ul astfel - Engine::Init()
a. Se inițializează API-ul OpenGL (glfwInit())
b. Se creează fereastra de lucru cu un context OpenGL 3.3+
I. Se atașează evenimentele de fereastră prin intermediul
WindowsCallbacks.cpp
c. Se inițializează managerul de texturi
3. Se creează și inițializează o nouă scenă 3D de lucru având la bază modelul de update
prezentat anterior (Main.cpp)
4. Se pornește rularea scenei încărcate (LoopUpdate())

Standardul OpenGL
OpenGL este un standard (API) pe care îl putem folosi pentru a crea aplicații grafice real-time.
Este aproape identic cu Direct3D, ambele având o influență reciprocă de-a lungul anilor.

Mai multe informații despre istoricul OpenGL se pot găsi la adresa:


https://en.wikipedia.org/wiki/OpenGL [https://en.wikipedia.org/wiki/OpenGL]
Explicații complete prinvind API-ul OpenGL cât și utilizarea acestuia se pot găsi pe
pagina oficială a standardului: https://www.opengl.org/sdk/docs/man/
[https://www.opengl.org/sdk/docs/man/]

Atunci când nu sunteți siguri ce face o anumită comandă sau ce reprezintă parametrii funcțiilor
este recomandat să consultați documentația: https://www.opengl.org/sdk/docs/man/
[https://www.opengl.org/sdk/docs/man/]

Versiunea curentă a acestui standard este 4.6. Pentru cursul de EGC vom folosi standardul
3.0/3.3, care este în același timp și versiunea actuală pentru varianta pentru mobile a OpenGL,
numită OpenGL ES https://en.wikipedia.org/wiki/OpenGL_ES
[https://en.wikipedia.org/wiki/OpenGL_ES].

Începând cu 2016 a fost lansat și API-ul Vulkan ce oferă access avansat low-level la
capababilitățile grafice moderne ale procesoarelor grafice. Standardul Vulkan este orientat
dezvoltării aplicațiilor de înaltă performanță iar complexitatea acestuia depășește cu mult
aspectele de bază ce vor fi prezentate în cadrul cusului/laboratorului.

Utilizarea API
Pe parcursul laboratoarelor (dar și a cursului) se va trece prin toate etapele importante ce stau
la baza redării grafice. Astfel vor fi învățate concepte precum:

încărcare și randare de obiecte 3D simple


funcționarea pipeline-ului grafic
vizualizare, proiecție, control camera
utilizare shadere (vertex și fragment shader)
iluminare
texturare

Cerințe generale de laborator

Citiți cu foarte mare atenție Framwork-ul de laborator întrucât îl veți utiliza pe tot
parcursul laboratorului de EGC inclusiv și la temele de casă
Citiți comentariile din cod – ar trebui să răspundă la majoritatea întrebărilor pe care le
aveți
Citiți documentația de la __InputController.h__ [https://github.com/UPB-
Graphics/Framework-EGC/blob/master/Source/Core/Window/InputController.h] întrucât veți
utiliza constant funcțiile din cadrul acestei clase (prin suprascriere) pentru definirea de
interacțiuni și comportament personalizat
Dacă nu ințelegeți modelul de funcționare al aplicației rugați asistentul să explice încă o
dată cum funcționează toată aplicația

C++

Framework-ul este scris în limbajul C++, ce va fi utilizat pe tot parcursul laboratoarelor.


Conceptele utilizate în cadrul laboratorului și care trebuie știute sunt:

concepte de bază de OOP - obiecte, moștenire, metode virtuale, etc


utilizarea bibliotecilor standard: în special std::vector
[http://www.cplusplus.com/reference/vector/vector/], std::list
[http://www.cplusplus.com/reference/list/list/] și std::unorderd_map
[http://www.cplusplus.com/reference/unordered_map/unordered_map/]

Pentru cei mai puțin familiarizați cu limbajul C++ recomandăm să parcurgeți tutoriale: Learn
C++ [http://www.learncpp.com/]

Visual Studio 2019

În cadrul laboratorului vom utiliza Visual Studio 2019 Community Edition


[https://www.visualstudio.com/vs/community/]
Installer-ul de Visual Studio vine cu posibilitatea de a instala modular doar ceea ce este
necesar. Pentru acest laborator trebuie instalat doar modulul default Desktop
development with C++, care se regăsește în Workloads
Framework-ul conține deja un proiect preconfigurat pentru Visual Studio
Framework_EGC.sln (folderul /Visual Studio [https://github.com/UPB-Graphics/Framework-
EGC/tree/master/Visual%20Studio])
Deschideți soluția în Visual Studio

Cei care nu au mai utilizat IDE-ul Visual Studio pentru scrierea de aplicații C++ sunt rugați să
citească toturialul Getting Started with C++ in Visual Studio [https://msdn.microsoft.com/en-
us/library/jj620919.aspx#BKMK_CreateApp]

GLM
În grafică, matematica este folosită peste tot, de la simple matrici pentru rotații până la
integrale infinit dimensionale pentru algoritmii folosiți în industria filmului, de aceea ne dorim
să avem un suport de matematică robust, bine documentat și nu în ultimul rând cât mai
apropiat de formatul OpenGL. În loc să scriem noi o bibliotecă de matematică vom folosi
biblioteca GLM. GLM ne oferă rotații, translații, vectori de dimensiune 2/3/4, matrici și multe
alte funcționalități avansate (de ex. modele de zgomot). Vom folosi doar cele mai simple
funcționalități în laboratoarele de la această materie.

glm::mat4 identity = glm::mat4 (1, 0, 0, 0,


0, 1, 0, 0,
0, 0, 1, 0,
0, 0, 0, 1);
glm::mat4 identity2 = glm::mat4(1); // short form for writing identity matrices
glm::vec3 culoare = glm::vec3(1, 0.5, 1);
glm::vec2 directie = glm::vec3(-1, 1);
glm::vec3 pozitie = glm::vec3(100, 10, -20);
pozitie.x = 2; // you can select components like so: .x .y .z .t .r .g. b. a

Laboratorul 1

Framework

Framework-ul de laborator se găsește pe Github [https://github.com/UPB-Graphics/Framework-EGC]


Puteți să descărcați direct arhiva accesând acest link [https://github.com/UPB-Graphics/Framework-
EGC/archive/master.zip]

Informații laborator

Sursele ce stau la baza fiecărui laborator se află în directorul:


/Source/Laboratoare/LaboratorN/, N reprezentând numărul laboratorului.

În cadrul laboratorului 1 puteți încărca modele 3D în cadrul scenei și cere afișarea scenei
utilizând funcția

RenderMesh(Mesh * mesh, glm::vec3 position, glm::vec3 scale)

Culorile pixelilor prin care se reprezintă scena sunt salvate într-un buffer, numit Framebuffer.
Contextul definit oferă automat un astfel de buffer și este configurat să ruleze cu double-
buffering

API-ul OpenGL utilizat în cadrul laboratorului:


// defineste un spatiu de desenare in spatiul ferestrei de afisare a aplicatiei
// x, y reprezinta coordonatele coltului stanga jos
// width, height reprezinta dimensiunea spatiului de desenare.
void glViewport(GLint x, GLint y, GLsizei width, GLsizei height);

// seteaza culoarea cu care va fi colorat tot ecranul la operatia de clear


void glClearColor(float r, float g, float b, float a);

// implementeaza operatia de clear


void glClear(GL_COLOR_BUFFER_BIT);

Culorile în OpenGL sunt specificate ca float în intervalul 0 - 1

Bufferul de culoare utilizat (atât în cadrul laboratorului dar și în mod uzual datorită limitărilor
impuse de afișarea pe monitoare) este în format RGBA8. Fiecare componentă (red, green,
blue, alpha) este memorată pe 8 biți, deci are o valoare in intervalul 0 – 255. Astfel:

roșu (255, 0, 0) este reprezentată ca (1, 0, 0) pe procesorul grafic


galben este (1, 1, 0) și tot așa

Control aplicație

Programul rulat oferă posibilitatea vizualizării scenei create prin intermediul unei camere
predefinite.

Taste de control pentru cameră

W, A, S, D, Q, E - deplasare față, stânga, spate, dreapta, jos, sus


MOUSE RIGHT + MOUSE MOVE - rotație cameră

Cerințe laborator

1. Descărcați framework-ul, compilați și rulați proiectul


Trebuie să deschideți proiectul Framework_EGC.sln (folderul /Visual Studio) în
Visual Studio 2019
2. Încărcați un alt model 3D și randați-l în scenă la o poziție diferită față de cele 2 cuburi
/Resources/Models conține o serie de modele 3D ce pot fi încărcate
ÎnLaborator1::Init() găsiți modul în care puteți să declarați (și încărcați) un
nou obiect de tip Mesh
3. La apăsarea unei taste să se schimbe culoarea de ștergere a ecranului
4. La apăsarea unei taste să se schimbe obiectul afisat (render) la o poziție (să cicleze prin
3 obiecte, de ex cube, teapot, sphere)
5. Să se miște prin spațiu un obiect oarecare la apăsarea tastelor W, A, S, D, E, Q (pozitiv
și negativ pe toate cele 3 axe)

Citiți cu atenție documentația evenimentelor de input din fișierul InputController.h


[https://github.com/UPB-Graphics/Framework-
EGC/blob/master/Source/Core/Window/InputController.h] întrucât le veți utiliza în cadrul
fiecărui laborator
egc/laboratoare/01.txt · Last modified: 2020/10/12 12:52 by gabriel.ivanica
Laboratorul 02
Video Laborator 2: https://youtu.be/RtXuIQO8l0U [https://youtu.be/RtXuIQO8l0U].
Autor: Alex Gradinaru [mailto:alex.gradinaru@cs.pub.ro]

OpenGL – Date
Dacă am încerca să reducem întregul API de OpenGL la mari concepte acestea ar fi:

date
stări
shadere

Shaderele vor fi introduse pe parcursul cursului.


Stările reprezintă un concept mai larg, OpenGL fiind de fapt un mare automat finit cu o
mulțime de stări și posibilități de a seta aceste stări. De-a lungul laboratoarelor o parte din
aceste stări vor fi folosite pentru a obține efectele dorite.
Datele conțin informațiile ce definesc scena, precum:

obiecte tridimensionale
proprietăți de material ale obiectelor (plastic, sticlă, etc)
pozițiile, orientările și dimensiunile obiectelor în scenă
orice alte informații necesare ce descriu proprietăți de obiecte sau de scenă

De exemplu pentru o scenă cu un singur pătrat avem următoarele date:

vârfurile pătratului - 4 vectori tridimensionali ce definesc poziția fiecărui vârf în spațiu


caracteristicile vârfurilor
dacă singura caracteristică a unui vârf în afară de poziție ar fi culoarea am avea
încă 4 vectori tridimensionali (RGB)
topologia pătratului, adică modul în care legăm aceste vârfuri

OpenGL este un API de grafică tridimensională, adică, toate obiectele care pot fi definite sunt
raportate la un sistem de coordonate carteziene tridimensional. Cu toate acestea putem utiliza
API-ul pentru a afișa obiecte bi-dimensionale chiar dacă acestea sunt definite prin coordonate
(x,y,z) prin plasarea tuturor datelor într-un singur plan și utilizarea unei proiecții
corespunzătoare.
În cadrul laboratorului vom utiliza coordonata Z = 0. Astfel orice punct tridimensional va
deveni P(x,y,0)

Topologie
Primitiva de bază în OpenGL este triunghiul. Astfel, așa cum se poate observa și în imaginea
de sus, pentru a desena un obiect acesta trebuie specificat prin triunghiuri.

Cubul descris mai sus este specificat prin lista celor 8 coordonate de vârfuri și o listă de 12
triunghiuri care descrie modul în care trebuie unite vârfurile specificate în lista precedentă
pentru a forma fețele cubului. Folosind vârfuri și indici putem descrie în mod discret orice
obiect tridimensional.

Mai jos regăsiți principalele primitive acceptate de standardul OpenGL 3.3+.


După cum se poate observa, există mai multe metode prin care geometria poate fi specificată:

GL_LINES și GL_TRIANGLES sunt cele mai des utilizate primitive pentru definirea
geometriei
GL_POINTS este des utilizat pentru a crea sistemele de particule
Celelalte modele reprezintă doar niște optimizari ale celor 3 primitive de bază, atât din
perspectiva memoriei dar și a ușurinței în a specifica anumite topologii însă utilitatea lor
este deseori limitată întrucât obiectele mai complexe nu pot fi specificate decât prin
utilizarea primitivelor simple

În cadrul framework-ului puteți seta tipul de primitivă utilizat de către un obiect la randare prin
intermediul funcției Mesh::SetDrawMode(GLenum primitive) [https://github.com/UPB-
Graphics/Framework-EGC/blob/master/Source/Core/GPU/Mesh.h#L99] unde primitive poate fi
oricare dintre primitivele menționate în imaginea de mai sus.

Ordinea specificării vârfurilor


O observație importantă legată de topologie este ordinea vârfurilor într-o primitivă solidă (nu
linie, nu punct) cu mai mult de 2 vârfuri. Această ordine poate fi în sensul acelor de ceas sau
în sens invers.
Face Culling
API-ul OpenGL oferă posibilitatea de a testa orientarea aparentă pe ecran a fiecărui triunghi
înainte ca acesta să fie redat și să îl ignore în funcție de starea de discard setată: GL_FRONT
sau GL_BACK. Acestă funcționalitate poartă numele de Face Culling
[https://www.opengl.org/wiki/Face_Culling] și este foarte importantă deoarece reduce
costul de procesare total.

Modul cum este considerată o față ca fiind GL_FRONT sau GL_BACK poate fi schimbat
folosind comanda glFrontFace [https://www.khronos.org/registry/OpenGL-
Refpages/gl4/html/glFrontFace.xhtml] (valoarea inițială pentru o față GL_FRONT este considerată
ca având ordinea specificării vârfurilor în sens trigonometric / counter clockwise):

// mode can be GL_CW (clockwise) or GL_CCW (counterclockwise)


// the initial value is GL_CCW
void glFrontFace(GLenum mode);

Exemplu: pentru un cub maxim 3 fețe pot fi vizibile la un moment dat din cele 6 existente. În
acest caz maxim 6 triunghiuri vor fi procesate pentru afișarea pe ecran în loc de 12.

În mod normal face-culling este dezactivat. Acesta poate fi activat folosind comanda glEnable
[https://www.opengl.org/sdk/docs/man4/html/glEnable.xhtml]:

glEnable(GL_CULL_FACE);

Pentru a dezactiva face-culling se folosește comanda glDisable


[https://www.opengl.org/sdk/docs/man4/html/glEnable.xhtml]:
glDisable(GL_CULL_FACE);

Pentru a specifica ce orientare a fețelor să fie ignorată se folosește comanda glCullFace


[https://www.opengl.org/wiki/GLAPI/glCullFace]

// GL_FRONT, GL_BACK, and GL_FRONT_AND_BACK are accepted.


// The initial value is GL_BACK.
glCullFace(GL_BACK);

Meshe
Un „mesh” este un obiect tridimensional definit prin vârfuri și indici. În laborator aveți
posibilitatea să încărcați meshe în aproape orice format posibil prin intermediul clasei Mesh
[https://github.com/UPB-Graphics/Framework-EGC/blob/master/Source/Core/GPU/Mesh.h#L67].

Vertex Buffer Object (VBO)


Un vertex buffer object reprezintă un container în care stocăm date ce țin de conținutul
vârfurilor precum:

poziție
normală
culoare
coordonate de texturare
etc…

Un vertex buffer object se poate crea prin comanda OpenGL glGenBuffers


[https://www.opengl.org/sdk/docs/man/html/glGenBuffers.xhtml]:

GLuint VBO_ID; // ID-ul (nume sau referinta) buffer-ului ce va fi cerut de la GPU


glGenBuffers(1, &VBO_ID); // se genereaza ID-ul (numele) bufferului

Așa cum se poate vedea și din explicația API-ului, funcția glGenBuffers


[https://www.opengl.org/sdk/docs/man/html/glGenBuffers.xhtml] primește numărul de buffere ce
trebuie generate cât și locația din memorie unde vor fi salvate referințele (ID-urile) generate.
În exemplul de mai sus este generat doar 1 singur buffer iar ID-ul este salvat în variabila
VBO_ID.
Pentru a distruge un VBO și astfel să eliberăm memoria de pe GPU se folosește comanda
glDeleteBuffers [https://www.opengl.org/sdk/docs/man4/html/glDeleteBuffers.xhtml]:

glDeleteBuffers(1, &VBO_ID);

Pentru a putea pune date într-un buffer trebuie întâi să legăm acest buffer la un „target”.
Pentru un vertex buffer acest „binding point” se numește GL_ARRAY_BUFFER și se poate
specifica prin comanda glBindBuffer
[https://www.khronos.org/opengles/sdk/1.1/docs/man/glBindBuffer.xml]:

glBindBuffer(GL_ARRAY_BUFFER, VBO_ID);
În acest moment putem să facem upload de date din memoria CPU către GPU prin
intermediul comenzii glBufferData
[https://www.opengl.org/sdk/docs/man4/html/glBufferData.xhtml]:

glBufferData(GL_ARRAY_BUFFER, sizeof(vertices[0]) * vertices.size(), &vertices[0], GL_STATIC_DRAW);

Comanda citește de la adresa specificată, în exemplul de sus fiind adresa primului vârf
&vertices[0], și copiază în memoria video dimensiunea specificată prin parametrul al
2-lea.
GL_STATIC_DRAW reprezintă un hint pentru driver-ul video în ceea ce privește
metoda de utilizare a bufferului. Acest simbol poate avea mai multe valori dar în cadrul
laboratorului este de ajuns specificarea prezentată. Mai multe informații găsiți pe
pagina de manual a funcției glBufferData
[https://www.opengl.org/sdk/docs/man4/html/glBufferData.xhtml]

Pentru a înțelege mai bine API-ul OpenGL vă rocomandăm să citiți documentația indicată
pentru fiecare comandă prezentată. Atunci când se prezintă o nouă comandă, dacă apăsați
click pe numele acesteia veți fi redirecționați către pagina de manual a comenzii respective.
De asemenea, documentația oficială și completă a API-ului OpenGL poate fi gasită pe pagina
OpenGL 4 Reference Pages [https://www.opengl.org/sdk/docs/man/]

Index Buffer Object (IBO)


Un index buffer object (numit și element buffer object) reprezintă un container în care stocăm
indicii vertecșilor. Cum VBO si IBO sunt buffere, ele sunt extrem de similare în construcție,
încărcare de date și ștergere.

glGenBuffers(1, &IBO_ID);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, IBO_ID);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices[0]) * indices.size(), &indices[0], GL_STATIC_DRAW);

La fel ca la VBO, creăm un IBO și apoi îl legăm la un punct de legatură, doar că de data
aceasta punctul de legatură este GL_ELEMENT_ARRAY_BUFFER. Datele sunt trimise către
bufferul mapat la acest punct de legatură. În cazul indicilor toți vor fi de dimensiunea unui
singur întreg.

Vertex Array Object (VAO)


Într-un vertex array object putem stoca toată informația legată de starea geometriei
desenate. Putem folosi un număr mare de buffere pentru a stoca fiecare din diferitele atribute
(„separate buffers”). Putem stoca mai multe (sau toate) atribute într-un singur buffer
(„interleaved” buffers). În mod normal înainte de fiecare comandă de desenare trebuie
specificate toate comenzile de „binding” pentru buffere sau atribute ce descriu datele ce
doresc a fi randate. Pentru a simplifica această operație se folosește un vertex array object
care ține minte toate aceste legături.

Un vertex array object este creat folosind comanda glGenVertexArrays


[https://www.opengl.org/sdk/docs/man4/html/glGenVertexArrays.xhtml]:
unsigned int VAO;
glGenVertexArrays(1, &VAO);

Este legat cu glBindVertexArray


[https://www.opengl.org/sdk/docs/man4/html/glBindVertexArray.xhtml]:

glBindVertexArray(VAO);

Înainte de a crea VBO-urile și IBO-ul necesar pentru un obiect se va lega VAO-ul obiectului și
acesta va ține minte automat toate legăturile specificate ulterior.

După ce toate legăturile au fost specificate este recomandat să se dea comanda


glBindVertexArray(0) pentru a dezactiva legătura către VAO-ul curent, deoarece altfel
riscăm ca alte comenzi OpenGL ulterioare să fie legate la același VAO și astfel să introducem
foarte ușor erori în program.

Înainte de comanda de desenare este suficient să legăm doar VAO-ul ca OpenGL să știe toate
legatările create la construcția obiectului.

Laborator 2

Descriere laborator
În cadrul laboratorului vom învăța să folosim VAO, VBO, IBO și astfel să generăm și încărcăm
geometrie simplă.
Laboratorul pune la dispoziție structura VertexFormat [https://github.com/UPB-
Graphics/Framework-EGC/blob/master/Source/Core/GPU/Mesh.h#L14] ce va fi utilizată ca bază
pentru a crea geometria.

struct VertexFormat
{
// position of the vertex
glm::vec3 position;

// vertex normal
glm::vec3 normal;

// vertex texture coordinate


glm::uvec2 text_coord;

// vertex color
glm::vec3 color;
};

Clasa Mesh pune la dispoziție posibilitatea de a încărca geometrie simplă folosind diverse
metode:

// Initializes the mesh object using a VAO GPU buffer that contains the specified number of indices
bool InitFromBuffer(unsigned int VAO, unsigned short nrIndices);

// Initializes the mesh object and upload data to GPU using the provided data buffers
bool InitFromData(std::vector<VertexFormat> vertices,
std::vector<unsigned short>& indices);
// Initializes the mesh object and upload data to GPU using the provided data buffers
bool InitFromData(std::vector<glm::vec3>& positions,
std::vector<glm::vec3>& normals,
std::vector<unsigned short>& indices);

Taste de control pentru cameră

W, A, S, D, Q, E - deplasare față, stânga, spate, dreapta, jos, sus


MOUSE RIGHT + MOUSE MOVE - rotație cameră

F3 - afișează/ascunde gridul din scenă


Space - desenează primitivele doar prin puncte sau linii (wireframe) sau geometrie opacă

Cerințe laborator
Toate cerințele ce țin de încărcare de geometrie trebuie rezolvate prin intermediul funcției
Laborator2::CreateMesh dar puteți folosi metodele Mesh::InitFromData() pentru a
verifica validitatea geometriei.

1. Descărcați framework-ul de laborator [https://github.com/UPB-Graphics/Framework-


EGC/archive/master.zip]
2. Completați geometria și topologia unui cub: vectorii de vertecși și indecși din
inițializare. VertexFormat este o structură pentru vertex cu 2 parametrii (poziție,
culoare).
3. Completați funcția Laborator2::CreateMesh astfel încât să încărcați geometria pe
GPU
creați un VAO
creați un VBO și adăugați date în el
creați un IBO și adăugați date în el
afișați noul obiect (RenderMesh[cube3]) astfel încât să nu se suprapună cu un alt
obiect
4. Creați o nouă formă geometrică simplă, de exemplu un tetraedru și desenați-l în scenă
5. Atunci când se apasă tasta F2 faceți toggle între modul de culling GL_BACK și
GL_FRONT
nu uitați să activați și să dezactivați face culling folosind glEnable() /
glDisable()
6. Creați un pătrat format din 2 triunghiuri astfel încât fiecare triunghi să fie vizibil doar
dintr-o parte
în orice moment de timp nu trebuie să se vadă decât 1 triunghi

egc/laboratoare/02.txt · Last modified: 2020/10/21 15:43 by victor.asavei


Laboratorul 03
Video Laborator 3: https://youtu.be/G-rR8QFZVuI [https://youtu.be/G-rR8QFZVuI]
Autor: Stefania Cristea [mailto:stefania.cristea1708@gmail.com]

Transformări 2D
Obiectele 2D sunt definite într-un sistem de coordonate carteziene 2D, de exemplu, XOY, XOZ sau
YOZ. În cadrul acestui laborator vom implementa diferite tipuri de transformări ce pot fi aplicate
obiectelor definite în planul XOY: translații, rotații și scalări. Acestea sunt definite în format
matriceal, în coordonate omgene, așa cum ați învățat deja la curs. Matricile acestor transformări
sunt următoarele:

Translația

x 1 0 tx x
⎡ ⎤ ⎡ ⎤⎡ ⎤

⎢y ⎥ = ⎢0 1 ty ⎥ ⎢ y ⎥

⎣ ⎦ ⎣ ⎦⎣ ⎦
1 0 0 1 1

Rotația

Rotația față de origine



x cos(u) −sin(u) 0 x
⎡ ⎤ ⎡ ⎤⎡ ⎤

⎢y ⎥ = ⎢ sin(u) cos(u) 0⎥⎢ y ⎥
⎣ ⎦ ⎣ ⎦⎣ ⎦
1 0 0 1 1

Rotația față de un punct oarecare


Rotația relativă la un punct oarecare se rezolvă în cel mai simplu mod prin:

1. translatarea atât a punctului asupra căruia se aplică rotația cât și a punctului în jurul căruia
se face rotația a.î. cel din urmă să fie originea sistemului de coordonate.
2. rotația normală (în jurul originii),
3. translatarea rezultatului a.î. punctul în jurul căruia s-a făcut rotația să ajungă în poziția sa
inițială

Scalarea

Scalarea față de origine



x sx 0 0 x
⎡ ⎤ ⎡ ⎤⎡ ⎤

⎢y ⎥ = ⎢ 0 sy 0⎥⎢ y ⎥

⎣ ⎦ ⎣ ⎦⎣ ⎦
1 0 0 1 1

Dacă sx = sy atunci avem scalare uniformă, altfel avem scalare neuniformă.

Scalarea față de un punct oarecare

Scalarea relativă la un punct oarecare se rezolvă similar cu rotația relativă la un punct oarecare.

Utilizarea bibliotecii GLM


În cadrul laboratorului folosim biblioteca GLM, care este o bibliotecă implementată cu matrici în
formă coloană, exact același format ca OpenGL. Forma coloană diferă de forma linie prin ordinea
de stocare a elementelor matricei în memorie, Matricea de translație arată în modul următor în
memorie:

glm::mat3 Translate(float tx, float ty)


{
return glm::mat3(
1, 0, 0, // coloana 1 in memorie
0, 1, 0, // coloana 2 in memorie
tx, ty, 1); // coloana 3 in memorie

Din această cauză, este convenabil ca matricile să fie scrise manual în forma aceasta:

glm::mat3 Translate(float tx, float ty)


{
return glm::transpose(
glm::mat3( 1, 0, tx,
0, 1, ty,
0, 0, 1)
);
}

În cadrul framework-ului de laborator, în fișierul Transform2D.h sunt definite funcțiile pentru


calculul matricilor de translație, rotație și scalare. În momentul acesta toate funcțiile întorc
matricea identitate. În cadrul laboratorului va trebui să modificați codul pentru a calcula matricile
respective.

Transformări compuse
De ce sunt necesare matricile? Pentru a reprezenta printr-o singură matrice de transformări o
secvență de transformări elementare, în locul aplicării unei secvențe de transformări elementare
pe un anume obiect.

Deci, dacă dorim să aplicăm o rotație, o scalare și o translație pe un obiect, nu facem rotația
obiectului, scalarea obiectului urmată de translația lui, ci calculăm o matrice care reprezintă
transformarea compusă (de rotație, scalare și translație), după care aplicăm această transformare
compusă pe obiectul care se dorește a fi transformat.
Astfel, dacă dorim să aplicăm o rotație (cu matricea de rotație R), urmată de o scalare (S ),
urmată de o translație (T ) pe un punct (x,y), punctul transformat (x′ ,y ′ ) se va calcula astfel:


x 1 0 tx sx 0 0 cos(u) −sin(u) 0 x
⎡ ⎤ ⎡ ⎤⎡ ⎤⎡ ⎤⎡ ⎤

⎢y ⎥ = ⎢0 1 ty ⎥ ⎢ 0 sy 0 ⎥ ⎢ sin(u) cos(u) 0⎥⎢ y ⎥
⎣ ⎦ ⎣ ⎦⎣ ⎦⎣ ⎦⎣ ⎦
1 0 0 1 0 0 1 0 0 1 1

Deci, matricea de transformări compuse M este M = T ∗ S ∗ R.

În cadrul laboratorului, în fișierul Laborator3.cpp, există o serie de obiecte (pătrate) pentru


care, în funcția Update(), înainte de desenare, se definesc matricile de transformări. Comanda
de desenare se dă prin funcția RenderMesh2D().

modelMatrix = glm::mat3(1);
modelMatrix *= Transform2D::Translate(150, 250);
RenderMesh2D(meshes["square1"], shaders["VertexColor"], modelMatrix);

Pentru exemplul anterior, matricea de translație creată va avea ca efect translatarea pătratului
curent cu (150, 250). Pentru efecte de animație continuă, pașii de translație ar trebui să se
modifice în timp.

Exemplu:

tx += deltaTimeSeconds * 100;
ty += deltaTimeSeconds * 100;
model_matrix *= Transform2D::Translate(tx, ty);

Rețineți: dacă la animație nu țineți cont de timpul de rulare al unui frame


(deltaTimeSeconds), veți crea animații dependente de platformă.

Exemplu: dacă la fiecare frame creșteți pe tx cu un pas constant (ex: tx += 0.01), atunci
animația se va comporta diferit pe un calculator care merge mai repede față de unul care merge
mai încet. Pe un calculator care rulează la 50 FPS, obiectul se va deplasa 0.01 * 50 = 0.5 unități
în dreapta într-o secundă. În schimb, pe un calculator mai încet, care rulează la 10 FPS, obiectul
se va deplasa 0.01 * 10 = 0.1 unități în dreapta într-o secundă, deci animația va fi de 5 ori mai
lentă.

Din acest motiv este bine să țineți cont de viteza de rulare a fiecărui calculator (dată prin
deltaTimeSeconds, care reprezintă timpul de rulare al frame-ului anterior) și să modificați pașii
de translație, unghiurile de rotație și factorii de scalare în funcție de această variabilă.

Transformarea fereastra-poartă
Desenele reprezentate într-un program de aplicație grafică (2D sau 3D) sunt, de regulă, raportate
la un sistem de coordonate diferit de cel al suprafeței de afișare.

În exercițiile anterioare din acest laborator, coordonatele obiectelor au fost raportate la


dimensiunea ferestrei definită prin glViewport().

Exemplu: Dacă viewport-ul meu are colțul din stânga jos (0, 0) și are lățimea 1280 și înălțimea
720, atunci toate obiectele ar trebui desenate în acest interval, dacă vreau să fie vizibile. Acest
lucru mă condiționează să îmi gândesc toată scena în (0, 0) - (1280, 720). Dacă vreau să scap de
această limitare, pot să îmi gândesc scena într-un spațiu logic (de exemplu îmi creez toate
obiectele în spațiul (-1, -1) - (1, 1) și apoi să le desenez în poarta de afișare, dar aplicând ceea ce
se numește transformarea fereastră poartă.

În cele ce urmează vedem ce presupune această transformare și cum pot să imi gândesc scena
fără să fiu limitat de dimensiunea viewport-ului.

Definiția matematică:

xp − xpmin xf − xf min
=
xpmax − xpmin xf max − xf min

yp − ypmin yf − yf min
=
ypmax − ypmin yf max − yf min

Transformarea este definită prin 2 dreptunghiuri, în cele două sisteme de coordonate, numite
fereastră sau spațiul logic și poartă sau spațiul de afișare. De aici numele de transformarea
fereastră-poartă sau transformarea de vizualizare 2D.

F: un punct din fereastră

P: punctul în care se transformă F prin transformarea de vizualizare

Poziția relativă a lui P în poarta de afișare trebuie să fie aceeași cu poziția relativă a lui F în
fereastră.
xpmax − xpmin
sx =
xf max − xf min

ypmax − ypmin
sy =
yf max − yf min

sx, sy depind de dimensiunile celor două ferestre


tx, ty depind de pozițiile celor două ferestre față de originea sistemului de coordonate în
care sunt definite

tx = xpmin − sx ∗ xf min

ty = ypmin − sy ∗ yf min

În final, transformarea fereastră-poartă are următoarele ecuații:

xp = xf ∗ sx + tx

yp = yf ∗ sy + ty

Considerăm o aceeași orientare a axelor celor două sisteme de coordonate. Dacă acestea au
orientări diferite (ca în prima imagine), trebuie aplicată o transformare suplimentară de corecție a
coordonatei y.

Efectele transformării

mărire/micșorare, în funcție de dimensiunile ferestrei și ale porții


deformare dacă fereastra și poarta nu sunt dreptunghiuri asemenea
pentru scalare uniformă, s = min(sx, sy) , afișarea centrată în poartă presupune o
translație suplimentară pe axa Ox sau pe axa Oy:

T sx = (xpmax − xpmin − s ∗ (xf max − xf min))/2

T sy = (ypmax − ypmin − s ∗ (yf max − yf min))/2

decuparea primitivelor aflate în afara ferestrei vizuale

Matricea transformării fereastră-poartă


De reținut este că transformarea fereastră-poartă presupune o scalare și o translație. Ea are
următoarea expresie, cu formulele de calcul pentru sx, sy, tx, ty prezentate anterior:

xp sx 0 tx xf
⎡ ⎤ ⎡ ⎤⎡ ⎤

⎢ yp ⎥ = ⎢ 0 sy ty ⎥ ⎢ yf ⎥
⎣ ⎦ ⎣ ⎦⎣ ⎦
1 0 0 1 1

Transformarea de vizualizare este deja implementată în clasa Laborator3_Vis2D:

//2D vizualization matrix


glm::mat3 Laborator3_Vis2D::VisualizationTransf2D(const LogicSpace & logicSpace, const ViewportSpace & viewSpace)
{
float sx, sy, tx, ty;
sx = viewSpace.width / logicSpace.width;
sy = viewSpace.height / logicSpace.height;
tx = viewSpace.x - sx * logicSpace.x;
ty = viewSpace.y - sy * logicSpace.y;

return glm::transpose(glm::mat3(
sx, 0.0f, tx,
0.0f, sy, ty,
0.0f, 0.0f, 1.0f));
}

În cadrul laboratorului, în clasa Laborator3_Vis2D, este creat un pătrat, în spațiul logic (0,0) -
(4,4). De reținut este faptul că acum nu mai trebuie să raportăm coordonatele pătratului la
spațiul de vizualizare (cum se intamplă în exercițiile anterioare), ci la spațiul logic pe care l-am
definit noi.

logicSpace.x = 0; // logic x
logicSpace.y = 0; // logic y
logicSpace.width = 4; // logic width
logicSpace.height = 4; // logic height

glm::vec3 corner = glm::vec3(0.001, 0.001, 0);


length = 0.99f;

Mesh* square1 = Object2D::CreateSquare("square1", corner, length, glm::vec3(1, 0, 0));


AddMeshToList(square1);

În funcția Update() se desenează același pătrat creat anterior, de 5 ori: patru pătrate în cele
patru colțuri și un pătrat în mijlocul spațiului logic. Se definesc 2 viewport-uri, ambele conținând
aceleași obiecte. Primul viewport este definit în jumătatea din stânga a ferestrei de afișare, iar al
doilea, în jumătatea din dreapta. Pentru primul viewport se definește transformarea fereastră-
poartă default și pentru al doilea viewport, cea uniformă. Observați că în al doilea viewport
pătratele rămân întotdeauna pătrate, pe când în primul viewport se văd ca dreptunghiuri (adică
sunt deformate), dacă spațiul logic și spațiul de vizualizare nu sunt reprezentate prin
dreptunghiuri asemenea.

Utilizare
Unde se poate folosi această transformare fereastră-poartă? De exemplu, într-un joc 2D cu
mașini de curse, se dorește în dreapta-jos a ecranului vizualizarea mașinii proprii, într-un
minimap. Acest lucru se face prin desenarea scenei de două ori.

Dacă de exemplu toată scena (traseul și toate mașinile) este gândită în spațiul logic (-10,-10) -
(10,10) (care are dimensiunea 20×20) și spațiul de afișare este (0,0) - (1280, 720), prima dată
se desenează toată scena cu parametrii funcției fereastră-poartă anterior menționați:

LogicSpace logic_space = LogicSpace(-10, -10, 20, 20);


ViewportSpace view_space = ViewportSpace(0, 0, 1280, 720);
vis_matrix *= VisualizationTransf2D(logic_space, view_space);

Dacă la un moment dat mașina proprie este în spațiul (2,2) - (5,5), adică de dimensiune 3×3 și
vreau să creez un minimap în colțul din dreapta jos al ecranului de rezoluție 280×220, pot desena
din nou aceeași scenă, dar cu urmatoarea transformare fereastră-poartă:

LogicSpace logic_space = LogicSpace(2, 2, 3, 3);


ViewportSpace view_space = ViewportSpace(1000, 500, 280, 220);
vis_matrix *= VisualizationTransf2D(logic_space, view_space);
Laboratorul 3

Descriere laborator
În cadrul acestui laborator aveți de programat în două clase:

Laborator3.cpp, pentru familiarizarea cu transformările 2D de translație, rotație și


scalare
Laborator3_Vis2D.cpp, pentru familiarizarea cu transformarea fereastră-poartă

Din clasa Main puteți să alegeți ce laborator rulați:

World *world = new Laborator3();

sau

World *world = new Laborator3_Vis2D();

OpenGL este un API 3D. Desenarea obiectelor 2D și aplicarea transformărilor 2D sunt simulate
prin faptul că facem abstracție de coordonata z.

Transformarea fereastră-poartă este și ea simulată pentru acest framework, dar veți învăța pe
parcurs că ea este de fapt inclusă în lanțul de transformări OpenGL și că nu trebuie definită
explicit.

Cerințe laborator

1. Descarcăți framework-ul de laborator [https://github.com/UPB-Graphics/Framework-


EGC/archive/master.zip]
2. Completați funcțiile de translație, rotație și scalare din /Laborator3/Transform2D.h
3. Să se modifice pașii de translație, rotație și scalare pentru cele trei pătrate ca să se creeze
animații.
4. Cu tastele W, A, S, D să se translateze fereastra logică Laborator3_Vis2D. Cu tastele Z
și X să se facă zoom in și zoom out pe fereastra logică.

egc/laboratoare/03.txt · Last modified: 2020/10/25 19:04 by stefania.cristea1708


Laboratorul 04
Video Laborator 4: https://youtu.be/huCrfe9sbMQ
[https://youtu.be/huCrfe9sbMQ]
Autor: Alex Dinu [mailto:adix64@gmail.com]

Transformări 3D
Obiectele 3D sunt definite într-un sistem de coordonate 3D, de exemplu XYZ. În
cadrul acestui laborator vom implementa diferite tipuri de transformări ce pot fi
aplicate obiectelor: translații, rotații și scalări. Acestea sunt definite în format
matriceal, în coordonate omgene, așa cum ați învățat deja la curs. Matricile
acestor transformări sunt următoarele:

Translația

x 1 0 0 tx x
⎡ ⎤ ⎡ ⎤⎡ ⎤

⎢y ⎥ ⎢0 1 0 ty ⎥ ⎢ y ⎥
⎢ ⎥ = ⎢

⎥⎢
⎥ ⎥
⎢ z′ ⎥ ⎢0 0 1 tz ⎥ ⎢ z ⎥

⎣ ⎦ ⎣ ⎦⎣ ⎦
1 0 0 0 1 1

Rotația

Rotația față de axa OX



x 1 0 0 0 x
⎡ ⎤ ⎡ ⎤⎡ ⎤

⎢y ⎥ ⎢0 cos(u) −sin(u) 0⎥⎢ y ⎥
⎢ ⎥ = ⎢

⎥⎢
⎥ ⎥
⎢ z′ ⎥ ⎢0 sin(u) cos(u) 0⎥⎢ z ⎥

⎣ ⎦ ⎣ ⎦⎣ ⎦
1 0 0 0 1 1

Rotația față de axa OY



x cos(u) 0 sin(u) 0 x
⎡ ⎤ ⎡ ⎤⎡ ⎤

⎢y ⎥ ⎢ 0 1 0 0⎥⎢ y ⎥
= ⎢ ⎥
⎢ ⎥ ⎢ ⎥⎢ ⎥
⎢ z′ ⎥ ⎢ −sin(u) 0 cos(u) 0⎥⎢ z ⎥
⎣ ⎦ ⎣ ⎦⎣ ⎦
1 0 0 0 1 1

Rotația față de axa OZ



x cos(u) −sin(u) 0 0 x
⎡ ⎤ ⎡ ⎤⎡ ⎤

⎢y ⎥ ⎢ sin(u) cos(u) 0 0⎥⎢ y ⎥
⎢ ⎥ = ⎢

⎥⎢
⎥ ⎥
⎢ z′ ⎥ ⎢ 0 0 1 0⎥⎢ z ⎥
⎣ ⎦ ⎣ ⎦⎣ ⎦
1 0 0 0 1 1

Rotația față de o axă paralelă cu axa OX

Rotația relativă la o axă paralelă cu axa OX se rezolvă în cel mai simplu mod
prin:

1. translatarea atât a punctului asupra căruia se aplică rotația cât și a


punctului în jurul căruia se face rotația a.î. cel din urmă să se afle pe axa
OX
2. rotația normală (în jurul axei OX)
3. translatarea rezultatului a.î. punctul în jurul căruia s-a făcut rotația să
ajungă în poziția sa inițială

Similar se procedeaza și pentru axele paralele cu OY și OZ.

La curs veți învăța cum puteți realiza rotații față de axe oarecare (care nu sunt
paralele cu OX, OY sau OZ).

Scalarea

Scalarea față de origine



x sx 0 0 0 x
⎡ ⎤ ⎡ ⎤⎡ ⎤

⎢y ⎥ ⎢ 0 sy 0 0⎥⎢ y ⎥
⎢ ⎥ = ⎢

⎥⎢
⎥ ⎥
⎢ z′ ⎥ ⎢ 0 0 sz 0 ⎥ ⎢ z ⎥

⎣ ⎦ ⎣ ⎦⎣ ⎦
1 0 0 0 1 1
Dacă sx = sy = sz atunci avem scalare uniformă, altfel avem scalare
neuniformă.

Scalarea față de un punct oarecare

Scalarea relativă la un punct oarecare se rezolvă în cel mai simplu mod prin:

1. translatarea atât a punctului asupra căruia se aplică scalarea cât și a


punctului față de care se face scalarea a.î. cel din urmă să fie originea
sistemului de coordonate
2. scalarea normală (față de origine)
3. translatarea rezultatului a.î. punctul față de care s-a făcut scalarea să
ajungă în poziția sa inițială

Utilizarea bibliotecii GLM


În cadrul laboratorului folosim biblioteca GLM care este o bibliotecă implementată
cu matrici în formă coloană, exact același format ca OpenGL. Forma coloană
diferă de forma linie prin ordinea de stocare a elementelor matricei în memorie,
Matricea de translație arată în modul următor în memorie:

glm::mat4 Translate(float tx, float ty, float tz)


{
return glm::mat4(
1, 0, 0, 0, // coloana 1 in memorie
0, 1, 0, 0, // coloana 2 in memorie
0, 0, 1, 0, // coloana 3 in memorie
tx, ty, tz, 1); // coloana 4 in memorie

Din această cauză, este convenabil ca matricile să fie scrise manual în forma
aceasta:

glm::mat4 Translate(float tx, float ty, float tz)


{
return glm::transpose(
glm::mat4( 1, 0, 0, tx,
0, 1, 0, ty,
0, 0, 1, tz,
0, 0, 0, 1)
);
}
În framework-ul de laborator, în fișierul Transform3D.h sunt definite funcțiile
pentru calculul matricilor de translație, rotație și scalare. În momentul acesta
toate funcțiile întorc matricea identitate. În cadrul laboratorului va trebui să
modificați codul pentru a calcula matricile respective.

În cadrul laboratorului, în fișierul Laborator4.cpp, există o serie de obiecte


(cuburi) pentru care, în funcția Update(), inainte de desenare, se definesc
matricile de transformări. Comanda de desenare se dă prin funcția
RenderMesh(), care are ca parametru și matricea de transformări.

modelMatrix = glm::mat4(1);
modelMatrix *= Transform2D::Translate(1, 2, 1);
RenderMesh(meshes["box"], modelMatrix);

Pentru exemplul anterior, matricea de translație creată va avea ca efect


translatarea cubului curent cu (1, 2, 1). Pentru efecte de animație continuă, pașii
de translație ar trebui să se modifice în timp.

Cerințe laborator

1. Descărcați framework-ul de laborator [https://github.com/UPB-


Graphics/Framework-EGC/archive/master.zip]
2. Completați funcțiile de translație, rotație și scalare din
/Laborator4/Transform3D.h
3. Să se realizeze animații la apăsarea tastelor (în OnInputUpdate) pentru
cele 3 cuburi, astfel:
cu tastele W, A, S, D, R, F să se deplaseze primul cub în scenă
cu tastele 1 și 2 să se scaleze al doilea cub (să se mărească și să
se micșoreze) față de centrul propriu
cu tastele 3, 4, 5, 6, 7, 8 să se rotească al treilea cub față de axele
locale OX, OY, OZ

egc/laboratoare/04.txt · Last modified: 2020/11/02 11:22 by ovidiu.dinu


Laboratorul 05
Video Laborator 5: https://youtu.be/HOv-P8QnEAA [https://youtu.be/HOv-P8QnEAA]
Autor: Florin Iancu [mailto:florineugen.iancu@gmail.com]

Spațiul Obiect
Spațiul obiect mai este denumit și SPAȚIUL COORDONATELOR LOCALE.

Pentru a putea lucra mai eficient și a reutiliza obiectele 3D definite, în general fiecare obiect este
definit într-un sistem de coordonate propriu. Obiectele simple sau procedurale pot fi definite direct din
cod însă majoritatea obiectelor utilizate în aplicațiile 3D sunt specificate în cadrul unui program de
modelare precum 3D Studio Max, Maya, Blender etc. Definind independent fiecare obiect 3D,
putem să îi aplicăm o serie de transformări de rotație, scalare și translație pentru a reda obiectul în
scena 3D. Un obiect încărcat poate fi afișat de mai multe ori prin utilizarea unor matrici de
modelare, câte una pentru fiecare instanță a obiectului inițial, ce mențin transformările 3D aplicate
acestor instanțe.

În general, fiecare obiect 3D este definit cu centrul (sau centrul bazei ca în poza de mai jos) în
originea propriului său sistem de coordonate, deoarece în acest fel pot fi aplicate mai ușor
transformările de modelare. Astfel, rotația și scalarea față de centrul propriu sunt efectuate
întotdeauna față de origine.

Spațiul Lume
Spațiul lume sau SPAȚIUL COORDONATELOR GLOBALE este reprezentat prin intermediul matricei
de modelare, aceeași despre care s-a vorbit mai sus. Matricea se obține printr-o serie de rotații,
scalări și translații. Prin înmulțirea fiecărui vertex al unui obiect (mesh 3D) cu această matrice,
obiectul va fi mutat din spațiul local în spațiul lume, adică se face trecerea de la coordonate locale la
coordonate globale.

Folosind matrici de modelare diferite putem amplasa un obiect în scenă de mai multe ori, în locații
diferite, cu rotație și scalare diferită dacă este necesar. Un exemplu este prezentat în scena de mai
jos.
Spațiul de Vizualizare
Spațiul de vizualizare sau SPAȚIUL CAMEREI este reprezentat de matricea de vizualizare.

Matricea de modelare poziționează obiectele în scenă, în spațiul lume. Dar o scenă poate fi vizualizată
din mai multe puncte de vedere. Pentru aceasta există transformarea de vizualizare. Dacă într-o scenă
avem mai multe obiecte, fiecare obiect are o matrice de modelare diferită (care l-a mutat din spațiul
obiect în spațiul lume), însă toate obiectele au aceeași matrice de vizualizare. Transformarea de
vizualizare este definită pentru întreaga scenă.

În spațiul lume camera poate să fie considerată ca un obiect având cele 3 axe locale OX, OY, OZ (vezi
poza). Matricea de vizualizare se poate calcula folosind funcția glm::lookAt.

glm::mat4 View = glm::lookAt(glm::vec3 posCameraLume, glm::vec3 directieVizualizare, glm::vec3 cameraUP);


Ox,Oy,Oz sunt axele sistemului de coordonate ale lumii (spațiul scenei 3D). Punctul O nu este marcat
în imagine. O’x’,O’y’,O’z’ sunt axele sistemului de coordonate al observatorului (spațiul de vizualizare).
Punctul O’ nu este marcat în imagine (este înăuntrul aparatului).

Vectorul forward este direcția în care observatorul privește, și este de asemenea normala la planul de
vizualizare (planul fiind baza volumului de vizualizare, ce seamănă cu o piramidă și este marcat cu
contur portocaliu). Vectorul right este direcția dreapta din punctul de vedere al observatorului.
Vectorul up este direcția sus din punctul de vedere al observatorului.

În imagine, observatorul este un pic înclinat, în mod intenționat, în jos, față de propriul sistem de axe.
Când observatorul este perfect aliniat cu axele, right coincide cu +x’, up coincide cu +y’, iar forward
coincide cu -z’. În imagine, se poate vedea că up nu coincide cu +y’, iar forward nu coincide cu -z’.

Vectorul “up” se proiectează în planul de vizualizare, cu direcția de proiecție paralelă cu normala la


planul de vizualizare. Proiecția acestuia dă direcția axei verticale a planului de vizualizare.

În spațiul lume camera poate fi considerată un simplu obiect 3D asupra căruia aplicăm transformările
de rotație și translație. Dacă în spațiul lume, camera poate fi poziționată oriunde și poate avea orice
orientare, în spațiul de vizualizare (spațiul observator) camera este întotdeauna poziționată în (0,0,0)
și privește în direcția OZ negativă.

Matricea de vizualizare conține transformări de rotație și translație, la fel ca și matricea de modelare.


De aceea, dacă ținem scena pe loc și mutăm camera, sau dacă ținem camera pe loc și
rotim/translatăm scena, obținem același efect:

„The engines don’t move the ship at all. The ship stays where it is and the engines move the universe
around it.”
- Futurama

Totuși, cele două matrici au scopuri diferite. Una este folosită pentru poziționarea obiectelor în scenă,
iar cealaltă pentru vizualizarea întregii scene din punctul de vedere al camerei.

Exemplu: Dacă vrem să ne uităm pe axa OX(lume) din poziția (3, 5, 7) codul corespunzător pentru
funcția glm::lookAt este:

glm::lookAt(glm::vec3(3, 5, 7), glm::vec3(1, 0, 0), glm::vec3(0, 1, 0));


Spațiul de Proiecție
După aplicarea transformării de vizualizare, în spațiul de vizualizare, camera se află în origine și
privește înspre –OZ. Pentru a putea vizualiza pe ecran această informație este necesar să se facă
proiecția spațiului vizualizat de cameră într-un spațiu 2D. Cum spațiul vizibil al camerei poate fi de
diferite feluri, cel mai adesea trunchi de piramida (proiecție perspectivă) sau paralelipiped
(proiecție ortografică), în OpenGL este necesară trecerea într-un spațiu final numit spațiu de
proiecție ce reprezintă un cub centrat în origine cu dimensiunea 2, deci coordonatele X, Y, Z între -1 și
+1.

Din spațiul de proiecție este foarte ușor matematic să obținem proiecția finală 2D pe viewport fiind
nevoie doar să mapăm informația din cubul [-1,1] scalată corespunzător pe viewport-ul definit de
aplicație.

Matricea de Proiecție
Trecerea din spațiul de vizualizare în spațiul de proiecție se face tot utilizând o matrice, denumită
matrice de proiecție, calculată în funcție de tipul de proiecție definit. Biblioteca GLM oferă funcții de
calcul pentru cele mai utilizate 2 metode de proiecție în aplicațiile 3D, anume: proiecția perspectivă
și ortografică

Datele (vertecșii din spațiul de vizualizare) sunt înmulțite cu matricea de proiecție pentru a se
obține pozițiile corespunzătoare din spațiul de proiecție.

Proiecția Ortografică
În proiecția ortografică observatorul este plasat la infinit. Distanța până la geometrie nu influențează
proiecția și deci nu se poate determina vizibil din proiecție. Proiecția ortografică păstrează paralelismul
liniilor din scenă.

Proiecția ortografică este definită de lățimea și înălțimea ferestrei de vizualizare cât și a distanței de
vizualizare dintre planul din apropiere și planul din depărtare. În afara acestui volum obiectele nu
vor mai fi văzute pe ecran.
Matricea de proiecție poate fi calculată utilizând funcția glm::ortho unde punctele left, right,
bottom, top sunt relative față de centrul ferestrei (0, 0) și definesc înălțimea și lățimea ferestrei
de proiecție

glm::mat4 Projection = glm::ortho(float left, float right, float bottom, float top, float zNear, float zFar);

Proiecția Perspectivă
Proiecția perspectivă este reprezentată de un trunchi de piramidă (frustum) definit prin cele 2 planuri,
cel din apropiere și cel din depărtare, cât și de deschiderea unghiurilor de vizualizare pe cele 2
axe, OX și OY. În proiecția perspectivă distanța până la un punct din volumul de vizualizare
influențează proiecția.

Matricea de proiecție în acest caz poate fi calculată cu ajutorul funcției glm::perspective ce


primește ca parametri deschiderea unghiului de vizualizare pe orizontală (Field of View - FoV),
raportul dintre lățimea și înălțimea ferestrei de vizualizare (aspect ratio), cât și distanța până la cele
2 planuri zFar și zNear.

glm::mat4 Projection = glm::perspective(float fov, float aspect, float zNear, float zFar);
În cazul proiecției perspectivă, după înmuțirea coordonatelor din spațiul view, componenta w a fiecărui
vertex este diferită, ceea ce înseamnă că spațiul de proiecție nu e același pentru fiecare vertex. Pentru
a aduce toți vectorii în același spațiu se împarte fiecare componentă a vectorului rezultat cu
componenta w. Această operație este realizată automat de procesorul grafic, în cadrul unei aplicații
fiind nevoie doar de înmulțirea cu matricea de proiecție.

Volum de vizualizare perspectivă (stânga) și rezultatul obținut (dreapta) în urma aplicării


transformării de proiecție asupra geometriei din scenă

Spațiul Coordonatelor de Dispozitiv Normalizate (NDC)


După aplicarea transformărilor de Modelare, Vizualizare și Proiecție iar apoi divizarea cu W a
vectorilor, se obține spațiul de coordonate normalizate (NDC) reprezentat de un CUB centrat în origine
(0, 0, 0) cu latura 2. Informația din acest cub se poate proiecta foarte ușor pe orice suprafață 2D de
desenare definită de utilizator.

Exemplu rezultat al proiecției în coordonate dispozitiv normalizate (NDC). Proiecție ortografică


(stânga), perspectivă (dreapta)
Exemplu vizualizare spațiu NDC din direcția camerei (stânga) și proiecția corespunzătoare pentru
un anumit viewport (dreapta)

Aplicarea Transformărilor de Modelare, Vizualizare și Proiecție


Aplicarea trasformărilor de Modelare, Vizualizare și Proiecție se face prin înmulțirea fiecărui vertex
al geometriei din scenă cu cele 3 matrici calculate.

pos_vertex = Projection * View * Model * pos_vertex

În cadrul laboratorului trebuie doar să calculăm aceste matrici și să le trimitem ca parametru funcției
de randare RenderMesh. Înmulțirile respective sunt executate pe procesorul grafic în cadrul
programului vertex shader ce va fi introdus începând cu laboratorul următor.

Transformări de Cameră
Implementarea unei camere în cadrul unei aplicații 3D depinde de cerințele aplicației. În practică cele
mai utilizate tipuri de implementări de cameră sunt: First person și Third person.

First-person Camera
Camera de tipul First-person presupune faptul că scena 3D este vizualizată din perspectiva ochilor
unui observator, adesea uman. Constrângerile de implementare sunt următoarele:

Translația camerei First-person

translațiile față/spate se calculează utilizând vectorul forward (direcția de vizualizare sau


proiecția acestuia în planul orizontal XOZ)
translațiile sus/jos se calculează utilizând vectorul local Up sau cel mai adesea direcția OY
globală (glm::vec3(0, 1, 0))
translațiile dreapta/stânga se calculează folosind vectorul local right (ce se poate obține și prin
operația de cross product între vectorii forward și up) sau folosind proiecția acestuia pe
planul orizontal XOZ

posCamera = posCamera + glm::normalize(direction) * distance;

Rotația camerei First-person

rotațiile se fac păstrând observatorul pe loc și modificând direcția în care privește acesta
pentru rotația stânga/dreapta, vectorii forward respectiv right se pot calcula prin aplicarea
transformării de rotație în jului axei OY globale. Se poate roti și în jurul axei OY locale
(vectorul up), însă în general nu prea are aplicabilitate practică
vectorul up se poate recalcula folosind cross product între right și forward

forward = RotateWorldOY(angle) * forward;


right = RotateWorldOY(angle) * right;
up = glm::cross(right, forward);

rotația sus/jos se poate face rotind vectorii forward respectiv up în jurul vectorului axei OX
adică vectorul right (right rămâne constant)

forward = RotateLocalOX(angle) * forward;


up = glm::cross(right, forward);

Matricile de rotație necesare se pot calcula folosind funcția glm::rotate

glm::mat4 = glm::rotate(glm::mat4 model, float angle, glm::vec3 rotationAxis);

primul parametru reprezintă o matrice de modelare asupra căreia aplicăm transformarea


specificată. Atunci când nu avem o transformare precedentă se pornește de la matricea
identitate glm::mat4(1.0f)
rotationAxis este axa față de care rotim. În cazul nostru pentru rotația față de OX este
vectorul right, pentru rotația față de OZ este vectorul forward, sau glm::vec3(0, 1, 0)
pentru rotația față de OY global
întrucât vectorii utilizați sunt glm::vec3 când facem înmulțirea va trebui să construim un
vector de 4 componente ca să putem înmulți cu matricea de 4×4. Puteți construi vectorul
astfel:

glm::vec3 forward = ...


glm::vec4 newVec = glm::vec4(forward, 1.0);

Dacă vrem să rotim vectorul “forward” în jurul axei OY globale atunci facem astfel:

// get the rotate vec4 vector


glm::vec4 newVector = glm::rotate(glm::mat4(1.0f), angle, glm::vec3(0, 1, 0)) * glm::vec4(forward, 1);

// extract the vec3 vector and then normalize it


forward = glm::normalize(glm::vec3(newVector));

După ce ați făcut calculele de rotație aveți grijă să păstrați vectorii normalizați

glm::vec3 vector = ...


glm::vec3 rezultat = glm::normalize(vector);
Third-person Camera
În cazul camerei de tip Third-person observatorul se mută în jurul unui obiect de interes, ce
reprezintă întotdeauna centrul atenției. Deci rotațiile se fac într-un mod diferit

Rotația Camerei Third-person

se translatează observatorul pe direcția de vizualizare în punctul de interes (target)


se aplică rotația de tip First-person specifică
se traslatează observatorul înapoi pe noua direcție de vizualizare cu aceeași distanță

În laborator aveți variabila distanceToTarget care reține distanța până la punctul față de care rotim

Translația Camerei Third-person

Poziția camerei depinde de poziția punctului de interes. Astfel, mișcarea punctului de interes va
determina și translația camerei în mod corespunzător.

Cerințe laborator

1. Descarcăți framework-ul de laborator [https://github.com/UPB-Graphics/Framework-


EGC/archive/master.zip]
2. Să se implementeze camera de tip First Person (fișierul LabCamera.h)
3. Să se implementeze camera de tip Third Person (fișierul LabCamera.h)
4. Să se completeze funcțiile de translație ale camerei dinLaborator5::OnInputUpdate()
5. Să se completeze funcțiile de rotație ale camerei din Laborator5::OnMouseMove()
6. Să se deseneze încă 2 obiecte în scena 3D având rotația/scalarea/translația diferite
aveți grijă să setați matricea de modelare de fiecare dată înainte de desenare
utilizați glm::translate(), glm::rotate() și glm::scale() pentru a construi o
matrice de modelare pentru fiecare obiect
7. Schimbare proiecție perspectivă/ortografică
tasta O face trecerea în proiecție ortografică
tasta P face trecerea în proiecție perspectivă
8. Să se modifice FoV-ul camerei în cazul proiecției persepective
folosiți 2 taste pentru a modifica pozitiv și negativ FoV-ul
se va folosi OnInputUpdate()
9. Să se modifice lățimea și/sau înălțimea ferestrei de proiecție în cazul proiecției ortografice
se va folosi OnInputUpdate()
egc/laboratoare/05.txt · Last modified: 2020/11/12 14:19 by victor.asavei
Laboratorul 06
Video Laborator 6: https://youtu.be/f7q2TGCRly0 [https://youtu.be/f7q2TGCRly0]
Autor: Anca Băluțoiu [mailto:maria_anca.balutoiu@upb.ro]

Banda Grafica
Banda Grafica este un lant de operatii executate de procesoarele GPU. Unele dintre
aceste operatii sunt descrise in programe numite shadere (eng. shaders), care sunt
scrise de programator si transmise la GPU pentru a fi executate de procesoarele
acestuia. Pentru a le deosebi de alte operatii executate in banda grafica, pe care
programatorul nu le poate modifica, shaderele sunt numite „etape programabile”.
Ele dau o mare flexibilitate in crearea de imagini statice sau dinamice cu efecte
complexe redate in timp real (de ex. generarea de apa, nori, foc etc prin functii
matematice).

Folosind OpenGL sunt transmise la GPU: coordonatele varfurilor, matricile de


transformare a varfurilor (M: modelare, V: vizualizare, P: proiectie, MV: modelare-
vizualizare, MVP: modelare-vizualizare-proiectie), topologia primitivelor, texturi si ale
date.
1. In etapa programabila VERTEX SHADER se transforma coordonatele unui varf,
folosind matricea MVP, din coordonate obiect in coordonate de decupare (eng. clip
coordinates). De asemenea, pot fi efectuate si calcule de iluminare la nivel de varf.
Programul VERTEX SHADER este executat in paralel pentru un numar foarte mare de
varfuri.

2. Urmeaza o etapa fixa, in care sunt efectuate urmatoarele operatii:

asamblarea primitivelor folosind varfurile transformate in vertex shader si


topologia primitivelor;
eliminarea fetelor nevizibile;
decuparea primitivelor la frontiera volumului canonic de vizualizare (ce
inseamna? [https://gamedev.stackexchange.com/q/6279]);
impartirea perspectiva, prin care se calculeaza coordonatele dispozitiv
normalizate ale varfurilor: xd = xc/w; yd = yc/w;zd = zc/w, unde [xc,yc,zc,w]
reprezinta coordonatele unui varf in sistemul coordonatelor de decupare;
transformarea fereastra–poarta: din fereastra (-1, -1) – (1, 1) in viewport-ul
definit de programator.
3. Urmatoarea etapa este Rasterizarea. Aceasta include:

calculul adreselor pixelilor in care se afiseaza fragmentele primitivelor


(bucatele de primitive de dimensiune egala cu a unui pixel);
calculul culorii fiecarui fragment, pentru care este apelat programul
FRAGMENT SHADER
in etapa programabila FRAGMENT SHADER se calculeaza culoarea unui
fragment pe baza geometriei si a texturilor; programul FRAGMENT SHADER
este executat in paralel pentru un numar mare de fragmente.
testul de vizibilitate la nivel de fragment (algoritmul z-buffer);
operatii raster, de exemplu pentru combinarea culorii fragmentului cu aceea
existenta pentru pixelul in care se afiseaza fragmentul.

Rezultatul etapei de rasterizare este o imagine memorata intr-un tablou de pixeli ce


va fi afisat pe ecran, numit ^^frame buffer^^.

Incepand cu a cincea generatie


[https://en.wikipedia.org/wiki/List_of_Intel_graphics_processing_units#Fifth_generation] de
procesoare video integrate si OpenGL 3.x, intre etapele 2 si 3 exista inca o etapa
programabila, numita Geometry shader.

Shader OpenGL
Pentru implementarea de programe SHADER in OpenGL se foloseste limbajul dedicat
GLSL (GL Shading Language).

Legarea unui shader la programul care foloseste OpenGL este o operatie complicata,
de aceea va este oferit codul prin care se incarca un shader.

Un VERTEX SHADER e un program care se executa pentru FIECARE vertex trimis


catre banda grafica. Rezultatul transformarilor, care reprezinta coordonata post-
proiectie a vertexului procesat, trebuie scris in variabila standard gl_Position
[https://www.opengl.org/wiki/Built-in_Variable_(GLSL)#Vertex_shader_outputs]
care e folosita apoi de banda grafica. Un vertex shader are tot timpul o functie
numita main. Un exemplu de vertex shader:

#version 330

layout(location = 0) in vec3 v_position;

// Uniform properties
uniform mat4 Model;
uniform mat4 View;
uniform mat4 Projection;

void main()
{
gl_Position = Projection * View * Model * vec4(v_position, 1.0);
}

Un FRAGMENT SHADER e un program ce este executat pentru FIECARE fragment


generat in urma operatiei de rasterizare (ce inseamna?
[https://graphicdesign.stackexchange.com/q/260]). Fragment shader are in mod
obligatoriu o functie numita main. Un exemplu de fragment shader:

#version 330

layout(location = 0) out vec4 out_color;

void main()
{
out_color = vec4(1, 0, 0, 0);
}

Cum legam un obiect geometric la shader?


Legarea intre obiecte (mesh, linii etc.) si shadere se face prin atribute. Datorita
multelor versiuni de OpenGL exista multe metode prin care se poate face aceasta
legare. In laborator vom invata metoda specifica OpenGL 3.3 si OpenGL 4.1.
Metodele mai vechi nu mai sunt utilizate decat in atunci cand hardware-ul utilizat
impune restrictii de API.

API-ul OpenGL modern (3.3+) utilizeaza metoda de legare bazata pe layout-uri


[https://www.opengl.org/wiki/Layout_Qualifier_(GLSL)]. In aceasta metoda se folosesc
pipe-uri ce leaga un atribut din OpenGL de un nume de atribut in shader.

glEnableVertexAttribArray(2);
glVertexAttribPointer(2, 3, GL_FLOAT, GL_FALSE, sizeof(VertexFormat), (void*)0);

Prima comanda seteaza pipe-ul cu numarul 2 ca fiind utilizat. A doua comanda


descrie structura datelor in cadrul VBO-ului astfel:

pe pipe-ul 2 se trimit la shader 3 float-uri (argument 3) pe care nu le


normalizam (argument 4)
argumentul 5 numit si stride, identifica pasul de citire (in bytes) in cadrul
VBO-ului pentru a obtine urmatorul atribut; cu alte cuvinte, din cati in cati
octeti sarim cand vrem sa gasim un nou grup de cate 3 float-uri care
reprezinta acelasi lucru
argumentul 6 identifica offsetul inital din cadrul buffer-ul legat la
GL_ARRAY_BUFFER (VBO); cu alte cuvinte, de unde plecam prima oara.

In Vertex Shader vom primi atributul respectiv pe pipe-ul cu indexul specificat la


legare, astfel:
layout(location = 2) in vec3 vertex_attribute_name;

Mai multe informatii se pot gasi pe pagina de documentatie Vertex Shader attribute
index
[https://www.opengl.org/wiki/Layout_Qualifier_(GLSL)#Vertex_shader_attribute_index].

Pentru mai multe detalii puteti accesa:

API-ul de OpenGL aici: https://www.opengl.org/sdk/docs/man/


[https://www.opengl.org/sdk/docs/man/]
API-ul pentru GLSL aici: https://www.opengl.org/sdk/docs/manglsl/
[https://www.opengl.org/sdk/docs/manglsl/]

Un articol despre istoria complicata a OpenGL si competitia cu Direct3D/DirectX poate


fi citit aici [https://softwareengineering.stackexchange.com/q/60544].

Cum trimitem date generale la un shader?


La un shader putem trimite date de la CPU prin variabile uniforme. Se numesc
uniforme pentru ca nu variaza pe durata executiei shader-ului. Ca sa putem trimite
date la o variabila din shader trebuie sa obtinem locatia variabilei in programul
shader cu functia glGetUniformLocation
[https://www.opengl.org/sdk/docs/man4/html/glGetUniformLocation.xhtml]:

int location = glGetUniformLocation(int shader_program, "uniform_variable_name_in_shader");

shader_program reprezinta ID-ul programului shader compilat pe placa


video
in cadrul framework-ului de laborator ID-ul se poate obtine apeland functia
shader→GetProgramID() sau direct accesand vriabila membru
shader→program
Apoi, dupa ce avem locatia (care reprezinta un offset/pointer) putem trimite la acest
pointer informatie cu functii de tipul glUniform
[https://www.opengl.org/sdk/docs/man4/html/glUniform.xhtml]:

//void glUniformMatrix4fv(GLint location, GLsizei count, GLboolean transpose, const GLfloat *value)
glm::mat4 matrix(1.0f);
glUniformMatrix4fv(location, 1, GL_FALSE, glm::value_ptr(matrix));

// void glUniform4f(GLint location, GLfloat v0, GLfloat v1, GLfloat v2, GLfloat v3)
glUniform4f(location, 1, 0.5f, 0.3f, 0);

//void glUniform3i(GLint location, GLint v0, GLint v1, GLint v2)


glUniform3i(location, 1, 2, 3);

//void glUniform3fv(GLint location, GLsizei count, const GLfloat *value)


glm::vec3 color = glm::vec3(1.0f, 0.5f, 0.8f);
glUniform3fv(location, 1, glm::value_ptr(color));

Functiile glUniform sunt de forma glUniform[Matrix?]NT[v?] (regex) unde:

Matrix - in cazul in care e prezent identifica o matrice


N - reprezinta numarul de variabile de tipul T ce vor fi trimise:
1, 2, 3, 4 in cazul tipurilor simple
pentru matrici mai exista si 2×3, 2×4, 3×2, 3×4, 4×2, 4×3
T - reprezinta tipul variabilelor trimise
ui - unsigned int
i - int
f - float
v - datele sunt specificate printr-un vector, se da adresa de memorie a primei
valori din vector

Comunicarea intre shadere-le OpenGL


In general pipeline-ul programat este alcatuit din mai multe programe shader. In
cadrul cursului de EGC vom utiliza doar Vertex Shader si Fragment Shader. OpenGL
ofera posibilitatea de a comunica date intre programele shader consecutive prin
intermendiul atributelor in si out

In metoda specifica OpenGL 3.3 numele de atribut attribute_name trebuie sa fie


acelasi atat in Vertex Shader cat si in Fragment Shader pentru a se stie legatura intre
input/output.

Vertex Shader:

#version 330 // GLSL version of shader (GLSL 330 means OpenGL 3.3 API)

out vec3 attribute_name;

Fragment Shader:

in vec3 attribute_name;

In caz ca avem support pentru GLSL 410 (OpenGL 4.1) se poate specifica si locatia
attributului astfel, caz in care doar locatiile vor fi folosite pentru a lega iesirea unui
Vertex Shader de intrarea la Fragment Shader si nu numele atributului.
Mai multe detalii se pot obtine de la: Program separation linkage
[https://www.opengl.org/wiki/Layout_Qualifier_(GLSL)#Program_separation_linkage]

Vertex Shader:
#version 410 // GLSL 410 (OpenGL 4.1 API)

layout(location = 0) out vec4 vertex_out_attribute_name;

Fragment Shader:

#version 410

layout(location = 0) in vec4 fragment_in_attribute_name;

Cerinte laborator
tasta F5 - reincarca shaderele in timpul rularii aplicatiei. Nu este nevoie sa opriti
aplicatia intrucat shaderele sunt compilate si rulate de catre placa video si nu au
legatura cu codul sursa C++ propriu zis.

1. Descarcati framework-ul de laborator [https://github.com/UPB-


Graphics/Framework-EGC/archive/master.zip]
2. Completati functia RenderSimpleMesh astfel inca sa trimiteti corect valorile
uniform catre Shader
Se interogeaza locatia uniformelor “Model”, “View” si “Projection”
Folosind glUniformMatrix4fv sa se trimita matricile corespunzatoare
catre shader
Daca ati completat corect functia, si ati completat gl_Position in vertex
shader, ar trebui sa vedeti un cub pe centrul ecranului rottit 45 grade in
jurul lui Y si colorat variat
3. Completati Vertex Shaderul
a. Se de clara atributele de intrare pentru Vertex Shader folosind layout
location

layout(location = 0) in vec3 v_position;


// same for the rest of the attributes ( check Lab6.cpp CreateMesh() );

b. Se declara atributele de iesire catre Fragment Shader

out vec3 frag_color;


// same for other attributes

c. Se salveza valorile de iesire in main()

frag_color = vertex_color;
// same for other attributes
d. Se calculeaza pozitia in clip space a vertexului primit folosind matricile
Model, View, Projection

gl_Position = Projection * View * Model * vec4(v_position, 1.0);

4. Completati Fragment Shaderul


Se primesc valorile atributelor trimise de la Vertex Shader
Valoarea de intrare ale fiecarui atribut e calculata prin interpolare liniara
intre vertexii ce formeaza patch-ul definit la desenare (triunghi, linie)

in vec3 frag_color;

Se calculeaza valoarea fragmentului (pixelului) de output

out_color = vec4(frag_color, 1);

5. Sa se utilizeze normala vertexilor pe post de culoare de output in cadrul


Fragment Shader-ului
Inspectati de asemenea structura VertexFormat pentru a intelege
ceea ce se trimite pe fiecare pipe
6. Sa se interschimbe pipe-ul 1 cu pipe-ul 3. Trimiteti normala pe pipe-ul 3 si
culoarea vertexului pe pipe-ul 1
Se inspecteaza rezultatul obtinut
7. Bonus: sa se trimita timpul aplicatiei (Engine::GetElapsedTime()), si sa se
varieze pozitia si culoarea (unul sau mai multe canale de culoare) dupa o
functie de timp (trigonometrica etc.)

egc/laboratoare/06.txt · Last modified: 2020/11/14 22:11 by maria_anca.balutoiu


Laboratorul 07
Video Laborator 7: https://youtu.be/y1st9QxXbn8 [https://youtu.be/y1st9QxXbn8]
Autor: Cristian Lambru [mailto:andrei.lambru@upb.ro]

Iluminare folosind GLSL


Lumina este un factor foarte important în redarea cât mai realistă a unei scene 3D. Împreună cu proprietățile
de material ale unui obiect, lumina determină modalitatea în care obiectul este afișat în scena 3D.

Există mai multe modele empirice pentru calculul reflexiei luminii într-un punct al unei suprafețe: Phong
(1975 [http://www.cs.northwestern.edu/~ago820/cs395/Papers/Phong_1975.pdf]), Blinn (1977
[https://www.microsoft.com/en-us/research/wp-content/uploads/1977/01/p192-blinn.pdf]), Oren-Nayar (1994
[http://www1.cs.columbia.edu/CAVE/publications/pdfs/Oren_SIGGRAPH94.pdf]), Cook-Torrance (1981
[http://inst.eecs.berkeley.edu/~cs283/sp13/lectures/cookpaper.pdf]), Lambert (1760
[https://ia600204.us.archive.org/35/items/bub_gb_zmpJAAAAYAAJ/bub_gb_zmpJAAAAYAAJ.pdf]), etc (la curs veți
discuta despre modelul Lambert, Phong și Blinn).

Modelul Phong pentru calculul reflexiei luminii


Intensitatea luminii reflectată într-un punct de pe suprafață către observator este normalizată în intervalul
[0, 1] , unde 0 reprezintă situația în care lumina care ajunge în acel punct nu este reflectată deloc către

observator și 1 este situația în care tot fasciculul de lumină care ajunge in punctul respectiv este reflectat
către observator. Pentru a calcula această intensitate în punctul ales vom folosi un model de reflexie care
extinde modelul Phong și care conține un total de 4 componente ale intensității luminii pentru a descrie
intensitatea finală in punctul de pe suprafață:

Componenta emisivă
Componenta ambientală
Componenta difuză
Componenta speculară

Contribuția fiecărei componente este calculată ca o combinație dintre proprietățile de material ale obiectului
(factorul de strălucire și de difuzie al materialului) și proprietățile sursei de lumină (intensitatea sursei de
lumină, poziția sursei de lumină).

Astfel, intensitatea finală a luminii într-un punct aparținând unei suprafețe este:

float intensitate = emisiva + ambientala + difuza + speculara; # GLSL

În cele ce urmează prezentăm pe scurt ce reprezintă cele 4 componente și cum pot fi calculate.

Componenta emisivă
Aceasta reprezintă lumina emisă de un obiect și nu ține cont de nicio sursă de lumină. O utilizare des
întâlnită pentru componenta emisivă este aceea de a simula obiectele care au strălucire proprie (de ex:
sursele de lumina precum neonul sau televizorul).

Avem astfel:

float emisiva = Ke; # GLSL

Ke – intensitatea emisivă a materialului

Componenta ambientală
Aceasta reprezintă lumina reflectată de către obiectele din scenă de atât de multe ori încât pare să vină de
peste tot.

Astfel, lumina ambientală nu vine dintr-o direcție anume, apărând ca și cum ar veni din toate direcțiile. Din
această cauză, componenta ambientală este independentă de poziția sursei de lumină.

Componenta ambientală depinde de intensitatea de material ambientală a suprafeței obiectului și de


intensitatea luminii.

Similar componentei emisive, componenta ambientală este o constantă (se poate extinde modelul atribuind
fiecărei lumini din scenă o intensitate ambientală).

Avem astfel:

float ambientala = Ka * intensitateAmbientalaGlobala; # GLSL

Ka – constanta de reflexie ambientală a materialului


intensitateAmbientalaGlobala – intensitatea ambientală a luminii

Componenta difuză
Aceasta reprezintă lumina reflectată de suprafața obiectului în mod egal în toate direcțiile.

Cantitatea de lumină reflectată este proporțională cu unghiul de incidență al razei de lumină cu suprafața
obiectului.

Avem astfel: ⃗  ⃗ 
dif uza = Kd ⋅ intensitateLumina ⋅ max(N ⋅ L, 0)

float difuza = Kd * intensitateLumina * max (dot(N,L), 0); # GLSL

Kd - constanta de reflexie difuză a materialului


intensitateLumina – intensitatea luminii
N – normala la suprafață (normalizată)
L – vectorul direcției luminii incidente (normalizat)
⃗  ⃗ 
max(N ⋅ L, 0) – produsul scalar N ⋅ L reprezintă
⃗  ⃗ 
măsura unghiului dintre acești 2 vectori; astfel,
dacă i este mai mare decât π/2 valoarea produsului scalar va fi mai mică decât 0, acest lucru
însemnând că suprafața nu primește lumină ( sursa de lumină se află în spatele suprafeței ) și de aici
și formula care asigură că în acest caz suprafața nu primește lumină difuză

Componenta speculară

Un reflector perfect, de exemplu o oglindă, reflectă lumina numai într-o singură direcție R,
⃗ 
care este
simetrică cu L față
 ⃗
de normala la suprafață. Prin urmare, doar un observator situat exact pe direcția
respectivă va percepe raza reflectată.
Componenta speculară reprezintă lumina reflectată de suprafața obiectului numai în jurul acestei direcții, R.
⃗ 

Acest vector se obține prin:

vec3 R = reflect (-L, N) # GLSL

Este necesar să se utilizeze -L deoarece reflect() are primul parametru vectorul incident care intră
în suprafață, nu cel care iese din ea așa cum este reprezentat în figură

În modelul Phong se aproximează scăderea rapidă a intensității luminii reflectate atunci când α crește prin
(cosα) , unde n este exponentul de reflexie speculară al materialului (shininess).
n

După cum se observă, față de celelalte 3 componente, componenta speculară depinde și de poziția
observatorului. Dacă observatorul nu se află într-o poziție unde poate vedea razele reflectate, atunci nu va
vedea reflexie speculară pentru zona respectivă. De asemenea, nu va vedea reflexie speculară dacă lumina
se află în spatele suprafeței.

Astfel avem: ⃗  ⃗ 
speculara = Ks ⋅ intensitateLumina ⋅ primesteLumina ⋅ (max(V ⋅ R, 0))
n

float speculara = Ks * intensitateLumina * primesteLumina * pow(max(dot(V, R), 0), n) # GLSL

Ks - constanta speculară de reflexie a materialului


V – vectorul direcției de vizualizare (normalizat)
R – vectorul direcției luminii reflectate (normalizat)
n – coeficientul de strălucire (shininess) al materialului
primesteLumina – 1 dacă N ⋅ L este
⃗  ⃗ 
mai mare decât 0; sau 0 în caz contrar

Un alt model de iluminare (Blinn (1977 [https://www.microsoft.com/en-us/research/wp-


content/uploads/1977/01/p192-blinn.pdf])) pentru componenta speculară se bazează pe vectorul median, notat
cu H.
⃗ 
El face unghiuri egale cu L și
⃗ 
cu V . Dacă suprafața ar fi orientată astfel încât normala sa să aibă
⃗ 

direcția lui H , atunci observatorul ar percepe lumina speculară maximă (deoarece ar fi pe direcția razei
⃗ 

reflectate specular).

Termenul care exprimă reflexia speculară este în acest caz: ⃗  ⃗  n


(N ⋅ H )

pow(dot(N, H), n) # GLSL

⃗  ⃗  ⃗ 
H = (L + V ) (normalizat)
Atunci când sursa de lumină și observatorul sunt la infinit, utilizarea termenului N ⋅ H este
⃗  ⃗ 
avantajoasă
deoarece H
⃗ 
este constant.

Ținând cont de toate acestea, avem pentru componenta speculară următoarea formulă:
⃗  ⃗  n
speculara = Ks ⋅ intensitateLumina ⋅ primesteLumina ⋅ (max(N ⋅ H , 0)

float speculara = Ks * intensitateLumina * primesteLumina * pow(max(dot(N, H), 0), n) # GLSL

Atenuarea intensității luminii


Atunci când sursa de lumină punctiformă este suficient de îndepărtată de obiectele scenei vizualizate,
vectorul L⃗  este același în orice punct. Sursa de lumină este numită în acest caz direcțională. Aplicând
modelul pentru vizualizarea a două suprafețe paralele construite din același material, se va obține o aceeași
intensitate (unghiul dintre L⃗  și normală este același pentru cele două suprafețe). Dacă proiecțiile suprafețelor
se suprapun în imagine, atunci ele nu se vor distinge. Această situație apare deoarece în model nu se ține
cont de faptul că intensitatea luminii descrește proporțional cu inversul pătratului distanței de la sursa de
lumină la obiect. Deci, obiectele mai îndepărtate de sursă sunt mai slab luminate. O posibilă corecție a
modelului, care poate fi aplicată pentru surse poziționale (la distanță finită de scenă) este:

float intensitate = emisiva + ambientala + factorAtenuare * ( difuza + speculara ); # GLSL

factorAtenuare = 1/d
2
este o funcție de atenuare
d este distanța de la sursă la punctul de pe suprafață considerat

Corecția de mai sus nu satisface cazurile în care sursa este foarte îndepărtată. De asemenea, dacă sursa
este la distanță foarte mică de scenă, intensitățile obținute pentru două suprafețe cu același unghi i , între L
⃗ 

și N,
⃗ 
vor fi mult diferite.

O aproximare mai bună este următoarea: factorAtenuare = 2


1/(Kc + Kl ⋅ d + Kq ⋅ d )

Kc - factorul de atenuare constant


Kl - factorul de atenuare liniar
Kq - factorul de atenuare patratic

Pentru a include în final culoarea de material a obiectului și culoarea luminii (care alternativ pot fi incluse și
în formulele de mai sus) se folosește:

vec3 culoare = culoareObiect * (emisiva + culoareLumina * (ambientala + factorAtenuare * ( difuza + speculara ))); # GLSL

Modele de shading
De asemenea, există mai multe modele de shading, care specifică metoda de implementare a modelului de
calcul al reflexiei luminii. Mai exact, modelul de shading specifică unde se evaluează modelul de reflexie.
Dacă vrem să calculăm iluminarea pentru o suprafață poligonală:

în modelul de shading Lambert, se calculează o singură culoare pentru un poligon al suprafeței


în modelul de shading Gouraud (1971 [https://collections.lib.utah.edu/pdfjs/web/viewer.html?
v=1&file=/dl_files/3b/70/3b70218f4236a783b37dbb283cf29c18e7842c7d.pdf]), se calculează câte o culoare
pentru fiecare vârf al unui poligon. Apoi, culorile fragmentelor poligonului se calculează prin
interpolare între vârfuri (interpolarea liniară a culorilor vârfurilor, pentru fragmentele de pe laturi și
interpolare liniară între culorile capetelor fiecărui segment interior, pentru fragmentele interioare
poligonului). Calcularea culorilor vârfurilor se poate efectua în vertex shader.
în modelul de shading Phong (1975
[http://www.cs.northwestern.edu/~ago820/cs395/Papers/Phong_1975.pdf]), se calculează câte o normală
pentru fiecare vârf al unui poligon. Apoi, pentru fiecare fragment se determină o normală prin
interpolare între normalele din vârfuri. Astfel, se calculează o culoare pentru fiecare fragment al unui
poligon (în fragment shader)

Figura 1. Diferite modele de shading: Lambert (o culoare per primitivă), Gouraud (o culoare per vârf), Phong
(o culoare per fragment)

În acest laborator se va discuta modelul de shading Gouraud.

Detalii de implementare
Pentru simplitate, în cadrul laboratorului vom implementa modelul de shading Gouraud (în vertex shader):

Se vor calcula practic doar componentele difuze și speculare așa cum au fost prezentate anterior;
componenta emisivă nu va fi folosită iar calculul componentei ambientale va fi simplificat astfel încât
să nu mai trebuiască trimis nimic din program către shader (mai multe detalii la punctul 3).
Vom folosi ca proprietăți de material pentru obiecte doar intensitatea de material difuză și speculară
(transmise din program către shader) : Ks și Kd.
În shader vom aproxima lumina ambientală cu o intensitateAmbientalaGlobala care va fi o
constantă în shader, iar în loc de Ka (constanta de material ambientală a obiectului) vom folosi Kd
(constanta de material difuză a obiectului).
Intensitatea luminii va fi 1 și nu va mai fi necesar să fie folosită la înmulțirile din formulele de calcul
pentru componentele difuză și speculară.
Calculele de iluminare se vor face în world space, deci înainte de a fi folosite, poziția și normala vor
trebui aduse din object space în world space. Acest lucru se poate face astfel:
pentru poziție:

vec3 world_pos = (model_matrix * vec4(v_position,1)).xyz;

pentru normală:

vec3 world_normal = normalize( mat3(model_matrix) * v_normal );

Vectorul direcției luminii L:

vec3 L = normalize( light_position - world_pos );

Vectorul direcției din care priveste observatorul V:

vec3 V = normalize( eye_position - world_pos );

Vectorul median H:

vec3 H = normalize( L + V );
Funcții GLSL utile care pot fi folosite pentru implementarea modelului de iluminare

normalize(V) – normalizează vectorul V


normalize(V1+V2) – normalizează vectorul obținut prin V1+V2
normalize(P1-P2) - returnează un vector de direcție normalizat între punctele P1 și P2
dot(V1,V2) – calculează produsul scalar dintre V1 și V2
pow(a, shininess) – calculează a la puterea shininess
max(a,b) – returnează maximul dintre a și b
distance(P1,P2) – returnează distanța euclidiană dintre punctele P1 și P2
reflect(V,N) - calculează vectorul de reflexie pornind de la incidenta V și normala N

Cerințe laborator
Tasta F5 - reîncarcă programele shader în timpul execuției aplicației. Nu este nevoie să opriți aplicația
întrucât un program shader este compilat și executat de către procesorul grafic și nu are legătură cu codul
sursă C++ propriu-zis.

1. Descărcați framework-ul de laborator [https://github.com/UPB-Graphics/Framework-EGC/archive/master.zip]


2. Completați funcția RenderSimpleMesh astfel încât să trimiteți corect valorile uniforme către Shader:
poziția luminii
poziția camerei
proprietățile de material (Kd, Ks, shininess, culoare obiect)
3. Implementați iluminarea în Vertex Shader
Vectorii N, V, L și poziția în spațiul global
Componenta ambientală
Componenta difuză
Componenta speculară (atât în varianta de bază cât și folosind vectorul median)
Factor de atenuare
Culoarea finală
4. Completați fragment shader-ul astfel încât să aplicați iluminarea calculată în Vertex Shader
5. Colorați sfera și planul din scenă (de ex: sfera - albastru, planul - gri)

egc/laboratoare/07.txt · Last modified: 2020/11/27 14:26 by victor.asavei

S-ar putea să vă placă și