Előadás Home Introduction Environment setup Best homeworks

Irány fényforrás


Amikor a fényforrás nagyon távol van (kvázi végtelen), akkor az egyes fénysugarak közel párhuzamosan érkeznek az adott tárgyra. Tehát úgy tűnik, hogy az összes fénysugár ugyanabból az irányból érkezik, függettlenül a tárgy helyzetétől és a nézőiránytól. Amikor egy fényforrás úgy modellezünk, mintha végtelen távol lenne, azt irány fényforrásnak nevezzük. Egy jó példa az irány fényforrásra a Nap. Ugyan nincs végtelen távol, de elég távol van ahhoz, hogy úgy érzékeljük a belőle érkező fénysugarak párhuzamosan érik el az adott objektumok felszínét. A következő ábra demonstrálja az irányfényforrás működését:

title


Mivel az egyes fénysugarak párhuzamosak, ezért nincs jelentősége hogy az egyes objektumok hogyan helyezkednek el a fényforráshoz képest, mert az adott térrészen minden objektumot ugyanabból az irányból fog megvilágítás érni. Mivel a fénysugarak irányvektora ugyanaz, ezért a megvilágítás számítása ugyanaz lesz minden egyes objektum esetén. Egy irányfényforrás definiálásához, a fényforrás pozíció vektora helyett egy irány vektort fogunk definiáli. A shaderen belül a megvilágítás számítása hasonlóan fog történni mint eddig, csak annyi változik, hogy a definiált irányvektort fogjuk használni. (Eddig a fényforrás pozíciójából és az objektum felületéből számítottuk ki az irányt.):

In [ ]:
struct Light {
    // vec3 position; // No longer necessery when using directional lights.
    vec3 direction;
  
    vec3 ambient;
    vec3 diffuse;
    vec3 specular;
};

void main() {
    vec3 lightDir = normalize(-light.direction);
}
Mivel eddig úgy értelmeztük a fénysugár irányát, mint ami az adott fragmentből mutat a fényforrás irányába, ezért most negálnunk kell a definiált irányfényforrás vektorát. (Ezt fordítva is fel lehet venni és akkor nem kell negálni, viszont mindig tartsuk észben, hogy a cross product eredményét befolyásolja az adott vektorok iránya.) A negálás után már a fragmentből mutat a fényforrás irányába az irányvektorunk és így a többi számítás maradhat a régi. Továbbá mindig figyeljünk oda rá, hogy normalizáljuk a vektorokat mielőtt műveleteket hajtunk rajtuk végre.

Annak demonstrálásához hogy az irányfényforrás esetén minden objektum ugyanolyan megvilágítást kap 10 db kockát fogunk definiálni a térben a következő képpen:
In [ ]:
for(unsigned int i = 0; i < 10; i++) {
    glm::mat4 model = glm::mat4(1.0f);
    model = glm::translate(model, cubePositions[i]);
    float angle = 20.0f * i;
    model = glm::rotate(model, glm::radians(angle), glm::vec3(1.0f, 0.3f, 0.5f));
    lightingShader.setMat4("model", model);

    glDrawArrays(GL_TRIANGLES, 0, 36);
}

Definiáljunk egy direction adattagot a Light struktúrában a shader programon belül és ezután a főprogramból tudjuk vezérelni a fényforrás irányát:

In [ ]:
lightingShader.setVec3("light.direction", -0.2f, -1.0f, -0.3f);
Note: Mind a fényforrás pozíció és irány vektorát vec3 típusként reprezentáltuk, azonban egyesek szeretik vec4 típusként reprezentálni őket. Ekkor figyeljünk rá, hogy a w homogén koordináta pozícó vektro esetén 1.0, míg irány vektor esetén 0.0 legyen. Hiszen pozíció esetén azt szeretnénk, hogy minden transzformáció megfelelően hajtódna végre, azonban irányfényforrás esetén nem szeretnénk ha a transzformációk befolyásolnák azt. Ez lehetőséget ad egy egyszerű vizsgálat elvégzésére, ha a w komponens 1.0, akkor pozíció, ha 0.0 akkor irányvektorról van szó:
In [ ]:
if(lightVector.w == 0.0) // note: be careful for floating point errors
  // do directional light calculations
else if(lightVector.w == 1.0)
  // do light calculations using the light's position (like last tutorial)
Irányfényforrást alkalmazva az eredménybek hasonlónak kell lennie:

title

  • A teljes forráskód: Code
  • Vertex shader: Code
  • Fragment shader: Code
  • Lamp Vertex shader: Code
  • Lamp Fragment shader: Code
  • Box texture: img
  • Border texture: img
  • Texture1: img
  • Texture2: img

Pont fényforrás


Az irányfényforrás tökéletes globális fényforrásként, ami az egész teret megvilágítja valamilyen irányból, azonban ha egy adott térrészt szeretnénk megvilágítani, akkor használhatjuk a pont fényforrást. A pontfényforrás rendelkezik egy pozícióval a térben, minden irányban sugároz és a fényének az ereje (intenzitása) csökken a távolság függvényében. Pl. egy villanykörte (izzó) megfeleltethető egy pontfényforrásnak:

title


Az előző tutorialban is egy pontszerű fényforással dolgoztunk, ami minden irányban sugároz, azonban nem vettük számításba, hogy egy pontfényforrás csak egy adott térrészt képes megvilágítani, hiszen a fényereje nem végtelenül erős. Az előző tutorialban azt láttuk, hogy mind a 10 kockát, amit definiáltunk függettlenül attól, hogy milyen távol helyezkedik el a fényforrástól ugyanolyan intenzitással éri a megvilágítás, azonban ez nem életszerű. Azt szeretnénk, hogy azok a kockák amelyek távolabb helyezkednek el a fényforrástól kevesebb megvilágítást kapjanak.

Csillapítás


A legegyszerűbb módszer, hogy csökkentsük a megvilágítás intenzitását a távolság függvényében a lineáris csillapítás. Azonban egy ilyen lineáris csillapítás nagyon természetellenesnek hat. A valóságban a közeli objektumokat erős megvilágítás éri, majd nagyon gyorsan csökken az intenzitás, ahogy távolodunk a fényforrástól és minél távolabb vagyunk elkezd egyre lasabban csökkenni az intenzitás. A következő matematikai formula pontosan egy ilyen csillapítást definiál:

\begin{equation} F_{att} = \frac{1.0}{K_c + K_l * d + K_q * d^2} \end{equation}

$d$ reprezentálja a fragment távolságát az adott fényforrástól. A csillapítás kiszámításához három változót definiálunk: egy konstans értéket $K_c$, egy lineáris tagot $K_l$ és egy kvadratikus tagot $K_q$.

  • A konstans tagot általában 1.0 értékre állítjuk, így biztosítva, hogy a nevező sose lehessen kisebb mint 1.0, hiszen ebben az esetben egy bizonyos távolság után újra felerősödhetne az intenzitás és ez nagyon nem valóságszerű.
  • A lineáris tagot az adott távolsággal szorozzuk, ami lineárisan csökkenti az intenzitást.
  • A kvadratikus tagot a távolság nényzetével szorozzuk ezáltal kvadratikusan csökkentjük az intenzitást. Kis távolság esetén a lineáris tag sokkal jobban fog érvényesülni, azonban minél távolabb kerülünk a fényforrástól a kvadratikus tag egyre jobban dominálni fog a lineáris tag fölött.
A fény intenzitása főleg a lineáris tag miatt fog csökkenni, mindaddig míg a távolság elég nagy nem lesz ahhoz, hogy a kvadratikus tag átvegye a dominálást. A hatás az, hogy a fényforráshoz közel nagyon intenzív megvilágítást kapnak az objektumok, azonban ez az intenzitás nagyon gyorsan csökken ahogy távolodunk a fényforrástól, majd végül lelassul az és egyre kisebb lépésben veszít az intenzitásából. A következő ábra ezt a csillapítást mutatja be:

title

Csillapítás adattagjainak megfelelő megválasztása


A három csillapítási érték beállítása függ a környezettől, attól, hogy mekkora távolságot szeretnénk bevilágítani az adott fényforrástól, a fény fajtájától és még sok változótól. A következő linken számos beállítást találhatunk: Ogre3D's wiki

A csillapítás implementálása


A fragment shaderbe fel kell vennünk három extra float változót: constant, linear és quadratic, amiket a Light struktúrában fogunk tárolni. Továbbá, újra pozíció vektorként kell definiálnunk a fényforrást az előző irányfényforrás helyett.
In [ ]:
struct Light {
    vec3 position;  
  
    vec3 ambient;
    vec3 diffuse;
    vec3 specular;

    float constant;
    float linear;
    float quadratic;
};

A főprogramban beállítjuk a csillapítás adattagjainak az értékét:

In [ ]:
lightingShader.setFloat("light.constant",  1.0f);
lightingShader.setFloat("light.linear",    0.09f);
lightingShader.setFloat("light.quadratic", 0.032f);
A csillapítás kiszámításához egyszerűen alkalmazzuk a fent definiált képletet, majd a kapott értéket megszorozzuk az ambient, diffuse és a specular komponensekkel. A fényforrástól való távolságot az OpenGL length függvénye segítségével határozzuk meg:
In [ ]:
float distance = length(light.position - FragPos);
float attenuation = 1.0 / (light.constant + light.linear * distance + light.quadratic * (distance * distance));    

ambient  *= attenuation; 
diffuse  *= attenuation;
specular *= attenuation;

A következőt kell tapasztalnunk:

title


Látható, hogy minél távolabb van egy doboz, annál kevesebb megvilágítást kap a csillapításnak köszönhetően.

  • A teljes forráskód: Code
  • Vertex shader: Code
  • Fragment shader: Code
  • Lamp Vertex shader: Code
  • Lamp Fragment shader: Code

Reflektor


A reflektor egy olyan speciálsi fényforrás, ami a tér egy adott pozíciójában található és ahelyett, hogy minden irányban világítana, csak egy speciális irányba sugároz. Ennek eredménye az, hogy csak azok az objektumok lesznek megvilágítva, amleyek a reflektor irányának egy bizonyos rádiuszán belül helyezkednek el, míg az ezen a rádiuszon kívül eső objektumok sötétek maradnak. A reflektorra az egyik legjobb példa a zseblámpa.

A reflektor fényforrást az OpenGL-ben egy world space beli koordinátával, egy iránnyal és egy ún. cutoff szöggel (rádiusz definiálása) jellemezhetünk. Tehát minden egyes fragment esetén ki kell számolnunk, hogy beleesik e a fényforrás rádiuszába (megvilágítási kúp):

title


  • LightDir: egy vektor, ami az adott fragmentből a fényforrás irányába mutat.
  • SpotDir: maga a fényforrás azaz a reflektor iránya. A LightDir az a reflektor egy adott fénysugarának az iránya, ami eltalál egy objektum adott fragmentjét.
  • Phi $\phi$: a cutoff szög, ami meghatározza a reflektor rádiuszát. Minden ami ezen a rádiuszon kívül esik nem lesz megvilágítva.
  • Theta $\theta$: a LightDir és a SpotDir vektor közötti szög. A $\theta$ értéknek kisebbnek kell lennie, mint a $\Phi$ értéknek, különben kívül esnénk a kúpon.

Tehát ki kell számolnunk a LightDir és a SpotDir vektor közötti dot product értéket, és ezt kell összehasonlítanunk a $\phi$ cutoff szöggel.

Zseblámpa implementálása


A zseblámpa egy reflektor fényforrás, aminek a pozíciója maga a viewer pozíciója lesz és folyamatosan változni fog a pozíciója és az orientációja, ahogy a viwer közlekedik a térben. Tehát a Light struktúrában a következő változókat definiáljuk: a zseblámpa pozíciója és ebből fogjuk kiszámítani magát a fénysugár irányát. Tárolnunk kell még magának a zseblámpának az irányát és a cutoff szöget.
In [ ]:
struct Light {
    vec3  position;
    vec3  direction;
    float cutOff;
};    

lightingShader.setVec3("light.position",  camera.Position);
lightingShader.setVec3("light.direction", camera.Front);
lightingShader.setFloat("light.cutOff",   glm::cos(glm::radians(12.5f)));
Mint látható a cutoff szögnek nem egy szöget hanem annak a cosinus értékét állítjuk be, ezt azért tesszük, mert a fragment shaderben a LightDir és SpotDir vektor közötti dot product értéket számoljuk ki, ami egy cosinus értékkel fog visszatérni és így egyből össze tudjuk hasonlítani őket. Ha a szöget szeretnénk összehasonlítani akkor az inverz cosinus értéket kellene kiszámítani, ami költséges művelet. Tehát számoljuk ki a $\theta$ értéket majd hasonlítsuk össze a $\phi$ értékkel, hogy meghatározzuk benne vagyunk e a zseblámpa rádiuszában:
In [ ]:
float theta = dot(lightDir, normalize(-light.direction));
    
if(theta > light.cutOff) {       
  // do lighting calculations
}
else  // else, use ambient light so scene isn't completely dark outside the spotlight.
  color = vec4(light.ambient * vec3(texture(material.diffuse, TexCoords)), 1.0);
Tehát kiszámljuk a dot product értéket, de először negáljuk a zseblámpa irányát, mert nekünk arra van szükségünk, hogy az a fragmentről a fényforrás felé mutasson. Ne felejtsük el normalizálni.
Note: Furcsának tűnhet, hogy azt vizsgáljuk, hogy a theta érték mikor nagyobb a cutoff szögnél. Nem fordítva kellene? Igaz, azonban mivel mi cosinus értékekkel számolunk így a 0 fok az cos(1), míg a 90 fok az cos(0): ![title](img/light_casters_cos.png)
Mostmár látható, hogy minél közelebb van a cosinus érték 1-hez a szög annál közelebb van nullához. Ezért kell fordítva vizsgálnunk!

Valami hasonlót kell kapnunk, hogyha futtatjuk az applikációt:

title


Viszont az eredmény egy kicsit hamisnak tűnik, főleg a rádiusz erős élei miatt. Egy realisztikus zseblámpa esetén, ahogy közelítünk a megvilágítási rádiusz széleihez egyre csökkennie kell a megvilágításnak így egy egyenletes fényerő csökkenést szimulálva belesimul a környezetébe.

Smooth/Soft élek


Hogy szimuláljuk a smooth éleket, azt fogjuk feltételezni, hogy a zseblámpának van egy külső és egy belső kúpja. A belső kúp úgy fog viselkedni, ahogy eddig is, tehát ott teljes lesz a megvilágítás, viszont a külső és a belső kúp közötti területen egyenletesen csökkenteni fogjuk a fényerőt. A külső kúp definiálásához felveszünk egy újabb cosinus értéket, majd ha egy fragment a külső és a belső kúp között helyezkedik el akkor kiszámítunk hozzá egy 0.0 és 1.0 közötti intenzitás értéket. Ha a fragment a belső kúpon belül helyezkedik el, akkor az intenzitás érték 1.0, ha viszont kívül esik a külső kúpon, akkor 0.0. A következő formula segítségével fogjuk számolni az intenzitás értéket:

\begin{equation} I = \frac{\theta - \gamma}{\epsilon} \end{equation}


$\epsilon$ (epsilon) a cosinus különbség a belső és a külső kúpok cosinus értékei alapján: $\epsilon = \phi - \gamma$. $I$ intenzitást hozzárendeljük az adott fragmenthez.

Tehát egyszerűen csak kiinterpoláljuk a theta értéket a belső és a külső kúp között, majd a diffuse és specular komponenseket megszorozzuk a számított intenzitás értékkel:
In [ ]:
float theta     = dot(lightDir, normalize(-light.direction));
float epsilon   = light.cutOff - light.outerCutOff;
float intensity = clamp((theta - light.outerCutOff) / epsilon, 0.0, 1.0);    
...
// we'll leave ambient unaffected so we always have a little light.
diffuse  *= intensity;
specular *= intensity;
...
A clamp függvény segítségével a számított intenzitás értéket 0.0 és 1.0 közzé szorítjuk. Jelenleg a következő eredményt kell látnunk:

title

  • A teljes forráskód: Code
  • Vertex shader: Code
  • Fragment shader: Code
  • Lamp Vertex shader: Code
  • Lamp Fragment shader: Code

Multiple lights

  • A teljes forráskód: Code
  • Vertex shader: Code
  • Fragment shader: Code
  • Lamp Vertex shader: Code
  • Lamp Fragment shader: Code