Aplicațiile grafice în timp real efectuează o desenare succesivă a câte unui cadru. Între 2 cadre succesive, parametrii de desenare, precum poziția și direcția de vizualizare a observatorului, transformările geometriei desenate sau alte elemente ce influențează desenarea, cum sunt informațiile unei surse de lumină, pot să difere. Utilizarea unui număr mare de cadre desenate pe secundă creează iluzia de animație continuă :) .
În framework-ul pus la dispozitie în cadrul acestui laborator, succesiunea de cadre se realizează în clasa world
, în metoda Run
, printr-o buclă care se oprește doar în momentul în care se închide fereastra.
while (!window->ShouldClose()) { LoopUpdate(); }
În interiorul acestei bucle, în metoda LoopUpdate
, se realizează următorul proces pentru fiecare cadru:
// 1. Polls and buffers the events window->PollEvents(); // 2. Computes frame deltaTime in seconds ComputeFrameDeltaTime(); // 3. Calls the methods of the instance of InputController in the following order // OnWindowResize, OnMouseMove, OnMouseBtnPress, OnMouseBtnRelease, OnMouseScroll, OnKeyPress, OnMouseScroll, OnInputUpdate // OnInputUpdate will be called each frame, the other functions are called only if an event is registered window->UpdateObservers(); // 4. Frame processing FrameStart(); Update(static_cast<float>(deltaTime)); FrameEnd(); // 5. Swap front and back buffers - image will be displayed to the screen window->SwapBuffers();
Pașii din codul de mai sus sunt:
deltaTime
.LabX
și sunt discutate mai jos.LabX
.Interacțiunea utilizatorului cu fereastra poate fi apăsarea unei taste de la tastatură, apăsarea unui buton de la mouse, redimensionarea ferestrei sau închiderea ei.
Interacțiunea utilizatorului cu tastele, în situația în care fereastra este selectată, este de 3 feluri:
Interacțiunea utilizatorului cu butoanele de la mouse este de 3 feluri, similar ca în situația tastelor, descrisă mai sus.
În framework-ul de laborator, interacțiunea cu utilizatorul se realizează în metodele OnWindowResize
, OnMouseMove
, OnMouseBtnPress
, OnMouseBtnRelease
, OnMouseScroll
, OnKeyPress
și OnMouseScroll
, ce sunt apelate pentru fiecare eveniment realizat într-un cadru. De exemplu:
void Lab4::OnKeyPress(int key, int mods) { if (key == GLFW_KEY_R) { printf("S-a apasat tasta R."); } }
Metoda OnKeyPress
se apelează pentru fiecare tastă apăsată într-un cadru. Trebuie verificat în interiorul metodei, pe baza parametrilor, ce tastă a fost apasată. Pentru mai multe informații despre fiecare metodă în parte, vă rog să citiți detaliile din fișierul header al clasei InputController.
Metoda OnInputUpdate
are un statut special în cadrul framework-ului de laborator și este similară cu metoda Update
, mai exact este apelată în fiecare cadru exact o dată. Apelul ei se realizează în cadrul pasului 3 de mai sus, astfel că se apelează înainte de metoda Update
. Recomandarea este să utilizați această metodă când gestionați interacțiunea cu utilizatorul.
Suplimentar acestor metode specifice tratării interacțiunii cu utilizatorul, se poate folosi atributul window
, intern clasei, pentru a verifica existența anumitor evenimente:
if(window->KeyHold(GLFW_KEY_R) { printf("Tasta R este apasata."); } if (window->MouseHold(GLFW_MOUSE_BUTTON_1)) { printf("Butonul stanga de la mouse este apasat."); }
Modificarea proprietăților unui obiect între două cadre succesive trebuie realizată pe baza timpului care a trecut între cele două cadre, respectiv timpul trecut pentru desenarea cadrului anterior. Mai exact, în situația în care dorim să modificăm poziția unui obiect cu 5 unități de spațiu pe secundă, de-a lungul axei Z, în sens pozitiv, putem aplica următorul proces:
object_position.z += 5 * 0.016;
Valoarea 0.016 reprezintă timpul mediu de desenare a unui cadru la o frecvență de 60 de cadre pe secundă. Într-o secundă, poziția obiectului se va deplasa cu 5 unități de-a lungul axei Z, în sens pozitiv. Pentru a nu folosi direct valoarea aceasta, putem utiliza valoarea deltaTime
, ce reprezintă timpul de desenare a cadrului precedent și este primită sub formă parametru în metodele Update
și OnInputUpdate
.
object_position.z += 5 * deltaTime;
O aplicație grafică în timp real utilizează procesorul grafic pentru a accelera desenarea unui cadru și a obține o frecvență mare de desenare a cadrelor. Deoarece procesoarele grafice prezintă diferențe mari unele față de celelalte, pentru a se evita implementarea desenării specific pentru fiecare tip de procesor grafic, au fost introduse mai multe standarde de programare a procesorului. Aceste standarde sunt implementate în interiorul driver-ului video. O privire de ansamblu a comunicării între o aplicație grafică în timp real și procesorul grafic este prezentată în imaginea de mai jos. Aceste standarde poartă numele de API-uri grafice și pentru a enumera doar cateva: OpenGL, Direct3D sau Metal.
Toate API-urile realizează în mare aceleași operații standard în cadrul implementării hardware din procesorul grafic pentru realizarea prelucrărilor în banda grafică. Diferența este dată de forma de transmitere a parametrilor pentru aceste operații. În cadrul acestui laborator, noi vom utiliza API-ul grafic OpenGL pentru programarea prelucrărilor în banda grafică.
Primul pas realizat la începutul unui cadru este curățarea grilei de pixeli și a grilei de valori de adâncime desenate la cadrul anterior. Acest proces se realizează în API-ul grafic OpenGL prin directiva:
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
Informația din fiecare celulă a grilei de valori de adâncime se suprascrie cu valoarea 1. Aceasta reprezintă valoarea componentei z a feței din spate pentru volumul de decupare. Culoarea cu care se suprascrie fiecare celulă din grila de pixeli se poate stabili prin:
glClearColor(0, 0, 0, 1);
Se poate stabili zona din ecran, poarta de afișare, în care să se deseneze obiectele prin directiva:
glViewport(0, 0, 1280, 720);
Un model 3D, cunoscut în limba engleză sub numele de 3D mesh, este un obiect tridimensional definit prin vârfuri și indici. În laborator aveți posibilitatea să încărcați modele 3D în aproape orice format posibil prin intermediul clasei Mesh.
Un vertex buffer object reprezintă un container în care stocăm date ce țin de conținutul vârfurilor precum:
Un vertex buffer object se poate crea prin comanda OpenGL glGenBuffers:
unsigned int 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
VBO_ID
.
Pentru a distruge un VBO și astfel să eliberăm memoria de pe GPU se folosește comanda glDeleteBuffers:
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:
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:
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices[0]) * vertices.size(), &vertices[0], GL_STATIC_DRAW);
&vertices[0]
, și copiază în memoria video dimensiunea specificată prin parametrul al 2-lea.
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.
Î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:
unsigned int VAO; glGenVertexArrays(1, &VAO);
Este legat cu glBindVertexArray:
glBindVertexArray(VAO);
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.
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 ș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 (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);
În mod normal face-culling este dezactivat. Acesta poate fi activat folosind comanda glEnable:
glEnable(GL_CULL_FACE);
Pentru a dezactiva face-culling se folosește comanda glDisable:
glDisable(GL_CULL_FACE);
Pentru a specifica ce orientare a fețelor să fie ignorată se folosește comanda glCullFace
// GL_FRONT, GL_BACK, and GL_FRONT_AND_BACK are accepted. // The initial value is GL_BACK. glCullFace(GL_BACK);
deltaTime
.
CreateMesh()
astfel încât să încărcați geometria în memoria RAM a procesorului grafic.Update()
, directiva de specificare a culorii de curățare a grilei de pixeli.DrawObjects()
, directivele de activare și dezactivare a optimizării Face culling.DrawObjects()
directiva de specificare a tipului de față pentru care triunghiurile se elimina din procesul de rasterizare.DrawObjects()
, directiva de specificare a poziției și dimensiunii porții de afișare.Update()
, desenați de 4 ori obiectele. Pentru fiecare desenare a obiectelor, utilizați una dintre cele 4 camere setate cu poziții și direcții de vizualizare predefinite.