Előadás Home Introduction Environment setup Best homeworks

Textúra

Láttuk, hogy az egyes vertex adatokhoz definiálhatunk szín információt is. De ahhoz, hogy realisztikus környezetet hozzunk létre túl sok vertex és szín információt kellene tárolnunk, ezért egy ún. textúra leírást fogunk alkalmazni. A textúra nem más, mint egy 1, 2 vagy 3 dimenziós kép, ami részletezettség illúzióját kelti az egyes objektumoknál, anélkük hogy újabb vertexeket kellene definiálnunk.

Ahhoz, hogy textúrát adjunk az előző órákon definiált hármoszöghöz, a háromszög minden csúcsa (vertex) esetén meg kell határoznunk, hogy az adott csúcs a textúra mely részéhez tartozik. Tehát minden vertexhez definiálnunk kell egy textúra koordinátát, ami meghatározza, hogy a textúra kép melyik részéről rendeljünk értéket az adott vertexhez. A háromszög esetén, tehát három textúra koordinátát kell definiálni, majd a fragment interpoláció automatikusan meghatározza, hogy a háromszög többi fragmentje milyen textúra színt kap.

Jelen esetben 2D textúra képet fogunk használni, ahol a textúra koordináták a 0 és 1 intervallumon értelmezettek az x és az y tengelynek megfelelően. Az eljárást, ami a textúra koordináták alapján meghatározza a textúra színét mintavételezésnek (sampling) nevezzük. A (0, 0) textúra koordináta a textúra kép bal alsó sarkának feleltethető meg, míg az (1, 1) koordináta a jobb felső sarkot jelöli. A következő ábra azt mutatja, hogy hogyan rendeljük hozzá a textúra koordinátákat egy háromszöghöz:

title


A háromszöghöz 3 db textúra koordinátát definiálunk. Azt akarjuk elérni, hogy a háromszög bal alsó csúcsa a textúra kép bal alső sarkához tartozzon, ezért a háromszög bal alsó vertexéhez a (0.0, 0.0) textúra koordinátát rendeljük. A háromszög jobb alsó csúcsához pedig a textúra kép jobb alsó koordinátáját rendeljük (1.0, 0.0). A háromszög felső csúcsához pedig a (0.5, 1.0) textúra koordinátát rendeljük, hogy az a kép felső részének a közepére essen. Tehát csak 3 db textúra koordinátát kell definiálnunk és tovább adnunk a vertex shadernek, ami tovább adja azokat a fragment shadernek, ahol minden egyes fragmenthez meghatározza a pipeline a megfelelő textúra koordinátákat az interpoláció során.

A textúra koordinátákat a következő képpen definiáljuk:

In [ ]:
float texCoords[] = {
    0.0f, 0.0f,  // lower-left corner  
    1.0f, 0.0f,  // lower-right corner
    0.5f, 1.0f   // top-center corner
};
A textúra koordináták interpolációját számos módon elvégezhtjüt, ezért definiálni kell az OpenGL számára a kívánt interpolációt.

Textúra Wrapping

A textúra koordináták általános esetben a (0,0) és (1,1) intervallumon definiáltak. Ha a koordináták kívül esnek ezen az intervallumon akkor az OpenGL a következő képpen viselkedhet:

  • GL_REPEAT: A textúra kép ismétlése. (default viselkedés)
  • GL_MIRRORED_REPEAT: Hasonló a GL_REPEAT-hez, csak minden ismétlésnél tükrözi a textúrát.
  • GL_CLAMP_TO_EDGE: Minden irányban a legszélső koordinátákat ismétli az objektum széléig. (Megnyúlt textúra mintázatot eredményez.)
  • GL_CLAMP_TO_BORDER: Az intervallumon kívül eső textúra koordináták egy felhasználó által definiált színt kapnak.

title

Az említett textúra wrapping viselkedést koordináta tengelyenként lehet beállítani, ahol az s, t és r végződések feleltethetőek meg az x, y és z koordinátáknak:
In [ ]:
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_MIRRORED_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_MIRRORED_REPEAT);
Az első paraméter definiálja a célpont típusát, mi most 2D textúrákkal dolgozunk, ezért a GL_TEXTURE_2D kulcsszót használjuk. A második paraméter határozza meg, hogy melyik textúra tengelyt szeretnénk definiálni. A GL_TEXTURE_WRAP_S és a GL_TEXTURE_WRAP_T beállítja, hogy az x és az y tengelyre szeretnénk alkalmazni az opciót. Az utolsó paraméter határozza meg, hogy az OpenGL melyik wrapping technikát használja.

Ha a GL_CLAMP_TO_BORDER opciót válassztjuk, akkor egy színt is definiálnunk kell, amit a következő képpen tehetünk meg:
In [ ]:
float borderColor[] = { 1.0f, 1.0f, 0.0f, 1.0f };
glTexParameterfv(GL_TEXTURE_2D, GL_TEXTURE_BORDER_COLOR, borderColor);

Textúra Filtering

A textúra koordináták nem függenek a felbontástól, azonban bármilyen tört értéket felvehetnek, ezért az OpenGL-nek ki kell számítania az adott textúra koordinátához tartozó tényleges textúra pixel értéket vagy más néven texel értéket. Ez főleg nagy objektumok esetén lényeges, amikor kis felbontású textúra áll rendelkezésre. Az OpenGL számos textúra mintavételezési eljárást biztosít, azonban mi a két legelterjetebbet tárgyaljuk:

  • GL_NEAREST: A GL_NEAREST az alap mintavételezési opció az OpenGL-ben. Azt a pixel értéket választja, aminek a középpontja a legközelebb van a textúra koordináta értékéhez. A lenti képen a kereszt jelöli a textúra koordináta tényleges értékét, amihez a bal felső texel van a legközelebb, ezért ezt választjuk színinformációként.

title

  • GL_LINEAR: A GL_LINEAR (más néven bilinear) mintavételezés egy interpolációs értéket számol ki a szomszédos texel értékek alapján. Egy texel minél közelebb található az adott textúra koordinátától, annál nagyobb szerepet játszik az interpoláció kiszámításánál.

title

A következő kép szemlélteti a különbséget, ha egy alacsony felbontású textúrát egy nagy objektumra illesztünk a fent tárgyalt két mintavételezési technika felhasználásával:

title

A GL_NEAREST esetén egy mozaikos mintázatot kapunk, ahol egyértelműen látszik, hogy melyik pixel értékét használtuk a textúrázáshoz, míg a GL_LINEAR esetén egy sokkal elmosotabb képet kapunk, ahol az egyes pixelek nehezebben kivehetők. A GL_LINEAR valósághűbb érzetet kelt, azonban számos fejlesztő mégis a GL_NEAREST opciót választja, ami egy 8 bites nézetet kölcsönöz.

A textúra mintavételezést értelmezhetjük úgy is, mint egy felnagyító vagy csökkentő operátort (fel és le skálázás), tehát például a GL_NEAREST mintavételezést használhatjuk lefele skálázásnál és a GL_LINEAR mintavételezést felskálázás esetén. Ezért a glTexParameter függvény segítségével be kell állítanunk a megfelelő mintavételezési módszert:
In [ ]:
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);

Mipmaps

Tegyük fel, hogy van egy nagy tér, ahol több ezer objektum található, mindegyik egy hozzá tartozó textúrával. Ezek közzül lesznek objektumok, amik közelebb és lesznek amik távolabb helyezkednek el a kamerától, azonban ugyanolyan nagy felbontású textúra van hozzájuk rendelve. A távoli objektumok a renderelés során csak pár fragmentre képződnek le, így az OpenGL-nek bonyolult kiszámítani a helyes színt az egyes fragmentekhez a nagy felbontású textúra alapján. Ez számos zavaró vizuális effektet eredményezhet a kis objektumok esetén, nem is beszélve a felesleges memória használatot.

A probléma kezelésére az OpenGL a mipmaps koncepciót választja, ami tulajdonképpen textúra képsorozatok gyűjteménye, ahol a sorozat következő tagja mindig fele akora, mint az előző kép. Egy bizonyos távolság után az OpenGL a legjobban passzoló méretű mipmap textúra képet fogja használni az objektum textúrázásához. A távolabbi objektumok esetén nem fog vizuálisan feltűnni a felbontás csökkenése, azonban ez jótékony hatással van a teljesítményre.

title


A glGenerateMipmaps metódus segítségével automatikusan le tudjuk generálni a mipmap textúra sorozatokat. Amikor az OpenGL egyik mipmap részletezettségi szintről egy másikra vált a távolság függvényében a két mipmap réteg között olyan vizuális hibák léphetnek fel mint pl. túl éles határvonalak. Ahogy mormál textúrák esetén is a mipmap textúra sorozatok esetén is van lehetőség textúra mintavételezésre az egyes felbontási szintek között:

  • GL_NEAREST_MIPMAP_NEAREST: a legközelebbi mipmap réteg és azon belül a legközelebbi szomszéd pixel értékét használja.
  • GL_LINEAR_MIPMAP_NEAREST: a legközelebbi mipmap réteg választj, de azon belül lineárisan interpolál a szomszéd pixelek alapján.
  • GL_NEAREST_MIPMAP_LINEAR: Lineárisan interpolál az egyes mipmap rétegek között, de a legközelebbi szomszédot veszi alapul pixel szinten.
  • GL_LINEAR_MIPMAP_LINEAR: Mind a rétegek mind pixel szinten lineárisan interpolál.

A glTexParameteri metódus segítségével tudjuk beállítani a kívánt mintavételezést:

In [ ]:
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
Tipikus hiba, amikor a mipmap mintavételezést "nagyobbítás" (GL_TEXTURE_MAG_FILTER) opcióra állítjuk. Ennek semmi hatása nem lesz, mert a mipmap textúrasorozatot leskálázás esetén használhatjuk.

Textúrák létrehozása és betöltése

Ahhoz, hogy egy textúrát használni tudjunk, először be kell tötltenünk az applikációba. Az stb_image.h képkezelő libraryt fogjuk használni a képek betöltéséhez és kezeléséhez. Az stb_image.h egy népszerű header-only képbetöltő library, ami támogatja a legnépszerűbb formátumokat. Az stb_image.h fájl letöltése után addjuk hozzá a projekthez továbbá hozzunk létre egy stb_image.cpp fájlt, amibe a következő kódsorokat írjuk:
In [ ]:
#define STB_IMAGE_IMPLEMENTATION
#include "stb_image.h"
Egy kép betöltéséhez az stb_image.h stbi_load metódusát fogjuk használni:
In [ ]:
int width, height, nrChannels;
unsigned char *data = stbi_load("texture_01.jpeg", &width, &height, &nrChannels, 0);
Az első argumentum maga a textúra kép. A következő három integer paraméter a kép szélessége, magassága és a csatornák száma (színes kép esetén 3).

Textúra generálás


Ahogy minden OpenGL objektum a textúra objektumokra is egy azonosító (id) segítségével hivatkozhatunk:

In [ ]:
unsigned int texture;
glGenTextures(1, &texture);
A glGenTextures metódus eslő paraméterével meghatározzuk, hogy mennyi textúra objektumot szeretnénk létrehozni és tárolni, a második paraméter egy unsigned int (egy textúra esetén) vagy egy unsigned int tömb, ami az azonosítókat tartalmazza. Ahhogy a többi objektumot is, egy textúra objektumot is csatolnunk (bind) kell a pipeline működéséhez:
In [ ]:
glBindTexture(GL_TEXTURE_2D, texture);
Most, hogy létrehoztuk a textúra objektumot, a glTexImage2D függvénnyel a betöltött képet felhasználva legeneráljuk a textúrát:
In [ ]:
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, data);
glGenerateMipmap(GL_TEXTURE_2D);
  • Az első paraméter határozza meg a textúra típusát, mi most GL_TEXTURE_2D opciótválasztunk.
  • A második paraméter határozza meg a mipmap szintet, de most mi ezt 0-ra állítjuk.
  • A harmadik paraméter határozza meg a tárolandó textúra formáját, ami most GL_RGB.
  • A negyedik és az ötödik paraméter állítja be a textúra szélességét és magasságát. Ez most ugyanaz, mint a betöltött textúra kép mérete.
  • A következő paraméter legyen mindig 0. (legacy stuff)
  • A hetedik és a nyolcadik paraméter határozza meg a bemeneti kép formátumát és adattípusát.
  • Az utolsó paraméter maga az aktuális textúra kép.
A glTexImage2D függvény hívás után a legenerált textúrát hozzá csatoltuk az adott textúra objektumhoz. Azonban ez csak az alap textúra, azaz ha szeretnénk a mipmap módszert használni vagy az összes különböző felbontású képet manuálisan kell generálnunk, vagy használhatjuk a glGenerateMipmap függvényt, ami automatikusan legenerálja a megfelelő képeket. Miután legeneráltuk a textúrát és az esetleges hozzá tartozó mipmap szinteket, szabadítsuk fel a betöltött kép által lefoglalt memóriát:
In [ ]:
stbi_image_free(data);

A textúra generálás teljes kódja:

In [ ]:
unsigned int texture;
glGenTextures(1, &texture);
glBindTexture(GL_TEXTURE_2D, texture);
// set the texture wrapping/filtering options
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
// load and generate the texture
int width, height, nrChannels;
unsigned char *data = stbi_load("texture_01.jpeg", &width, &height, &nrChannels, 0);
if (data) {
    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, data);
    glGenerateMipmap(GL_TEXTURE_2D);
}
else {
    std::cout << "Failed to load texture" << std::endl;
}
stbi_image_free(data);

Textúra alkalmazása


A következőekben egy téglalapot fogunk kirajzolni és erre az alakzatra fogjuk alkalmazni a textúrát. Ehhez meg kell mondanunk az OpenGL-nek, hogy hogyan mintavételezze a textúrát, ezért a vertex adatot frissítjük egy textúra koordináta adattal is:

In [ ]:
float vertices[] = {
    // positions          // colors           // texture coords
     0.5f,  0.5f, 0.0f,   1.0f, 0.0f, 0.0f,   1.0f, 1.0f,   // top right
     0.5f, -0.5f, 0.0f,   0.0f, 1.0f, 0.0f,   1.0f, 0.0f,   // bottom right
    -0.5f, -0.5f, 0.0f,   0.0f, 0.0f, 1.0f,   0.0f, 0.0f,   // bottom left
    -0.5f,  0.5f, 0.0f,   1.0f, 1.0f, 0.0f,   0.0f, 1.0f    // top left 
};

Mivel egy újabb vertex attribútumot definiáltunk, ezért erről az OpenGL-t is figyelmeztetni kell:

title

In [ ]:
glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)(6 * sizeof(float)));
glEnableVertexAttribArray(2);
Frissítenünk kell a vertex attribútum pointert egy 8 * sizeof(float) méretű lépéssel, továbbá módosítanunk kell a vertex shader programot, hogy fogadja a textúra koordinátákat egy vertex attribútumon keresztül és tovább adja azokat a fragment shadernek:
In [ ]:
#version 400 core
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec3 aColor;
layout (location = 2) in vec2 aTexCoord;

out vec3 ourColor;
out vec2 TexCoord;

void main() {
    gl_Position = vec4(aPos, 1.0);
    ourColor = aColor;
    TexCoord = aTexCoord;
}
A fragment shadernek fogadnia kell a vertex shaderből érkező textúra koordinátákat. A fragment shadernek a textúra objektumhoz is hozzá kell férnie. A GLSL rendelkezik egy beépített adattípussal, amit sampler-nek nevezünk, a mi esetünkben ez egy sampler2D típus, mert 2D textúrát használunk.
In [ ]:
#version 400 core
out vec4 FragColor;
  
in vec3 ourColor;
in vec2 TexCoord;

uniform sampler2D ourTexture;

void main() {
    FragColor = texture(ourTexture, TexCoord);
}
Ahhoz, hogy egy textúra színeit mintavételezzük, a beépített texture függvényét használjuk a GLSL-nek. Az eslő paraméter a textúra sampler objektum, míg a második paraméter a megfelelő textúra koordináták. A fragment shader kimenete a textúra szín az interpolált textúra koordináták függvényében. Mielőtt meghívnánk a glDrawElements függvényt a rajzoláshoz, még a textúra objektumot csatolnunk kell a pipelinehoz:
In [ ]:
glBindTexture(GL_TEXTURE_2D, texture);
glBindVertexArray(VAO);
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);

Az eddigiek alapján a következő kimenetet kell látnunk:

title

  • A teljes forrás kód: Code
  • A vertex shader kód: Code
  • A fragment shader kód: Code
  • Kép beolvasó library: Code
  • A textúra kép: img


A textúra képet keverhetjük a definiált vertex színekkel, ehhez egyszerűen összeszorozzuk a két értéket:

In [ ]:
FragColor = texture(ourTexture, TexCoord) * vec4(ourColor, 1.0);

Eredményként a következő képet kapjuk:

title


Texture Units


Talán észrevettétek, hogy a sampler2D változót uniform módon definiáltuk, de nem rendeltünk hozzá értéket a glUniform függvénnyel. A glUniform1i használatával valójában egy lokációt rendelünk a textúra sampler objektumhoz, így egyszerre több textúrát használhatunk a fragment shaderben. Ezt a textúra lokációt hívjuk textúra unit-nak. Az alapértelmezett textúra unit a 0-ás érték, ami egy aktív textúra unit, ezért nem kellett lokációt rendelnünk az előző esetben. Viszont nem az összes grafikus kártya driver rendelkezik alapértelmezett textúra unit-tal, ezért lehet, hogy ezt nektek kellmegtenni.

A textúra unit fő célja, hogy egyszerre több textúrát használhassunk a shaderekben. Azzal, hogy textúra unit-okat rendelünk az egyes sampler objektumokhoz, egyszerre több textúrát csatolhatunk (bind) miután az aktuális textúra unit-ot aktiváltuk. A glActiveTexture segítségével aktiválhatjuk az adoot textúrát:
In [ ]:
glActiveTexture(GL_TEXTURE0); // activate the texture unit first before binding texture
glBindTexture(GL_TEXTURE_2D, texture);
Miután aktiváltuk a textúra unit-ot, a glBindTexture hozzácsatolja a textúrát az adott aktív textúra unit-hoz. A GL_TEXTURE0 textúra unit alapértelmezetten mindig aktivált, ezért nem kellett az előző példában aktiválni.
Note: Az OpenGL minimum 16 textúra unit-ot biztosít, amiket a GL_TEXTURE0 ... GL_TEXTURE15 változókkal aktiválhatunk. Sorrendben vannak definiálva, így a GL_TEXTURE8 unit-ot a következő képpen is elérhetjük: GL_TEXTURE0 + 8. Ez akkor hasznos, ha egy ciklussal végig szeretnénk menni a textúra unit-okon.

A következőben módosítjuk a fragment shadert, hogy egy másik sampler objektumot is kezeljen:
In [ ]:
#version 400 core
...

uniform sampler2D texture1;
uniform sampler2D texture2;

void main() {
    FragColor = mix(texture(texture1, TexCoord), texture(texture2, TexCoord), 0.2);
}
A kimeneti szín, most két textúra kombinációja lesz. A GLSL rendelkezik egy beépített mix függvénnyel, ami az első két paraméterben kapott értéket a harmadik paraméter szerint lineárisan kombinálja. Ha a harmadik paraméter 0.0, akkor az első értékkel, ha 1.0, akkor a második értékkel tér vissza. Most 0.2-re állítottuk, így az első érték 80%-át kombinálja a második érték 20%-val.

Most egy újabb textúrát is betöltünk:
In [ ]:
unsigned char *data = stbi_load("texture_02.jpg", &width, &height, &nrChannels, 0);
if (data) {
    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height, 0, GL_RGB /*GL_RGBA*/, GL_UNSIGNED_BYTE, data);
    glGenerateMipmap(GL_TEXTURE_2D);
}
Ha egy .png fájl reprezentálja a textúrát, ami alpha (átlátszóság) csatorát is tartalmaz, akkor a kép adatformátuma esetén a GL_RGBA parancsot használjuk. Ahhoz, hogy mind a két textúrát használni tudjuk, mind a két textúrát csatolnunk kell a rendering pipelinehoz, azáltal, hogy hozzárendeljük a megfelelő textúra unit-hoz:
In [ ]:
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, texture1);
glActiveTexture(GL_TEXTURE1);
glBindTexture(GL_TEXTURE_2D, texture2);

glBindVertexArray(VAO);
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
Végül az OpenGL-nek meg kell adni, hogy melyik textúra unit-hoz melyik sampler objektum tartozik a shaderben. Ezt egyszer kell beállítani, így a render cikluson kívül tesszük meg:
In [ ]:
ourShader.use(); // don't forget to activate the shader before setting uniforms!  
glUniform1i(glGetUniformLocation(ourShader.ID, "texture1"), 0); // set it manually
ourShader.setInt("texture2", 1); // or with shader class
  
while(...)  {
    [...]
}
A következő eredményt kell látnunk:

title


A textúrák jelenleg fejjel lefelé állnak. Ez azért van mert az OpenGL a (0,0) koordinátát a bal alsó sarokban definiálja, de általában a képek esetén ez a bal felső sarok. Az stb_image.h segítségével azonban meg tudjuk ezt változtatni még a kép betöltése előtt:
In [ ]:
stbi_set_flip_vertically_on_load(true);

Ezek után a következő eredményt kell látnunk:

title

  • A teljes forrás kód: Code
  • A vertex shader kód: Code
  • A fragment shader kód: Code
  • A textúra kép: img
  • A textúra kép: img