În laboratoarele anterioare, am analizat modalitatea prin care se poate desena pe ecran geometria unui model 3D. Până în acest moment, culorile atribuite pixelilor în care a fost discretizată suprafața geometriei au fost alese artificial prin diferite abordări, precum interpolarea între culorile vârfurilor. Deoarece ochiul uman „vede” doar lumină :) , culoarea unui pixel în care a fost discretizată o suprafață trebuie să se calculeze pe baza influenței unei surse de lumină asupra zonei din suprafață, reprezentată de pixel. Pentru acest proces, se introduce în procesul de desenare conceptul de sursă de lumină. Există mai multe tipuri de astfel de surse:
Calcularea influenței iluminării ce provine de la o sursă de lumină, de orice tip, asupra unui punct de pe o suprafață, se realizează prin simularea procesului de transport al luminii de la poziția sursei, prin reflexie pe suprafață, în poziția punctului de pe suprafață, la poziția observatorului. O abordare care simulează acest proces de transport al luminii poartă numele de model de iluminare sau model de reflexie.
În general, în majoritatea modelelor de iluminare, influența luminii asupra unui punct este împărțită în 4 componente:
Diferenta dintre ultimele două componente este dată de comportamentul de reflexie, respectiv o reflexie difuză și o reflexie oglindă.
Deoarece lumina este aditivă, cu cât mai mulți fotoni ajung în ochii nostri, cu atât lumina este mai „puternică” :), rezultatul final al influenței iluminării într-un punct este dat de suma celor 4 componente:
$$ iluminare = componentaEmisiva + componentaIndirecta + componentaDifuza + compoentaOglinda $$
În situația în care ne referim strict la transportul luminii de la suprafața sursei la poziția observatorului, această componenta se poate aproxima printr-o distribuire uniformă a împrăștierii luminii de la suprafață în toate direcțiile, astfel că poate fi utilizată o singură valoare uniformă pentru toată suprafața.
În general, în aplicațiile grafice în timp real este precalculată și se evită utilizarea pentru un număr mare de obiecte dinamice. Odată cu apariția procesoarelor grafice ce au arhitecturi de ray-tracing, se folosesc astfel de tehnici pentru simularea transportului luminii de la o sursă, ce ajunge prin reflexii multiple pe mai multe suprafețe, secvențial, la poziția observatorului.
Acestea se referă la transportul luminii de la poziția unei surse, prin reflexie pe o suprafață, la poziția observatorului. Pentru comoditate, se împarte acest tip de reflexie în două componente distincte:
Această componentă se referă la cantitatea de lumină ce este împrăștiată uniform prin reflexie de pe o suprafață, în toate direcțiile din jurul punctului de pe suprafață, deasupra ei.
Această componentă se referă la cantitatea de lumină ce este reflectată de-a lungul direcției de reflexie simetrică față de direcția de incidență a luminii cu direcția vectorului normal. Această componentă este cunoscută sub numele de componentă speculară. Termenul speculum în limba latină se traduce în limba română cu termenul de oglindă :) .
În momentul în care o sursă de lumină se depărtează de un obiect, prin procesul de difuzie, cantitatea de lumină emisă de la sursă rămâne constantă, dar obiectul primește o cantitate mai mică de lumină. Simularea acestui fenomen se realizează prin scăderea intenstității iluminării pe baza distanței dintre poziția sursei de lumină și poziția pentru care se calculează iluminarea.
În practică, inclusiv în cadrul acestui laborator, se utilizează metode ce simulează fenomenele optice menționate mai sus.
vec3 emissive_component = Ke; # GLSL
Este cunoscută în domeniul graficii pe calculator sub numele de componentă ambientală. Pentru a nu calcula tot transportul luminii cu toate reflexiile de pe suprafețe, se consideră că influența indirectă a iluminării este aceeași în toate punctele din scenă. Această aproximare obține rezultate satisfăcătoare pentru o parte din scenarii.
Avem astfel:
vec3 ambient_component = Ka * global_ambient_color; # GLSL
În imaginea de mai jos se poate observa că un fascicul de lumină ce are o lățime de dimensiune $A$ este proiectat pe o suprafață, de-a lungul unei zone de dimensiune $B$. Se poate observa că în situația în care fasciculul este proiectat vertical pe suprafață, $B = A$.
Notăm cu $\alpha$ unghiul dintre vectorul $\vec{L}$, ce reprezintă direcția de la poziția punctului spre poziția sursei de lumină și vectorul normal, $\vec{N}$. Deoarece unghiul făcut de vectorul normal cu suprafața este 90 de grade, unghiul făcut de vectorul $\vec{L}$ cu suprafața este de $90-\alpha$. Pentru că suma unghiurilor unui triunghi este de 180 de grade și triunghiul din imagine este dreptunghic, rezultă că cel de-al treilea unghi din triunghi are dimensiunea $\alpha$. Astfel, rezultă că:
$$ cos(\alpha)=\frac{A}{B} \\ B=\frac{A}{cos(\alpha)} $$
Din acest motiv, se poate deduce că intensitatea de iluminare pentru orice punct de pe suprafață este $cos(\alpha)$. În situația în care $\alpha$ este de 0 grade, intensitatea de iluminare este $cos(0)=1$. Această abordare poartă numele de legea cosinusului a lui Lambert, propusă de Johann Heinrich Lambert în anul 1760.
În domeniul graficii pe calculator, această lege a cosinusului este utilizată pentru calculul componentei difuze a iluminării, ce se referă la lumina împrăștiată uniform în toate directiile. Mai exact, se consideră că lumina se împrăștie uniform în toate direcțiile cu intensitatea de iluminare dată de legea cosinusului a lui Lambert.
În practică, în loc de cosinus, se utilizează produsul scalar dintre $\vec{L}$ și $\vec{N}$, cu ambii vectori de lungime 1. Reamintim faptul că interpretarea geometrică a produsului scalar este:
$$ \vec{V_1}\cdot\vec{V_2}=\lVert\vec{V_1}\rVert\lVert\vec{V_2}\lVert\cos(\angle(\vec{V_1},\vec{V_2})) $$
În situația în care vectorii $\vec{V_1}$ și $\vec{V_2}$ sunt amândoi de lungime 1, expresia de mai sus este echivalentă cu:
$$ \vec{V_1}\cdot\vec{V_2}=cos(\angle(\vec{V_1},\vec{V_2})) $$
vec3 Vu = normalize(V);
Astfel, calculul componentei difuze a iluminării este:
$$ componentaDifuza = K_d \cdot culoareLumina \cdot max(\vec{N}\cdot \vec{L}, 0) $$
În limbajul GLSL, expresia de mai sus se transcrie sub forma:
vec3 diffuse_component = Kd * culoareLumina * max (dot(N,L), 0); # GLSL
Pentru calcularea componentei speculare, vom folosi modelul propus de Bui Tuong Phong în anul 1973:
$$ componentaSpeculara = K_s \cdot culoareLumina \cdot primesteLumina \cdot (max(\vec{V}\cdot \vec{R}, 0))^n $$
În limbajul GLSL, expresia de mai sus se transcrie sub forma:
vec3 specular_component = Ks * culoareLumina * primesteLumina * pow(max(dot(V, R), 0), n) # GLSL
Componenta speculară reprezintă lumina reflectată de suprafața obiectului numai în jurul direcției $\vec{R}$. Acest vector se obține prin:
vec3 R = reflect (-L, N) # GLSL
reflect()
are primul parametru vectorul incident, care intră în suprafață, nu cel care iese din ea așa cum este reprezentat în imaginea de mai sus.
În modelul Phong, se aproximează scăderea rapidă a intensității luminii reflectate atunci când $\alpha$ crește prin $(cos \alpha)^n$, unde $n$ este exponentul de reflexie speculară al materialului (shininess).
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.
Factorul de atenuare a intensității iluminării pe bază de distanță se aplică doar componentei difuze și speculare:
vec3 illumination = emissive_component + ambient_component + attenuation_factor * ( diffuse_component + specular_component); # GLSL
Pentru a simula o sursă de lumină precum o lanternă, ce împrăștie lumină de-a lungul unei direcții de iluminare, se introduc câteva informații suplimentare:
În domeniul graficii pe calculator, o astfel de sursă de lumină poartă numele de sursă de tip spot.
Un punct se află în conul de lumină al unei surse de tip spot dacă condiția următoare este îndepilită:
float cos_theta_angle = dot(-L, light_direction); float cos_phi_angle = cos(phi_angle); if (cos_theta_angle > cos_phi_angle ) { // fragmentul este iluminat, astfel ca se calculeaza valoarea luminii conform modelului Phong // se calculeaza atenuarea luminii }
Pentru a simula corect iluminarea de tip spot, este nevoie să tratăm și atenuarea iluminării corespunzătoare apropierii unghiului de tăiere. Putem astfel să utilizăm un model de atenuare pătratică ce oferă un rezultat convingător.
float spot_linear_att_factor = (cos_theta_angle - cos_phi_angle) / (1.0f - cos_phi_angle); float quadratic_spot_light_att_factor = pow(spot_linear_att_factor, 2);
Biblioteca GLM ne pune la dispoziție metode de construcție a matricilor celor 3 tipuri de transformări analizate: translație, modificare de scară și rotație. Următoarele 2 lanțuri de transformări sunt identice:
glm::mat4 model = glm::mat4(1); model = glm::translate(model, glm::vec3(-5, 1.5f, 1)); model = glm::rotate(model, glm::radians(60.0f), glm::vec3(0, 1, 0)); model = glm::scale(model, glm::vec3(0.1f));
glm::mat4 model = glm::mat4(1); model *= transform3D::Translate(glm::vec3(-5, 1.5f, 1)); model *= transform3D::RotateOY(glm::radians(60.0f)); model *= trasnform3D::Scale(glm::vec3(0.1f));
Dacă dorim să rotim un vector cu 30 de grade față de axa OY, putem utiliza construcția matricii pentru transformarea de rotație, pusă la dispoziție de biblioteca glm, după cum urmează:
glm::vec3 v = glm::vec3(1, 0, 0); glm::mat4 rotation = glm::rotate(glm::mat4(1), glm::radians(30.0f), glm::vec3(0, 1, 0)); // First option v = glm::mat3(rotation) * v; // Second option v = glm::vec3(rotation * glm::vec4(v, 1.0f));
Pentru a transmite un vector de obiecte ce au tipuri de date din biblioteca GLM într-un atribut de tip uniform, la un shader, se poate utiliza:
glm::vec3 light_positions[10]; glUniform3fv(location, 10, glm::value_ptr(light_positions[0]));
layout(location = 1) in vec3 v_normal;
vec3 world_position = (model_matrix * vec4(v_position,1)).xyz;
vec3 world_normal = normalize( mat3(model_matrix) * v_normal );
vec3 N = normalize( world_normal );
vec3 L = normalize( light_position - world_position );
vec3 V = normalize( eye_position - world_position );
VertexShader.glsl
calculul poziției vârfului și a vectorului normal în spațiul lumii și transmiteți-le spre fragment shader.FragmentShader.glsl
:FragmentShader.glsl
: