Laboratorul 03

Obiecte de tip framebuffer și umbre

Introducere

În acest laborator, vom introduce atât elemente noi de OpenGL, cât și o abordare pentru calcularea umbrelor realizate de iluminarea unei surse de lumină de tip spot. Metoda prezentată aici se numeste metoda mapării umbrelor, întâlnită în engleză sub numele de shadow mapping, ce a fost prezentată în cadrul cursului de EGC din anul III. Va recomandăm să revizualizați secțiunea despre texturi din pagina de Recapitulare EGC. Această recomandare reapare mai jos în pagină, acolo unde noțiunile de la EGC sunt explicit necesare.

Prima parte a laboratorului se concentrează doar pe descrierea obiectelor de tip framebuffer. Partea a doua reia sumar metoda mapării umbrelor și oferă mai multe detalii doar despre pașii tehnicii, ce țin de utilizarea obiectelor de tip framebuffer.

Obiecte de tip framebuffer

Redarea scenei în fereastra de desenare se realizează, de fapt prin redarea scenei într-o textură specială, ce este afișată ulterior în fereastră. API-ul grafic OpenGL nu permite desenarea direct într-o textură, ci impune utilizarea unui obiect suplimentar, numit buffer de cadru sau framebuffer. Acest obiect conține:

  • Texturile cu format de culoare în care se redă scena. Pot să fie mai multe texturi pe care se desenează, până la un număr limită dat de procesorul grafic, care este în general 8. În laboratorul 6 o să vedem aplicații pentru care este necesară desenarea în mai multe texturi. Putem să ne gândim la o textură ca la o structură de date în care păstrăm informație oarecare, nu doar culoare. De exemplu: putem păstra poziția în spațiul lume a fragmentului, obținută prin interpolare între vârfuri, sau vectorul normal în spațiul lume al fragmentului, obținut prin același proces de interpolare.
  • Textura în care se păstrează informația de adâncime a fragmentelor desenate în texturile cu format de culoare. Această informație este utilizată în pasul de test de adâncime din procesul de rasterizare.

Crearea obiectelor de tip framebuffer

Pentru a crea un obiect de tip framebuffer, putem folosi directiva OpenGL:

unsigned int framebuffer_object;
 
glGenFramebuffers(1, &framebuffer_object);

Fereastra de desenare deține un framebuffer implicit, ce este creat automat în framework-ul de laborator prin intermediul bibliotecii GLFW. Astfel, orice redare a scenei se realizează inițial în texturile acestui framebuffer. Pentru a desena în texturile obiectului de tip framebuffer creat de noi mai sus sau pentru a modifica acest obiect, este necesară legarea acestuia la banda grafică după cum urmează:

glBindFramebuffer(GL_FRAMEBUFFER, framebuffer_object);

Crearea și atașarea texturilor la un framebuffer

În momentul de față avem un framebuffer nou, gol, ce nu conține nicio textură de culoare sau de adâncime, dar care este legat la banda grafică. Putem lega texturi la framebuffer și atunci când vom reda o scenă și acest framebuffer va fi legat la banda grafică, procesul de desenare va scrie rezultatele în texturile obiectului legat de noi.

Reamintim că pentru a crea o textură cu format de culoare, folosim următoarele directive:

unsigned int color_texture;
 
glGenTextures(1, &color_texture);
glBindTexture(GL_TEXTURE_2D, color_texture);
 
// Pixelii din interiorul texturii au formatul RGB
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, NULL);

Pentru a revizualiza mai multe detalii despre gestionarea texturilor în API-ul grafic OpenGL, puteți consulta secțiunea despre texturi din pagina de Recapitulare EGC.

Pentru a atașa textura cu format de culoare (R, RG, RGB, RGBA), creată mai sus, la framebuffer, folosim:

glFramebufferTexture(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0+pct_atasare, color_texture, 0);

Astfel, atașăm textura color_texture la obiectul de tip framebuffer legat la banda grafică pe punctul de atașare pct_atasare. Valoarea 0 de la final ne spune că atașăm primul nivel din mipmap (rezoluția maximă). Dupa cum se poate observa, obiectele de tip framebuffer au puncte de atașare ce sunt foarte similare din punct de vedere conceptual cu ideea de pipe, folosită la definirea informației la nivel de vertex. În API-ul grafic OpenGL, acest tip de proiectare este foarte des folosit. Dacă atașăm o textură la un punct de legare pe care deja este legată o altă textură, legătura veche se va pierde și va rămâne doar cea nouă.

Este important să observăm mai sus că punctul de atașare este de tip GL_COLOR_ATTACHMENT. Mai există un alt tip de punct de atașare, cu un singur punct (unic!) folosit pentru textura de adâncime, numit GL_DEPTH_ATTACHEMENT. Pentru a lega o textură de adâncime, folosim:

glFramebufferTexture(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, depth_texture, 0);

Procesul de creare a unei texturi ce conține informație de adâncime este similar cu cel de creare a unei texturi ce conține informație de culoare, descris mai sus, cu excepția formatului definit în directiva glTexImage2D:

glTexImage2D(GL_TEXTURE_2D, 0, GL_DEPTH_COMPONENT32F, width, height, 0, GL_DEPTH_COMPONENT, GL_UNSIGNED_BYTE, 0);

Specificarea texturilor de desenare

Putem atașa mai multe texturi la un framebuffer, dar API-ul grafic OpenGL ne dă posibilitatea să alegem pe care vrem să le folosim la un pas de desenare. Putem să nu utilizăm unele din texturile unui framebuffer, chiar dacă ele sunt atașate la framebuffer.

Pentru a seta texturile care dorim să fie utilizate în procesul de desenare, folosim:

std::vector<GLenum> draw_textures;
 
draw_textures.push_back(GL_COLOR_ATTACHMENT0+attachment_index_color_texture);
 
glDrawBuffers(draw_textures.size(), &draw_textures[0]);

Pratic, cu directiva glDrawBuffers, setăm care sunt texturile în care se desenează. În exemplul de mai sus, avem o singură textură atașată pe atașamentul de culoare cu numărul 0, pe care o adaugăm într-un vector pe poziția 0.  Dacă avem obiectul de tip framebuffer în cauză legat la banda grafică și în fragment shader-ul utilizat pentru desenarea în acest framebuffer, avem codul de mai jos, deoarece în out_color scriem un pixel roșu, toți pixelii texturii de culoare vor fi roșii.

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

După cum s-a menționat mai sus, putem avea mai multe texturi cu format de culoare atașate unui framebuffer. Acest mecanism va fi prezentat în detaliu în laboratorul 6. Dar, ca o previzualizare a informației din acel laborator, codul de mai jos scrie în 4 texturi diferite, de tipuri diferite și se poate observa că textura din atașamentul de culoare numărul 0 este complet roșie, iar cea din atașamentul de culoare numărul 1 este complet albastră.

layout(location = 0) out vec4 out_color;
layout(location = 1) out vec3 color2;
layout(location = 2) out int int_texture;
layout(location = 3) out float float_texture;
 
void main()
{
    out_color = vec4(1, 0, 0, 1);
    color2 = vec3(0, 0, 1);
    int_texture = 1;
    float_texture = 3.14;
}

În acest exemplu, pe atașamentul de culoare numărul 0 merge ce e scris în out_color, pe atașamentul de culoare cu numărul 1 merge ce este scris în color2, pe atașamentul de culoare cu numărul 2 merge ce este scris în int_texture, iar pe atașamentul de culoare cu numărul 3 merge ce este scris în float_texture.

Verificarea statusului creării unui framebuffer

Ultima etapă necesară, înainte de folosirea obiectului de tip framebuffer, este testarea corectitudinii creării sale:

glCheckFramebufferStatus(GL_FRAMEBUFFER);

Utilizarea obiectelor de tip framebuffer

Așa cum s-a menționat mai sus, pentru redarea scenei în texturile unui obiect de tip framebuffer, este necesară legarea acestui obiect înainte de desenare:

glBindFramebuffer(GL_FRAMEBUFFER, framebuffer_object);

În general, înainte de desenarea într-un framebuffer, vrem să curățăm texturile cu format de culoare și de adâncime. Valoarea implicită de curățare este culoarea neagră pentru texturile cu format de culoare și valoarea 1 pentru texturile cu format de adâncime:

glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

Trebuie specificată și poarta de vizualizare în care vrem să desenăm în texturile obiectului de tip framebuffer:

glViewport(0, 0, width, height);

În situația în care dorim sa desenăm în toată textura, width și height de mai sus reprezintă rezoluția texturilor din framebuffer.

În momentul în care se dorește din nou redarea scenei în texturile obiectului de tip framebuffer implicit, putem folosi:

glBindFramebuffer(GL_FRAMEBUFFER, 0);

Metoda mapării umbrelor

Noi vom utiliza metoda mapării umbrelor pentru a obține efectul de umbre ale iluminării realizate de către o sursă de lumină de tip spot. Pentru a revizualiza mai multe detalii despre acest tip de sursă de lumină, vă rugam să consultați secțiunea aferentă din pagina de Recapitulare EGC.

Metoda conține 2 pași:

  1. Redarea scenei într-un framebuffer nou. Această desenare se realizează din poziția sursei de lumină, pe direcția de iluminare a sursei, specifică tipului de sursă spot. Practic, dorim să vedem ceea ce “vede” sursa de lumină. Pentru simplitate, în laboratorul acesta, sursa va avea un unghi de iluminare de 90 de grade, motiv pentru care se va folosi o proiecție perspectivă cu un unghi de vizualizare atât vertical cât și orizontal de 90 de grade. Texturile obiectului de tip framebuffer, obținute în urma desenării, conțin toate punctele din scenă ce sunt iluminate de către sursa de lumină.
  2. Redarea scenei în texturile obiectului de tip framebuffer implicit din perspectiva observatorului. În această desenare, se folosește textura cu format de adâncime obținută la pasul anterior. În fragment shader, fiecare fragment se verifică daca este iluminat de către sursa de lumină sau nu. Dacă pozitia în spațiul lume a fragmentului, obținută prin interpolare între vârfuri, “apare” în texturile de culoare ale obiectului de tip framebuffer, obținut prin desenarea scenei de la pasul anterior, înseamnă că acel fragment este iluminat și trebuie să se calculeze intensitatea iluminării pentru acesta. Acest “apare” este descris puțin mai în detaliu mai jos.

Umbrirea unui fragment

Pentru a verifica dacă un fragment obținut prin redarea scenei din perspectiva observatorului este iluminat sau se află în umbră, putem folosi textura cu format de adâncime din obiectul de tip framebuffer obținut la pasul 1. Se verifică dacă distanța dintre poziția în spațiul lume a fragmentului este aceeași cu cea din textura cu format de adâncime de la pasul 1, când poziția fragmentului este proiectată în această textură.

O exemplificare a acestui proces se regăsește în imaginea de mai jos, unde pixelul marcat cu roșu în panoul a) este proiectat în pixelul marcat cu roșu din textura cu formatul de adâncime, vizibilă în panoul b). Se poate observa că fragmentul marcat cu roșu în panoul a) a fost obținut prin rasterizarea modelului ce descrie terenul, dar proiecția lui pe textura de adâncime, întâlnește un pixel rezultat în urma rasterizării modelului de bambus.

În situația în care distanța dintre poziția în spațiul lume a fragmentului pentru care se calculează iluminarea și sursa de lumină este mai mare decat cea din textura cu format de adâncime, înseamnă că în această textură este desenat un obiect ce se află mai aproape de sursa de lumină și astfel umbrește fragmentul pentru care calculăm intensitatea iluminării. Acest exemplu este chiar în imaginea de mai sus, unde poziția în spațiul lume a fragmentului din panoul a), de pe teren, este mai departe de sursa de lumină față de pixelul ce se regăsește la poziția proiecției lui în textura cu format de adâncime, unde se află un fragment din frunza bambusului. O observație importantă de care trebuie să se țină cont este ca cele două distanțe sa se compare în același spațiu.

Trimiterea texturii cu format de adâncime se poate realiza la fel ca trimiterea unei texturi cu format de culoare. API-ul grafic OpenGL are și un alt mecanism special pentru aceste tipuri de texturi, nefolosit în acest laborator. Vedeți observația de mai jos.

glActiveTexture(GL_TEXTURE0+nr_unitate_texturare);
glBindTexture(GL_TEXTURE_2D, depth_texture);
 
glUniform1i(glGetUniformLocation(shader->program, "depth_texture"), nr_unitate_texturare);

Există mai multe abordări de implementare a metodei de mapare a umbrelor. În laboratorul curent, există o implementare minimală a metodei, ce are scop didactic. Implementarea se regăsește deja aproape completă în framework, astfel că rămâne ca voi să vă concentrați pe gestionarea obiectelor de tip framebuffer. Dacă doriți să aflați mai multe informații despre abordările posibile pentru implementarea acestei metode, puteți consulta următoarele resurse:

Cerințe laborator

  1. Completați metoda CreateFramebuffer() pentru a genera un nou obiect de tip framebuffer, împreună cu texturile atașate la el.
  2. Legați, pe rând, obiectul de tip framebuffer, creat anterior, la banda grafica pentru pasul 1 al metodei de mapare a umbrelor și obiectul de tip framebuffer implicit pentru pasul 2. După acest proces, dacă totul a fost realizat corect până aici, pe ecran se vor afișa în partea dreapta-jos, texturile cu format de culoare și de adâncime ale obiectului de tip framebuffer creat de voi, obținut prin redarea scenei din poziția sursei de lumină. Pentru a schimba între afișarea și ascunderea texturilor de pe ecran, se poate folosi tasta F1.
  3. în metoda RenderSimpleMesh, trimiteți textura cu format de adâncime din obiectul de tip framebuffer creat de voi, obținută la pasul 1 al metodei de mapare a umbrelor.
  4. Utilizați factorul de umbrire în fragment shader-ul cu numele ShadowMappingPassTwo.FS.glsl.

Poziția sursei de lumină de tip spot poate fi controlată prin intermediul tastelor W, S, A, D, Q și E, în absența apăsării butonului dreapta de la mouse.

spg/laboratoare/03.txt · Last modified: 2023/10/22 13: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