Előadás Home Introduction Environment setup Best homeworks

Megvilágítás és színek


A való világban a színeket folytonos értékként értelmezhetjük így a lehetséges színek száma végtelen. Ezt a végtelen tartományt egy véges értékkészletre vetítjük a digitális világban, de így is annyi színt vagyunk képesek megjeleníteni, hogy gyakorlatilag nem veszi észre a szemünk a különbséget. Egy színt az RGB (piros, zöld, kék) komponensekkel reprezentálunk és ezen három szín kombinációjaként tettszőleges színeket hozhatunk létre. Például a korall színt a következő képpen kapjuk:
In [ ]:
glm::vec3 coral(1.0f, 0.5f, 0.31f);
A szín, amit egy adott tárgy színeként látunk az nem magának az objektumnak a valódi színe, hanem az a szín ami visszaverődík a tárgyról, azaz amit nem nyel el. A Nap fényét fehér fényként érzékeljük (számos szín kombinációja), ahogy a képen is látható, tehát ha egy kék tárgyat megvilágítunk ezzel a fehér fénnyel, akkor az a fehér fény összes színét elnyeli, de a kék színt visszaveri. Ez a visszavert kék színű fény érkezik a szemünkbe és így kéknek látjuk az adott tárgyat.

A korall színű tárgyunk a fehér fény egyes komponenseit különböző intenzitással veri vissza:

title


Amikor az OpenGL segítségével definiálunk egy fényforrást, először adnunk kell neki egy színt. A fényforrás színe bármilyen tettszőleges szín lehet (nem csak fehér). A következő lépésben a fényforrás és a tárgy színének az elemenkénti szorzataként kapjuk a visszavert fény színét, ami a ténylegesen érzékelt szín:

In [ ]:
glm::vec3 lightColor(1.0f, 1.0f, 1.0f);
glm::vec3 toyColor(1.0f, 0.5f, 0.31f);
glm::vec3 result = lightColor * toyColor; // = (1.0f, 0.5f, 0.31f);
Láthatjuk, hogy a tárgy a fehér fény nagy részét elnyeli, de a saját színének a függvényében számos r,g,b komponenst visszaver. Ha a fényforrás színét zöldre állítjuk akkor a következőt tapasztaljuk:
In [ ]:
glm::vec3 lightColor(0.0f, 1.0f, 0.0f);
glm::vec3 toyColor(1.0f, 0.5f, 0.31f);
glm::vec3 result = lightColor * toyColor; // = (0.0f, 0.5f, 0.0f);
Mivel a fényforrásnak csak zöld komponense van, ezért a tárgy a piros és a kék komponenseket se elnyelni se visszaverni nem tudja. Viszont a tárgy színéből látjuk, hogy a zöld komponens felét elnyeli, míg a másik felét visszaveri, így a tárgyat sötétzöld színűnek érzékeljük (a korall szín hirtelen sötétzöld lesz).
A fényforrás helyének a szimulálásához egy fehér kockát fogunk kirajzolni a képernyőre. A következő vertex shadert fogjuk a rajzoláshoz használni:
In [ ]:
#version 400 core
layout (location = 0) in vec3 aPos;

uniform mat4 model;
uniform mat4 view;
uniform mat4 projection;

void main() {
    gl_Position = projection * view * model * vec4(aPos, 1.0);
}
A lámpa (fényforrás) megjelenítéséhez egy új VAO-t hozunk létre, mert a kocka objektumainkon számos transzformációt fogunk végrehajtani, de nem akarjuk, hogy ezek a transzformációk hatással legyenek a lámpa objektumra.
In [ ]:
unsigned int lightVAO;
glGenVertexArrays(1, &lightVAO);
glBindVertexArray(lightVAO);
// we only need to bind to the VBO, the container's VBO's data already contains the correct data.
glBindBuffer(GL_ARRAY_BUFFER, VBO);
// set the vertex attributes (only position data for our lamp)
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
A kockák rajzolásához szükséges fragment shader:
In [ ]:
#version 400 core
out vec4 FragColor;
  
uniform vec3 objectColor;
uniform vec3 lightColor;

void main() {
    FragColor = vec4(lightColor * objectColor, 1.0);
}
A fragment shaderben egy uniform változóval reprezentáljuk a fényforrás színét és az objektum színét. Majd a két szín szorzataként megkapjuk a visszavert fény színét (azaz amit látunk). Végül a uniform változókon keresztül a fragment shaderbe küldjük a megfelelő színeket:
In [ ]:
// don't forget to 'use' the corresponding shader program first (to set the uniform)
lightingShader.use();
lightingShader.setVec3("objectColor", 1.0f, 0.5f, 0.31f);
lightingShader.setVec3("lightColor",  1.0f, 1.0f, 1.0f);
Mivel nem akarjuk, hogy a fényforrás hatással legyen saját magára, ezért létrehozunk egy újabb fragment shadert a lámpa színének a meghatározásához, ami egy konstans fehér szín lesz:
In [ ]:
#version 400 core
out vec4 FragColor;

void main() {
    FragColor = vec4(1.0); // set all 4 vector values to 1.0
}
A lámpát reprezentáló kocka fő célja, hogy vizuálisan lássuk honnan érkezik a fény, hiszen egy szimpla fényforrás pozíciónak önmagában nincs vizuális jelentése. Ezért a lámpa kockáját a fényforrás pozíciójába rajzoljuk ki. A fényforrás (lámpa) pozícióját egy vec3 típusú változóval fogjuk leírni a világ koordináta rendszerében:
In [ ]:
glm::vec3 lightPos(1.2f, 1.0f, 2.0f);
A lámpát reprezentáló kockát a fényforrás pozíciójába transzformáljuk és lekicsinyítjük:
In [ ]:
model = glm::mat4(1.0f);
model = glm::translate(model, lightPos);
model = glm::scale(model, glm::vec3(0.2f));

Amikor a render ciklusban a lámpát rajzoljuk ki először aktiválni kell a hozzá tartozó shader objektumot és be kell tölteni a lámpához tartozó VAO-t:

In [ ]:
lampShader.use();
// set the model, view and projection matrix uniforms
...
// draw the lamp object
glBindVertexArray(lightVAO);
glDrawArrays(GL_TRIANGLES, 0, 36);
Az eddigiek alapján a következő eredményt kell kapnunk:

title

Megvilágítás

A valós világban a megvilágítás nagyon sok tényezőtől függ, ezért a szimulálása nagyon komplex feladat, bonyolult, számításigényes műveleteket sorozatától függ. Ezért az OpenGL-ben a valóság megközelítésére törekszünk egyszerű megvilágítási modellek használatával. Ezek a modellek a fény általunk ismert fizikai törvényszerűségein alapszanak. Az egyik ilyen használt modell a Phong modell. A Phong model három fő építőelemből áll: ambient, diffus és specular megvilágítási tényező. A következő ábra ezt reprezentálja:

title

  • Ambient megvilágítás: még ha sötétség is van akkor is tapasztalható némi fény a való világban (pl.: Hold fénye) ezért az objektumok sosem teljesen sötétek. Ennek szimulálására használjuk az ambient tényezőt, mint egy konstans értéket, ami mindig ad az objektumoknak valamilyen színt.
  • Diffuse megvilágítás: azt szimulálja, hogy az objektum megvilágítása (színe) függ az objektum feületére beeső fénysugár szögétől. Ez a Phong modell legerősebb tényezője. Egy objektum oldala minél inkább merőleges a beeső fénysugárra annál erősebb a megvilágítás hatása.
  • Specular megvilágítás: Egy világos foltként szimulálja a fény megcsillanását egy fényes felületen.

Ambient megvilágítás


A megvilágítás általában nem egy darab fényforrásból származik, hanem számos fényforrás által előidézett szóródásból jön létre. Pl. ha egy szobában állunk az ablakon beérkező fény nem csak direkten világít meg bennünket, hanme a falról visszaverődve is. Továbbá a szobában lévő lámpa is hatással van a végső megvilágításra. Azt az algoritmust, ami mindezekkel a tényezőkkel számol globális illuminációnak nevezzük, de ennek a számolása nagyon számításigényes és komplikált.

Az ambient modellt a globális illumináció leegyszerűsítésére használjuk, mégpedig úgy, hogy a megvilágítás végső eredményéhez hozzáadunk egy konstans értéket, ami azt szimulálja, hogy kindig van valamilyen kis szóródó fény a világban, még akkor is ha nincs direkt fényforrás.

Az ambient hatás eléréséhez a fény színét megszorozzuk egy konstans értékkel, majd ezt szorozzuk meg az objektum színével és így mindig lesz egy kis színe az objektumoknak:
In [ ]:
void main() {
    float ambientStrength = 0.1;
    vec3 ambient = ambientStrength * lightColor;

    vec3 result = ambient * objectColor;
    FragColor = vec4(result, 1.0);
}
A vizsgált tárgy elég sötét, de az ambient tényezőnek köszönhetően nem teljesen:

title

Diffuse megvilágítás


A diffuse megvilágítási tényezővel jelentős vizuális hatást adhatunk a megvilágitott tárgyhoz. A következő ábra a diffuse tényező kiszámítását mutatja:

title


A lámpa által kibocsájtott fénysugár megvilágítja az objektumot, azaz találkozik annak egy adott fragmentjevel. A diffuse tényező az objektum felületére beeső fénysugár szögétől függ, azaz ki kell számolnunk, hogy az adott fénysugár miyen szögben érkezik az adott fragmentre. A beeső fénysugár akkor fejti ki a legnagyobb hatást, ha a beesési szög merőleges a tárgy felszínére. A beesési szög kiszámításához az objektum felületére merőleges normál vektorokat használjuk (itt sárga vektor). A normál vektor és a beeső fénysugár által bezárt szög lesz a beesési szög, amit a két vektor dot (product) szorzataként tudunk kiszámolni.

Ha a két vektor derékszöget zár be egymással, akkor a dot product 0. Minél nagyobb szöget ($\theta$) zár be a két vektor annál kisebb lesz a fény hatása az objektum felületére.
Note: A dot product számításnál figyeljünk oda, hogy a két vektor egységvektor, azaz normalizáltak.


A dot product egy skalár értékkel fog visszatérni, amit arra használunk, hogy kiszámítsuk mekkora hatással van a fény a tárgy színére. Tehát a következőket kell kiszámolnunk:

  • Normal vektor: egy vektor, ami merőleges a vertex felszínre.
  • Irányított fénysugár: egy irányvektor, ami a fényforrás pozíciójának és az "eltalált" fragment poyíciójának a különbsége.

Normal vektor

A normal vektor az egy egységvektor, ami merőleges az objektum felszínére azaz a vertex surface-re. Mivel a vertex csak egy pont a 3D térben és önmagában nincs felszíne ezért vagy kiszámoljuk azt a szomszédos vertexek cross (product) szorzataként, vagy mivel a most kirajzolt kocka nem egy bonyolult alakzat egyszerűen hozzáadhatjuk a normal vektorokat a vertex data tömbhöz. Ha ezt az extra adatot hozzáadjuk a vertex adathoz, akkor frissítenünk kell a vertex shadert:
In [ ]:
#version 400 core
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec3 aNormal;
...
Ha hozzáadtuk a normál vektorokat a vertex adathoz, akkor frissírenünk kell a vertex attribútum pointert is, azaz konfigurálnunk kell az egyes attribútum lokációt. A lámpa shader nem fogja használni a normál vektorokat, ezért ott nem kell konfigurálni az egyes lokációt, azonban frissíteni kell ott is a nullás lokációhoz tartozó pozíció adatoknál a stride értékét:
In [ ]:
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
Mivel a lámpánál csak a pozíció információt használjuk és a következő 3 értéket azaz a normál vektort nem ezért 6-ra állítjkuk a stride-ot. Mivel a vertex adatot már a GPU-n tároljuk ezért nem érdemes a lámpa rajzolásához egy új vertex tömböt a GPU-ra küldeni, szimplán csak figyelmen kívül hagyjuk a normál vektorokat. Végül a vertex shaderből továbbadjuk a normál vektorokat a fragment shaderbe, mivel ott számoljuk a megvilágítást:
In [ ]:
out vec3 Normal;

void main() {
    gl_Position = projection * view * model * vec4(aPos, 1.0);
    Normal = aNormal;
}

A fragment shaderben ne felejtsük el definiálni a megfelelő bemeneti változót:

In [ ]:
in vec3 Normal;

Diffuse szín számítása


Mivel a fényforrás pozíciója csak egy szimpla változó ezért azt egy uniform változóként deklaráljuk a fragment shaderben:
In [ ]:
uniform vec3 lightPos;
Majd a globális változóként definiált fényforrás pozíciót ráküldjük a uniform változón keresztül a shaderre:
In [ ]:
lightingShader.setVec3("lightPos", lightPos);
Szükségünk van még az aktuális fragment pozícióra és mivel a megvilágítás kiszámolását a világ koordinátarendszerébe számoljuk ezért a vertex koordinátákat át kell transzformálnunk. Ezt úgy érjük el, hogy a vertex koordinátákat megszorozzuk a model mátrixal és így a world spacebe transzformáljuk őket:
In [ ]:
out vec3 FragPos;  
out vec3 Normal;
  
void main() {
    gl_Position = projection * view * model * vec4(aPos, 1.0);
    FragPos = vec3(model * vec4(aPos, 1.0));
    Normal = aNormal;
}

Hozzuk létre a megfelelő bemeneti változót a fragment shaderben:

In [ ]:
in vec3 FragPos;
Mostmár minden változónk megvan a diffuse megvilágítás kiszámításához. Első lépésben ki kell számolnunk a fény irányvektort a fényforrás és a fragment pozíciója között:
In [ ]:
vec3 norm = normalize(Normal);
vec3 lightDir = normalize(lightPos - FragPos);
Note: A megvilágítás számolásánál nem foglalkozunk a vektorok nagyságával és a pozíciókkal csak az irány az érdekes számunkra ezért mindig egység vektorokkal dolgozunk, továbbá az egységvektorok leegyszerűsítenek számos számítást (pl. dot product), ezért sose felejtsük el normalizálni a vektorműveletek eredményeit.


A következő lépésben a normál vektor és a fény irányvektora közötti dot product értékkel kiszámoljuk a fénysugár hatását az adott fragmentre. A dot product értékét szorozzuk meg a fény színével, hogy megkapjuk a diffuse komponenst:

In [ ]:
float diff = max(dot(norm, lightDir), 0.0);
vec3 diffuse = diff * lightColor;
Ha a két vektor által bezárt szög nagyobb mind 90 fok, akkor a dot product értéke negítív lesz, ezért használjuk a max függvényt, hogy ebben az esetben az érték legyen egyenlő nullával, azaz nem lesz hatása a diffuse komponensnek. (Negatív színnek a valós világban nincs sok értelme.) Mostmár mind az ambient és mind a dissufe megvilágítási értéket hozzá tudjuk adni az objektum végső színéhez:
In [ ]:
vec3 result = (ambient + diffuse) * objectColor;
FragColor = vec4(result, 1.0);

Jelenleg valami hasonlót kell látnunk:

title


Látható, hogy a kockák azon oldalai amik a fényforrásra merőlegesen állnak a legvilágosabbak, míg a többi oldala egyre kevesebb megvilágítést kap.

Normál vektor transzformálása

Jelenleg a normal vektort egyszerűen továbbítottuk a vertex shaderből a fragment shaderbe, azonban mivel minden műveletet a world spacben végzünk ezért a normal vektorokat is traszformálni kellene, de itt nem elég egyszerűen csak megszorozni a model mátrixal. Mivel a normal vektorok csak irányvektorok így nem reprezentálnak semmilyen pozíciót a térben, továbbá homogén koordinátájuk sincs. Ezért a transzláció művelete semmilyen hatást nem kell, hogy végezzen rajtuk. Tehát a model mátrixból eliminálnunk kell a transzlációd komponenst azaz csak a felső 3x3-as mátrixot fogjuk használni a model mátrixból. Csak a skálázás és a forgatás műveletét szeretnénk alkalmazni a normál vektorokra.

Továbbá, ha a model mátrix nem uniform skálázást is tartalmaz, akkor a norml vektorok a model mátrixal való szorzás után nem lesznek merőlegesek a felszínre, tehát ezt figyelembe kell vennünk. A következő ábra mutatja a nem uniform skálázás eredményét a normál vektorra:

title


Ennek kiküszöbölésére nem az eredeti model mátrixal fogjuk szorozni a normál vektorokat a world spacebe való transzformáláshoz, hanem egy ún. normal mátrixal. A normal matrix nem lesz más mint az eredeti model mátrix felső 3x3-as mátrix inverzének a transzponáltja. A vertex shaderben állítjuk elő ezt a normal mátrixot a következő képpen, majd szorozzuk meg vele a normal vektort:

In [ ]:
Normal = mat3(transpose(inverse(model))) * aNormal;
A mátrix invertálása nagyon költséges művelet még a vertex shaderben is és mivel a vertex shaderben minden egyes vertex transzformálásakor elbégezzük ezt a műveletet, ezért ezt ésszerűen egyszer érdemes kiszámolni a CPU-n majd az eredményt a shaderbe küldeni. A fenti példa csak oktatási célra szolgál.

Specular megvilágítás


Ahogy a diffuse esetnél a specular hatás is függ a beeső fény szögétől, de itt a néző iránytól is függ a látvány, azaz, hogy a kamerával milyen irányból nézzük az adott tárgyat. A specular hatás a fény visszaverődés tulajdonságától függ. A specular hatás akkor a legerősebb, ha az objektum felszíne úgy viselkedik mint egy tükör. A következő kép mutatja a specular hatás számolásához szükséges adatokat:

title


A specular hatáshoz ki kell számolnunk a visszavert fény irányát (reflection vector), ami a beeső fénysugár normal vektor körüli szóródása. A következő lépésben ki kell számítanunk a visszavert fénysugár és a nézőirány áltaé bezárt szöget. Minél kisebb ez a bezárt szög annál nagyobb a specular hatás. Az eredmény egy kis tükröződés (fényes pont).

A nézőirányt a néző (kamera) world space-beli koordinátájából és az adott fragment pozícióból számoljuk. Majd kiszámoljuk a specular intenzitást, ezt megszorozzuk a fény színével és hozzáadjuk az ambient és diffuse hatáshoz.
Note: Most mi a megvilágítást a world spaceben számoljuk, azonban sokan ezt a view spaceben teszik meg. Ekkor a néző pozíciója mindig (0,0,0) így ezt nem kell számolni, de tanulási célból intuitívabb ezt a world spaceben kipróbálni.


A néző pozíciója a mi esetünkben egyszerűen a kamera pozíciója, amit egy uniform változón keresztül a shaderre küldünk:

In [ ]:
uniform vec3 viewPos;
lightingShader.setVec3("viewPos", camera.Position);
Először definiálunk egy specular intenzitás értéket. Most ezt egy közepes értékre állítjuk, hogy ne legyen túl nagy hatása:
In [ ]:
float specularStrength = 0.5;
Ha ezt 1.0-ra állítanánk akkor egy nagyon fényes pontot kapnánk. Következő lépésben kiszámoljuk a nézőirány vektort és a hozzá tartozó reflekciós vektor irányát a normál vektor körül:
In [ ]:
vec3 viewDir = normalize(viewPos - FragPos);
vec3 reflectDir = reflect(-lightDir, norm);
Vegyük észre, hogy negáltuk a fényirány vektort, ez a reflect függvény miatt szükséges, mert pont az ellenkező irányból várja ezt az értéket mint ahogy mi korábban definiáltuk. (Ez a kivonás sorrensjétől függ.) A második komponense a reflect függvénynek a normál vektor. Végül kiszámítjuk a specular komponenst:
In [ ]:
float spec = pow(max(dot(viewDir, reflectDir), 0.0), 32);
vec3 specular = specularStrength * spec * lightColor;
Első lépésben kiszámítjuk a dot productot a nézőirány és a visszavert fény között, továbbá a max függvénnyel garantáljuk, hogy ez ne legyen negatív. Majd a kapott értéket a 32 kitevőre emeljük, ahol a 32 a fényesség értékét definiálja a felületnek. Minél nagyobb a fényesség értéke egy objektumnak annál hatékonyabban veri vissza a fényt, ahelyett hogy elszorná azt a normál vektor körül. A következő kép szemlélteti a specular hatást különböző fényesség értékekkel:

title


Végül a specular hatást hozzáadjuk az ambient és a diffuse komponenshez majd az eredményt megszorozzuk az objektum színével:

In [ ]:
vec3 result = (ambient + diffuse + specular) * objectColor;
FragColor = vec4(result, 1.0);
Ezzel implementáltuk a Phong modellt, és valami hasonlót kell látnunk:

title


  • A teljes forrás kód: Code
  • A vertex shader kód: Code
  • A fragment shader kód: Code
  • A lámpa vertex shader kód: Code
  • A lámpa fragment shader kód: Code
  • Vertex data: Code