Documente Academic
Documente Profesional
Documente Cultură
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
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.
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).
Mai multe detalii despre modelele de interpolare se pot gasi accesind urmatoarele resurse:
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
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:
Astfel, punctul P se afla in conul de lumina (primeste lumina) daca conditia urmatoare este
indepilita:
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.
// 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.
[Bonus]
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.
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:
Dupa ce avem pixelii imaginii incarcate putem genera un obiect de tip textura de OpenGL folosind comanda:
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);
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());
glActiveTexture(GL_TEXTURE1);
glBindTexture(GL_TEXTURE_2D, texture2->GetTextureID());
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
in vec2 texcoord;
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:
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);
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
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.
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 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.
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
// 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
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.
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:
Pe coordonatele .zw vor veni coordonatele de textură care vor fi trimise mai departe în TexCoords
Fragment shader
in vec2 TexCoords;
out vec4 color;
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
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:
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:
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
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:
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]
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.
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:
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++
Pentru cei mai puțin familiarizați cu limbajul C++ recomandăm să parcurgeți tutoriale: Learn
C++ [http://www.learncpp.com/]
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.
Laboratorul 1
Framework
Informații laborator
În cadrul laboratorului 1 puteți încărca modele 3D în cadrul scenei și cere afișarea scenei
utilizând funcția
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
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:
Control aplicație
Programul rulat oferă posibilitatea vizualizării scenei create prin intermediul unei camere
predefinite.
Cerințe laborator
OpenGL – Date
Dacă am încerca să reducem întregul API de OpenGL la mari concepte acestea ar fi:
date
stări
shadere
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ă
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.
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.
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):
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);
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].
poziție
normală
culoare
coordonate de texturare
etc…
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]:
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/]
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.
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.
Î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 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);
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.
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
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
⎣ ⎦ ⎣ ⎦⎣ ⎦
1 0 0 1 1
Scalarea relativă la un punct oarecare se rezolvă similar cu rotația relativă la un punct oarecare.
Din această cauză, este convenabil ca matricile să fie scrise manual în forma aceasta:
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
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);
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.
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.
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
tx = xpmin − sx ∗ xf min
ty = ypmin − sy ∗ yf min
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
xp sx 0 tx xf
⎡ ⎤ ⎡ ⎤⎡ ⎤
⎢ yp ⎥ = ⎢ 0 sy ty ⎥ ⎢ yf ⎥
⎣ ⎦ ⎣ ⎦⎣ ⎦
1 0 0 1 1
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
Î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:
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ă:
Descriere laborator
În cadrul acestui laborator aveți de programat în două clase:
sau
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
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
⎣ ⎦ ⎣ ⎦⎣ ⎦
1 0 0 0 1 1
Rotația relativă la o axă paralelă cu axa OX se rezolvă în cel mai simplu mod
prin:
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
⎣ ⎦ ⎣ ⎦⎣ ⎦
1 0 0 0 1 1
Dacă sx = sy = sz atunci avem scalare uniformă, altfel avem scalare
neuniformă.
Scalarea relativă la un punct oarecare se rezolvă în cel mai simplu mod prin:
Din această cauză, este convenabil ca matricile să fie scrise manual în forma
aceasta:
modelMatrix = glm::mat4(1);
modelMatrix *= Transform2D::Translate(1, 2, 1);
RenderMesh(meshes["box"], modelMatrix);
Cerințe laborator
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.
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’.
Î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ă.
„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:
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.
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.
Î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:
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
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)
Dacă vrem să rotim vectorul “forward” în jurul axei OY globale atunci facem astfel:
După ce ați făcut calculele de rotație aveți grijă să păstrați vectorii normalizați
În laborator aveți variabila distanceToTarget care reține distanța până la punctul față de care rotim
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
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).
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.
#version 330
// Uniform properties
uniform mat4 Model;
uniform mat4 View;
uniform mat4 Projection;
void main()
{
gl_Position = Projection * View * Model * vec4(v_position, 1.0);
}
#version 330
void main()
{
out_color = vec4(1, 0, 0, 0);
}
glEnableVertexAttribArray(2);
glVertexAttribPointer(2, 3, GL_FLOAT, GL_FALSE, sizeof(VertexFormat), (void*)0);
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].
//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);
Vertex Shader:
#version 330 // GLSL version of shader (GLSL 330 means OpenGL 3.3 API)
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)
Fragment Shader:
#version 410
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.
frag_color = vertex_color;
// same for other attributes
d. Se calculeaza pozitia in clip space a vertexului primit folosind matricile
Model, View, Projection
in vec3 frag_color;
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).
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:
Î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:
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ă.
Similar componentei emisive, componenta ambientală este o constantă (se poate extinde modelul atribuind
fiecărei lumini din scenă o intensitate ambientală).
Avem astfel:
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)
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.
⃗
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
direcția lui H , atunci observatorul ar percepe lumina speculară maximă (deoarece ar fi pe direcția razei
⃗
reflectate specular).
⃗ ⃗ ⃗
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)
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.
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ă:
Figura 1. Diferite modele de shading: Lambert (o culoare per primitivă), Gouraud (o culoare per vârf), Phong
(o culoare per fragment)
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:
pentru normală:
Vectorul median H:
vec3 H = normalize( L + V );
Funcții GLSL utile care pot fi folosite pentru implementarea modelului de iluminare
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.