This is an old revision of the document!


Laboratorul 04

Pentru rezolvarea cerințelor din acest laborator, aveți nevoie de codul utilizat în rezolvarea cerințelor din cadrul laboratorului 3. În situatia în care nu ați rezolvat acest laboratorul 3, va trebui să le realizați mai întâi pe el și ulterior să reveniți la cerințele celui curent.

Reamintire!!! Puteți prezenta rezolvările cerințelor de până la 2 laboratoare, în fiecare săptămână. De exemplu, puteți prezenta laboratorul curent și pe cel din săptămâna anterioară, în totalitate sau parțial, inclusiv punctajul pentru cerința bonus :) .

Pentru rezolvarea cerințelor din cadrul acestui labroator:

  1. Descărcați framwork-ul de laborator și copiați, din arhiva descărcată, directorul Lab4, în interiorul directorului gfx-framework-ppbg\src\lab din versiunea voastră de proiect.
  2. Adăugați în fișierul lab_list.h, linia #include “lab/lab4/lab4.h”.
  3. Folosiți din nou utilitarul CMake pentru a regenera proiectul. Pentru a vă reaminti procesul de realizare a setup-ului, puteți să reconsultați pagina dedicată acestui lucru.

API-ul grafic OpenGL

Stergerea ecranului

Primul pas realizat la inceputul unui cadru este curatarea grilei de pixeli si a grilei de valori de adancime desenate la cadrul anterior. Acest proces se realizeaza in API-ul grafic OpenGL prin directiva:

glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

Grila de valori de adancime se suprascrie cu valoarea 1, componenta z a fetei din spate pentru volumul de decupare. Culoarea cu care se suprascriu valorile in grila de pixeli se poate stabili prin:

glClearColor(0, 0, 0, 1);

Poarta de afisare

Se poate stabili zona din ecran, poarta de afisare, in care sa se deseneze obiectele prin directiva:

glViewport(0, 0, 1280, 720);

Modele 3D

Un model 3D, cunoscut in limba engleza 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.

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:

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

Așa cum se poate vedea și din explicația API-ului, funcția glGenBuffers 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:

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);
  • 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

Pentru a înțelege mai bine API-ul OpenGL vă recomandă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

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:

unsigned int VAO;
glGenVertexArrays(1, &VAO);

Este legat cu glBindVertexArray:

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.

Optiunea de optimizare 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 ș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​);

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:

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);

Aplicatiile grafice in timp real

Aplicatiile grafice in timp real realizeaza o desenare succesiva a cate unui cadru. Intre 2 cadre succesive, parametrii de desenare, precum pozitia si directia de vizualizare a observatorului, pozitia geometriei desenate sau alte elemente ce influenteaza desenare, cum este informatii unei surse de lumina, pot sa difere. Un numar mare de cadre desenate pe secunda, creeaza iluzia de animatie continua.

In framework-ul pus la dispozitie in cadrul acestui laborator, aceasta succesiune de cadre se realizeaza in clasa world in metoda Run printr-o bucla care se opreste doar in momentul in care se inchide fereastra.

while (!window->ShouldClose())
{
    LoopUpdate();
}

Analiza unui cadru

In interiorul acestei bucle, in interiorul metodei LoopUpdate se realizeaza urmatorul 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();

Pasii din codul de mai sus sunt:

  1. Fereastra in interfata grafica a sistemului de operare este creata prin intermediul bibliotecii GLFW. Tot prin aceasta biblioteca obtinem evenimente externe aplicatiei care se pot produce la nivelul ferestrei, precum apasarea de catre utilizator a unei taste sau a unui buton de la mouse, redimensionarea ferestrei sau inchiderea ei. Vom reveni asupra acestor evenimente mai jos.
  2. Se calculeaza timpul de desenare a cadrului anterior, denumit deltaTime.
  3. In framework-ul de laborator exista metode in clasele LabX pentru a trata fiecare tip de eveniment. Acestea sunt discutate mai jos.
  4. Metode ce se apeleaza exast o data per cadru.
  5. Se blocheaza procesul curent de pe CPU pentru a se astepta incheierea tuturor proceselor de desenare realizate de catre procesorul grafic.

Interactiunea cu utilizatorul

Interactiunea utilizatorului cu fereastra este realizata

Interactiunea utilizatorului cu tastele de la tastatura, in situatia in care fereastra este selectate. Aceasta interactiune poate fi de 3 feluri:

  1. Apasarea unei taste pentru prima data in cadrul curent, cunoscuta in limba engleza sub numele de key press
  2. Neapasarea unei taste la cadrul curent, in situatia in care tasta a fost apasata la cadrul anterior, cunoscuta in limba engleza sub numele de key release
  3. Apasarea unei taste la cadrul curent, indiferent cand a fost apasata prima data, cunoscuta in limba engleza sub numele de key hold

Interactiunea utilizatorului cu butoanele si rotita de la mouse. Pentru butoanele de la mouse, interactiunea poate fi de 3 feluri, similar ca in situatia tastelor.

In framework-ul de laborator, interactiunea cu utilizatorul se realizeaza in metodele OnWindowResize, OnMouseMove, OnMouseBtnPress, OnMouseBtnRelease, OnMouseScroll, OnKeyPress, OnMouseScroll, ce sunt apelate pentru fiecare eveniment realizat intr-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 apeleaza pentru fiecare tasta apasata intr-un cadru. Trebuie verificat in interiorul metodei, pe baza parametrilor ce tasta a fost apasata. Pentru mai multe informatii despre fiecare metoda in parte, va rog sa cititi detaliile din fisierul header al clasei InputController.

Apelul fiecarei metode se realizeaza in cadrul pasului 3 descris mai sus.

Metoda OnInputUpdate are un statut special in cadrul framework-ului de laborator si este similara cu metoda Update, mai exact este apelata in fiecare cadru exact o data. Apelul ei se realizat in cadrul pasului 3 de mai sus, astfel ca se apeleaza inainte de metoda Update. Recomandarea este sa utilizati aceasta metoda cand gestionati interactiunea cu utilizatorul.

Suplimentar acestor metode specifice tratarii interactiunii cu utilizatorul, se poate folosi atributul window, intern clasei, pentru a verifica existenta 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.");
}

Animatii independente de numarul de cadre desenate pe secunda

Modificarea proprietatilor unui obiect intre 2 cadre succesive trebuie realizata pe baza timpului care a trecut intre cele 2 cadre, respectiv timpul trecut pentru desenarea cadrului anterior. Mai exact, in situatia in care dorim sa modificam pozitia unui obiect cu 5 unitati de spatiu pe secunda, de-alungul axei Z, in sens pozitiv, putem aplica urmatorul proces:

object_position.z += 5 * 0.016;

Valoarea 0.016 reprezinta timpul mediu de desenare a unui cadru la o frecventa de 60 de cadre pe secunda. Intr-o secunda, pozitia obiectului se va deplasa cu 5 unitati de-alungul axei Z, in sens pozitiv. Pentru a nu folosi direct valoarea aceasta, putem utiliza valoarea deltaTime, ce reprezinta timpul de desenare a cadrului precedent si este primita ca parametru in metodele Update si OnInputUpdate.

object_position.z += 5 * deltaTime;

Cerinte laborator

Pentru toate cerintele in care se precizeaza ca animatiile trebuie sa fie continue, utilizati valoarea deltaTime.

  1. 0.05p - Completați metoda CreateMesh() astfel încât să încărcați geometria in memoria RAM a procesorului grafic.
    • 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
    • Dupa completarea corecta a metodei, rezultatul vizual ar trebui sa fie urmatorul:
  2. 0.05p - La apasarea unei taste, alegeti o culoarea aleatoare pentru curatarea grilei de pixeli
    • Utilizati in metoda Update() directiva de specificare a culorii curatare a informatiei de pe ecran.
    • La apasarea tastei R, alegeti o culoare aleatoare
  3. 0.05p - La apasarea unei taste,
    • Utilizati in metoda DrawObjects() directivere de activare si dezactivare a optimizarii Face culling
    • Utilizati in metoda DrawObjects() directiva de specificare a tipului de fata pentru care triunghiurile se elimina din procesul de rasterizare
    • La apasarea tastei F, schimbati intre eliminarea triunghiurilor pentru care se afiseaza fateta fata sau spate
  4. 0.05p - Pentru cele 3 cuburi din scena aplicati urmatoarele animatii:
    • Unul dintre cuburi sa se deplaseze continuu sus-jos intre limitele pe axa y: 0 si 3
    • Un alt cub sa se roteasca continuu fata de una dintre cele 3 axe principale
    • Un alt cub sa pulseze continuu intre scarile 0.5 si 2
  5. 0.05p - Desenati un alt cub pe care sa il deplasati prin spatiu la apăsarea tastelor W, A, S, D, E, Q (pozitiv și negativ pe toate cele 3 axe).
  6. 0.05p - Desenati obiectele de 4 ori in 4 ferestre de afisare diferite, conform imaginii de mai jos. Pastrati proportiile precizate in imagine.
    • Utilizati in metoda DrawObjects() directiva de specificare a pozitiei si dimensiunii portii de afisare.
    • In metoda Update() desenati de 4 ori obiectele. Trimiteti la desenare cele 4 camere setate cu pozitii si directii de vizualizare predefinite.

ppbg/laboratoare/04.1698867828.txt.gz · Last modified: 2023/11/01 21:43 by andrei.lambru
CC Attribution-Share Alike 3.0 Unported
www.chimeric.de Valid CSS Driven by DokuWiki do yourself a favour and use a real browser - get firefox!! Recent changes RSS feed Valid XHTML 1.0