Video Laborator 8: https://youtu.be/QuhUGAhrXUQ
Autori: Philip Dumitru, Andrei Lăpușteanu, Robert Caragicu
Va reamintim formulele pentru calculul culorii intr-un punct al unei suprafețe:
$culoarea = c_e + c_a + c_d + c_s$
Emisiva: $c_e = K_e$
Ambientala: $c_a = I_a \cdot K_a$
Difuza: $c_d = K_d \cdot I_{sursă} \cdot max(\vec{N}\cdot \vec{L}, 0)$
Speculara: $c_s = K_s \cdot I_{sursă} \cdot lum \cdot (max(\vec{N}\cdot \vec{H}, 0)^n $, unde $lum = (\vec{N}\cdot \vec{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:
$culoarea = K_e + I_a \cdot K_a + \sum{f_{at_i} \cdot I_{sursă_i} (K_d \cdot max(\vec{N}\cdot \vec{L_i}, 0) + K_s \cdot lum_i \cdot (max(\vec{N}\cdot \vec{H_i}, 0)^n})$
La laboratorul de saptamana trecuta, pentru ușurința implementării, am considerat mai multe simplificări:
Totuși, trebuie să menționăm că modelul complet urmărește formula de mai sus, unde constantele de material $K_e, K_a, K_d, K_s$ 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.
Modelul de iluminare aplicat in cazul implementarii in fragment shader este acelasi cu cel studiat in Laboratorul 07, din punct de vedere matematic. Totusi, exista o diferenta majora intre cele doua implementari prin faptul ca iluminarea nu se mai aplica la nivelul fiecarul vertex ci la nivel de fragment. Rezultatul final este superior calitativ intrucat iluminarea fiecarui fragment nu se va mai calcula pe baza interpolarii luminii calculate la nivel de vertex ci pe baza normalei si pozitiei in spatiu a fiecarui fragment.
Valorile de intrare primite de fragment shader sunt interpolate linar intre valorile vertexilor ce compun primitiva utilizata la desenare.
Imaginea de mai sus este obtinuta prin desenarea unui triunghi avand cele 3 varfuri de culori diferite: rosu, verde, albastru
Prin transmiterea culorii de la Vertex Shader la Fragment Shader culoarea fiecarui fragment de pe suprafata triunghiului este calculata ca o interpolare linara intre culorile vertexilor ce compun primitiva specificata (in acest caz, un triunghi).
Acelasi procedeu se aplica pentru orice alta proprietate, cum ar fi:
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;
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:
float cut_off = radians(30.0f); float spot_light = dot(-L, light_direction); if (spot_light > cos(cut_off)) { // fragmentul este iluminat de spot, deci se calculeaza valoarea luminii conform modelului Phong // se calculeaza atenuarea luminii }
Pentru a simula corect iluminarea de tip spot este nevoie sa tratam si atenuarea luminii corespunzatoare apropierii unghiului de cut-off. Putem astfel sa utilizam un model de atenuare patratica ce ofera un rezultat convingator.
float cut_off = radians(30.0f); float spot_light = dot(-L, light_direction); float spot_light_limit = cos(cut_off); // Quadratic attenuation float linear_att = (spot_light - spot_light_limit) / (1.0f - spot_light_limit); float light_att_factor = pow(linear_att, 2);
Pentru a simula mai multe lumini, putem scrie un shader care să calculeze contribuția fiecărei lumini în parte. Pentru a fi ușor să scriem codul de shadere, acesta poate fi modularizat.
În GLSL se pot defini funcții similar ca în limbajul C. Putem scrie o funcție pentru a calcula pentru o sursă de lumină culoarea rezultată din componentele difuze și speculare. Un exemplu de funcție ar fi:
vec3 point_light_contribution(vec3 light_pos, vec3 light_color) { vec3 color; //calculele componentelor difuze si speculare din modelul Phong de iluminare pentru lumina punctiforma. return color; }
Putem accesa orice variabilă globală din orice funcție din cod, inclusiv uniforme. Astfel putem citi în funcția point_light_contribution
normala suprafeței primită ca variabilă de intrare la fragment shader și uniformele cu constante de material, fără să le trimitem ca parametri.
Putem specifica ce tip de parametrii are funcția:
in
înseamnă că valoarea va fi copiată când se apelează funcția. Funcția poate modifica parametrul cum doreșteout
înseamnă că valoarea nu fi inițializată de apelant și după ce funcția modifică parametrul valoarea va fi copiată în variabila corespunzătoare apelantuluiinout
le combină pe cele două
Cuvintele cheie in
, out
si inout
se scriu înainte de tipul de data al parametrului funcției.
De exemplu, dacă dorim separat contribuția difuză și cea speculară, un exemplu de semnatură de funcție ar fi:
void point_light_contribution(vec3 light_pos, vec3 light_color, out vec3 diffuse_contribution, out vec3 specular_contribution);
Dacă nu se specifică, parametrul este de tip in
. Astfel, putem construi funcții ce întorc mai multe valori.
Metoda de declarare și definiție a funcțiilor este similară cu cea din C.
vec3 point_light_contribution(vec3 light_pos, vec3 light_color); void main() { //... } vec3 point_light_contribution(vec3 light_pos, vec3 light_color) { //... }
Nu este permisă recursivitate în GLSL.
Pentru a trimite ușor multe surse de lumină, putem defini vectori de uniforme. De exemplu, pentru a trimite mai multe surse de lumină punctiforme se declară în shader un vector de uniforme:
uniform vec3 point_light_pos[9]; uniform vec3 point_light_color[9];
Pe urmă putem trimite uniformele cu un apel în cod:
glm::vec3 point_light_pos[9]; glm::vec3 point_light_color[9]; GLuint location = glGetUniformLocation(program, "point_light_pos"); glUniform3fv(location, 9, glm::value_ptr(point_light_pos[0])); //glm::value_ptr intoarce adresa de memorie unde se gasesc datele unui vector, matrici etc.
O altă variantă este declararea unei structuri în shader
struct light_source { int type; vec3 position; vec3 color; vec3 direction; }; uniform light_source lights[9];
În GLSL structura este doar o definiție a unei agregări de tipuri de dată. Nu se poate obține locația unei structuri sau a unui vector de structuri. În schimb, putem interoga locația fiecărui membru din fiecare element din vectorul de structuri. De exemplu, se poate găsi locația uniformei lights[0].position
.
Următorul cod C++ trimite pozițiile surselor de lumină în shader:
light_source light_sources[9]; //light_source e o structura declarata similar cu cea scrisa in shader. for (int i = 0;i < 9;++i) { std::string name = std::string("lights[") + std::to_string(i) + std::string("].position"); GLuint location = glGetUniformLocation(program, name.c_str()); glUniform3fv(location, 1, glm::value_ptr(light_sources[i].position)); }
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.
uniform vec3 light_direction
[Bonus]
OnInputUpdate