This is the Polish translation of Getting-started/Textures article of learnopengl.com tutorial series.
Dowiedzieliśmy się, że aby dodać więcej szczegółów do naszych obiektów, możemy użyć kolorów dla każdego wierzchołka. Aby jednak uzyskać realistyczny obraz musielibyśmy mieć wiele wierzchołków, abyśmy mogli określić wiele kolorów. Zajmuje to znaczną ilością dodatkowego czasu, ponieważ każdy model potrzebuje dużo więcej wierzchołków, a dla każdego wierzchołka także atrybut koloru.
W tym celu artyści i programiści wolą zwykle używać tekstur. Tekstura jest obrazem 2D (są nawet tekstury 1D i 3D) używanym do dodawania szczegółów do obiektu; pomyśl o teksturze jako kawałku papieru o ładnym obrazie z cegły (na przykład) starannie nałożonym na Twój dom 3D, tak by wyglądał, jakby miał kamienną powierzchnię. Ponieważ możemy umieścić wiele szczegółów w jednym obrazie, możemy spowodować złudzenie, że obiekt zawiera więcej detali bez konieczności określania dodatkowych wierzchołków.
Poza obrazami, tekstury mogą być również wykorzystywane do przechowywania dużej kolekcji danych, aby wysłać je do shaderów, ale zostawimy to na inny temat.
Poniżej zobaczysz teksturę ceglanej ściany nałożoną na trójkąt z poprzedniego samouczka.
Aby nałożyć teksturę na trójkąt, musimy powiedzieć każdemu wierzchołkowi, której części tekstury on odpowiada. Każdy wierzchołek powinien więc posiadać koordynatę tekstury (ang. texture coordinate), który określa, jaka część obrazu tekstury ma zostać pobrana. Interpolacja fragmentów powoduje nałożenie tekstury na resztę fragmentów.
Współrzędne tekstur zawierają się w przedziale od 0 do 1 w osiach x i y (pamiętaj, że używamy tekstury 2D). Pobieranie koloru tekstury przy użyciu współrzędnych tekstur nazywa się próbkowaniem (ang. sampling). Współrzędne tekstury zaczynają się w punkcie (0,0) znajdującym się w lewym dolnym rogu obrazu tekstury, a kończą w punkcie (1,1) znajdującym się w prawym górnym rogu obrazu tekstury. Poniższy obraz przedstawia sposób mapowania współrzędnych tekstury na trójkącie:
Określamy 3 koordynaty tekstury dla trójkąta. Chcemy, aby lewy dolny róg trójkąta odpowiadał lewej dolnej części tekstury, dlatego używamy współrzędnej tekstury (0,0) dla dolnego lewego wierzchołka trójkąta. To samo tyczy się dolnego prawego rogu z współrzędną tekstury (1,0). Górny róg trójkąta powinien odpowiadać środkowi górnej krawędzi obrazu tekstury, dlatego wybieramy punkt (0.5, 1.0) jako jego koordynatę tekstury. Musimy tylko przekazać 3 współrzędne tekstury do VS, który następnie przekazuje je do FS, który dokładnie interpoluje wszystkie współrzędne tekstury dla każdego fragmentu.
Powstałe współrzędne tekstury wyglądałyby tak:
GLfloat texCoords[] = {
0.0f, 0.0f, // lewy dolny róg
1.0f, 0.0f, // lewy prawy róg
0.5f, 1.0f // górny środkowy róg
};
Próbkowanie tekstury ma wiele metod interpolacji - można to zrobić na wiele różnych sposobów. Naszym zadaniem jest poinformowanie OpenGL w jaki sposób powinien on próbkować teksturę.
Zawijanie tekstury (ang. Texture Wrapping)
Współrzędne tekstur zwykle są w przedziale (0,0) do (1,1), ale co się stanie, jeśli określimy współrzędne poza tym zakresem? Domyślnym zachowaniem OpenGL jest powtórzenie obrazu tekstury (w zasadzie ignorujemy część całkowitą zmiennoprzecinkowej współrzędnej tekstury), ale OpenGL daje nam więcej opcji:
- GL_REPEAT: Domyślne zachowanie dla tekstur. Powtarza obraz tekstury.
- GL_MIRRORED_REPEAT: Tak samo jak GL_REPEAT, ale odbija obraz lustrzanie z każdym powtórzeniem.
- GL_CLAMP_TO_EDGE: Obcina współrzędnych do przedziału [0, 1]. Rezultatem jest to, że większe współrzędne są przycinane do krawędzi, co prowadzi do rozciągnięcia krawędzi.
- GL_CLAMP_TO_BORDER:Współrzędne poza zakresem są teraz kolorowane na kolor podany przez użytkownika.
Każda z powyższych opcji ma inny wygląd przy użyciu współrzędnych tekstury poza zakresem domyślnym. Zobaczmy, jak to wszystko wygląda na przykładowej teksturze.
Każda z wyżej wymienionych opcji może być ustawiona na każdą oś współrzędnych (s, t (i r jeśli używasz tekstury 3D) odpowiadające x, y, z) za pomocą funkcji glTexParameter*:
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_MIRRORED_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_MIRRORED_REPEAT);
Pierwszy argument określa typ tekstury; pracujemy z teksturami 2D, więc typem tekstur jest GL_TEXTURE_2D. Drugi argument wymaga, abyśmy powiedzieli, jaką opcję chcemy ustawić i dla jakiej osi. Chcemy skonfigurować opcję WRAP i określić ją dla osi S i T. Ostatni argument to tryb zawijania tekstury i w tym przypadku OpenGL ustawi opcję GL_MIRRORED_REPEAT zawijania aktualnie aktywnej tekstury.
Jeśli wybierzemy opcję GL_CLAMP_TO_BORDER, powinniśmy również określić kolor obramowania. Odbywa się to za pomocą funkcji glTexParameter z przyrostkiem fv. Ustawiamy opcję GL_TEXTURE_BORDER_COLORi przekazujemy tablicę float zawierającą wartości koloru obramowania:
float borderColor[] = { 1.0f, 1.0f, 0.0f, 1.0f };
glTexParameterfv(GL_TEXTURE_2D, GL_TEXTURE_BORDER_COLOR, borderColor);
Filtrowanie tekstury
Współrzędne tekstury nie zależą od rozdzielczości, dlatego mogą to być dowolne wartości zmiennoprzecinkowe. W tym celu OpenGL musi określić, do którego piksela tekstury (znany również jako texel) ma zmapować daną współrzędną tekstury. Ma to szczególne znaczenie, jeśli masz bardzo duży obiekt i teksturę o niskiej rozdzielczość. Prawdopodobnie zgadłeś, że OpenGL posiada opcję filtrowania tekstur. Istnieje kilka opcji filtrowania, ale teraz omówimy najważniejsze opcje: GL_NEAREST i GL_LINEAR.
Domyślną metodą filtrowania tekstur w OpenGL jest GL_NEAREST (znany również jako filtrowanie najbliższego sąsiedztwa (ang. nearest neighbor filtering)). Jeśli wybierzesz metodę GL_NEAREST, to OpenGL wybiera piksel, którego środek znajduje się najbliżej danej współrzędnej tekstury. Poniżej widać 4 piksele, gdzie krzyżyk reprezentuje dokładną współrzędną tekstury. Górny lewy teksel ma swój środek najbliżej tej współrzędnej tekstury i dlatego jest on wybierany jako kolor próbki:
GL_LINEAR (znany również jako filtrowanie liniowe / bilinearne (ang. (bi)linear filtering)) interpoluje (uśrednia) wartość tekseli, które są najbliżej współrzędnej tekstury. Im mniejsza odległość współrzędnej tekstury od środka teksela, to tym bardziej kolor tego teksela przyczynia się do finalnego koloru. Poniżej widać, że zwrócony został uśredniony kolor sąsiednich pikseli:
Ale jaki jest efekt wizualny tych metod filtrowania tekstur? Zobaczmy, jak te metody działają przy użyciu tekstury o małej rozdzielczości na dużym obiekcie (tekstura jest więc skalowana do góry i poszczególne teksele są zauważalne):
GL_NEAREST generuje bloczki, w których możemy wyraźnie zobaczyć piksele, które tworzą teksturę, podczas gdy GL_LINEAR generuje gładki wzór, w którym poszczególne piksele są mniej widoczne. GL_LINEAR daje bardziej realistyczny obraz, ale niektórzy programiści wolą “8-bitowy” wygląd i w rezultacie wybierają opcję GL_NEAREST.
Filtrowanie tekstur można ustawiać dla operacji powiększania (ang. magnifying) i pomniejszania (ang. minifying) (podczas skalowania tekstury w górę lub w dół), dzięki czemu można np. używać filtrowania najbliższego sąsiedztwa, gdy tekstury są skalowane w dół i filtrowania liniowego dla skalowania w górę. Musimy zatem określić metodę filtrowania obu opcji za pomocą glTexParameter*. Kod powinien wyglądać podobnie do kodu zawijania tekstury:
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
Mipmapy
Wyobraźmy sobie, że mamy duży pokój z tysiącami przedmiotów, każdy z dołączoną teksturą. Będą znajdować się tam obiekty, gdzie niektóre z nich są daleko od widza, a niektóre są bliżej niego. W obu tych przypadkach jest używana tekstura wysokiej rozdzielczości. Ponieważ niektóre obiekty są daleko i prawdopodobnie tylko kilka fragmentów będzie widocznych, to OpenGL ma trudności z ustaleniem prawidłowej wartości koloru dla tego fragmentu z tekstury wysokiej rozdzielczości. Dzieje się tak, ponieważ musi wybrać kolor tekstury dla fragmentu, który rozciąga się na dużą część tekstury. Powoduje to widoczne artefakty na małych przedmiotach, nie wspominając już o marnowaniu pamięci, w przypadku używania tekstur o wysokiej rozdzielczości na małych obiektach.
Aby rozwiązać ten problem, OpenGL używa koncepcji zwanej mipmapami, która jest w zasadzie zbiorem obrazów tekstur, gdzie każda kolejna tekstura jest dwukrotnie mniejsza w porównaniu do poprzedniej. Pomysł mipmap powinien być łatwy do zrozumienia: po pewnej odległości od widza, OpenGL użyje innej mipmap z tekstury, która najlepiej odpowiada odległości do obiektu. Ponieważ obiekt jest daleko, mniejsza rozdzielczość nie będzie aż tak widoczna dla użytkownika. Ponadto, mipmapy mają dodatkową cechę bonusową - są dobre dla wydajności. Przyjrzyjmy się bliżej temu, jak wygląda mipmapa:
Tworzenie zbioru mipmapowanych tekstur dla każdej tekstury jest zbyt kłopotliwe, aby robić to ręcznie. Na szczęście OpenGL jest w stanie wykonać dla nas całą tę pracę za pomocą pojedynczego wywołania funkcji glGenerateMipmaps po utworzeniu tekstury. Później w tym samouczku zobaczysz wykorzystanie tej funkcji.
Podczas przełączania między poziomami mipmap w czasie renderowania, OpenGL może wyświetlać niektóre artefakty, takie jak ostre krawędzie widoczne między dwiema warstwami mipmap. Podobnie jak w przypadku zwykłego filtrowania tekstur, możliwe jest filtrowanie między poziomami mipmap używając filtrowania NEAREST i LINEAR do przełączania między poziomami mipmap. Aby określić metodę filtrowania między poziomami mipmap, możemy zastąpić oryginalne metody filtrowania jedną z następujących czterech opcji:
- GL_NEAREST_MIPMAP_NEAREST: pobiera najbliższą mipmapę w celu dopasowania do rozmiaru pikseli i używa interpolacji najbliższego sąsiedztwa do próbkowania tekstury.
- GL_LINEAR_MIPMAP_NEAREST: pobiera najbliższy poziom mipmapy i próbkuje ją wykorzystując interpolację liniową.
- GL_NEAREST_MIPMAP_LINEAR: liniowo interpoluje pomiędzy dwiema mipmapami, które najbardziej odpowiadają rozmiarowi piksela i próbkuje wykorzystując interpolację najbliższego sąsiedztwa.
- GL_LINEAR_MIPMAP_LINEAR: liniowo interpoluje między dwoma najbliższymi mipmapami i próbkuje tekstury poprzez interpolację liniową.
Podobnie jak filtrowanie tekstur możemy ustawić metodę filtrowania na jedną z 4 wspomnianych metod używając funkcji glTexParameter:
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
Najczęstszym błędem jest ustawienie jednego z filtrów mipmap jako filtru powiększenia. Nie ma to żadnego wpływu, ponieważ mipmapy są używane głównie w przypadku, gdy tekstury zostaną skalowane w dół: powiększenie tekstury nie korzysta z mipmap i nadawanie im opcji filtrowania mipmap generuje kod błędu GL_INVALID_ENUM.
Ładowanie i tworzenie tekstur
Pierwszą rzeczą, jaką musimy zrobić, aby faktycznie wykorzystać tekstury, jest załadowanie ich do naszej aplikacji. Obrazy tekstur mogą być przechowywane w dziesiątkach formatów plików, każdy z własnymi strukturami i porządkiem danych, więc jak je załadować do naszej aplikacji? Jednym z rozwiązań byłoby wybranie formatu pliku, którego chcemy użyć. Powiedzmy, że chcemy ładować pliki o formacie .PNG. Wtedy możemy napisać własną mini-bibliotekę do ładowania tego typu obrazów, aby przekonwertować je na dużą tablicę bajtów. Chociaż nie jest trudno napisać własną mini-bibliotekę do ładowania obrazów, to wciąż jest to kłopotliwe. A co jeśli chciałbyś obsługiwać więcej formatów plików? Następnie musisz napisać kolejną mini-bibliotekę ładującą obraz dla każdego formatu, który chcesz obsługiwać.
Innym rozwiązaniem i prawdopodobnie dobrym jest użycie biblioteki ładującej obrazy, która obsługuje kilka popularnych formatów i wykonuje całą ciężką pracę za nas. Bibliotekę taką jak stb_image.h.
stb_image.h
stb_image.h jest bardzo popularną biblioteką zawartą w jednym pliku nagłówkowym do ładowania obrazów, autorstwa Sean Barrett’a, która może ładować najpopularniejsze formaty plików i można łatwo ją zintegrować się z Twoją aplikacją. stb_image.h można pobrać stąd. Wystarczy pobrać pojedynczy plik nagłówkowy, dodać go do projektu jako stb_image.h i utworzyć dodatkowy plik C ++ o następującym kodzie:
#define STB_IMAGE_IMPLEMENTATION
#include "stb_image.h"
Definiując STB_IMAGE_IMPLEMENTATION, preprocesor modyfikuje plik nagłówkowy tak, że zawiera tylko odpowiedni kod źródłowy definicji, skutecznie przekształcając plik nagłówka w plik .cpp i to wszystko. Teraz po prostu dołącz stb_image.h gdzieś w swoim projekcie i go skompiluj.
W kolejnych sekcjach będziemy używać obrazu drewnianego kontenera. Aby załadować obraz za pomocą stb_image.h używamy funkcji stbi_load:
int width, height, nrChannels;
unsigned char *data = stbi_load("container.jpg", &width, &height, &nrChannels, 0);
Funkcja ta przyjmuje w pierwszym parametrze lokalizację pliku obrazu. Następnie oczekuje na podanie trzech int’ów jako drugiego, trzeciego i czwartego argumentu, które stb_image.h wypełni szerokością, wysokością i liczbą kanałów koloru. Potrzebujemy szerokości i wysokości obrazu do generowania obiektów tekstur OpenGL.
Generowanie obiektów tekstur
Podobnie jak każdy z poprzednich obiektów OpenGL, do tekstur odwołujemy się poprzez identyfikator; stwórzmy jeden obiekt tekstury:
GLuint texture;
glGenTextures(1, &texture);
Funkcja glGenTextures najpierw bierze pod uwagę, ile tekstur chcemy wygenerować i zapisać w tablicy Gluint, któa jest drugim argumentem (w tym przypadku jest to pojedyncza zmienna GLuint). Podobnie jak inne obiekty musimy ją aktywować, więc kolejne komendy odwołujące się do tego typu tekstury będą konfigurować obecnie aktywowaną teksturę:
glBindTexture(GL_TEXTURE_2D, texture);
Teraz, gdy tekstura jest aktywowana, możemy zacząć zapisywać do niej wcześniej wczytywane dane obrazu. Dane są zapisywane do tekstury przy użyciu glTexImage2D:
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, image);
glGenerateMipmap(GL_TEXTURE_2D);
Jest to duża funkcja z kilkoma parametrami, więc przejdźmy po nich krok po kroku:
- Pierwszy argument określa typ tekstury; ustawiając go na GL_TEXTURE_2D oznacza, że ta operacja zapisze dane tekstury do aktualnie aktywowanej tekstury z tym samym typem (więc tekstury, które były aktywowane z typem GL_TEXTURE_1D or GL_TEXTURE_3D nie zostaną naruszone).
- Drugi argument określa poziom mipmap, dla którego chcemy załadować dane tekstury, jeśli chcesz ręcznie ustawić poziom mipmap. Zostawimy go na poziomie bazowym 0.
- Trzeci argument mówi OpenGL w jakim formacie chcemy zapisać dane tekstury. Nasz załadowany obraz ma tylko wartości RGB, dlatego zapiszemy je w RGB.
- 4 i 5 argument określają szerokość i wysokość powstałej tekstury. Zapisaliśmy je wcześniej podczas ładowania obrazu, więc będziemy używać odpowiednich zmiennych.
- Następny argument powinien zawsze być 0 (przestarzała pozostałość).
- Argumenty 7 i 8 określają format i typ danych obrazu źródłowego. Ładowaliśmy obraz z wartościami RGB i zapisaliśmy je jako char (bajty).
- Ostatnim argumentem jest wskaźnik na dane załadowanego obrazu.
Po wywołaniu funkcji glTexImage2D, do obecnie aktywnego obiektu tekstury zostają dołączone dane obrazu. Jednak obecnie posiada on tylko podstawowy poziom mipmapy i jeśli chcemy używać więcej mipmap, musimy je ręcznie zdefiniować (przez zwiększanie drugiego argumentu w kolejnych wywołaniach) lub możemy skorzystać z funkcji glGenerateMipmap do wygenerowaniu mipmap. Spowoduje to automatyczne wygenerowanie wszystkich wymaganych mipmap dla aktualnie aktywnej tekstury.
Po zakończeniu generowania tekstury i odpowiadających mu mipmap, dobrą praktyką jest zwolnienie pamięci obrazu:
stbi_image_free(data);
Cały proces generowania tekstury wygląda więc tak:
unsigned int texture;
glGenTextures(1, &texture);
glBindTexture(GL_TEXTURE_2D, texture);
// ustaw opcje zawijania/filtrowania tekstury (na aktywnym obiekcie tekstury)
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
// załaduj obraz i wygeneruj obiekt tekstury
int width, height, nrChannels;
unsigned char *data = stbi_load("container.jpg", &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);
Używanie tekstur
W następnych sekcjach będziemy używać kształtu prostokąta, który został narysowany za pomocą glDrawElements z ostatniej części kursu Witaj Trójkącie . Musimy poinformować OpenGL, jak próbkować teksturę, więc musimy zaktualizować dane wierzchołkowe o współrzędne tekstury:
GLfloat vertices[] = {
// Pozycje // Kolory // Współrzędne tekstury
0.5f, 0.5f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f, // Prawy górny
0.5f, -0.5f, 0.0f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f, // Prawy dolny
-0.5f, -0.5f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, // Lewy dolny
-0.5f, 0.5f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 1.0f // Lewy górny
};
Ponieważ dodaliśmy dodatkowy atrybut wierzchołka, musimy ponownie powiadomić OpenGL o nowym formacie wierzchołków:
glVertexAttribPointer(2, 2, GL_FLOAT,GL_FALSE, 8 * sizeof(GLfloat), (GLvoid*)(6 * sizeof(GLfloat)));
glEnableVertexAttribArray(2);
Zauważ, że musimy również ustawić parametr stride (skok) dwóch wcześniejszych atrybutów wierzchołka na wartość 8 * sizeof(GLfloat).
Następnie musimy zmienić VS, aby przyjmował współrzędne tekstur jako atrybut wierzchołka, a następnie przekazać te współrzędne do FS:
#version 330 core
layout (location = 0) in vec3 position;
layout (location = 1) in vec3 color;
layout (location = 2) in vec2 texCoord;
out vec3 ourColor;
out vec2 TexCoord;
void main()
{
gl_Position = vec4(position, 1.0f);
ourColor = color;
TexCoord = texCoord;
}
FS powinien następnie przyjąć zmienną TexCoord jako zmienną wejściową.
FS powinien mieć również dostęp do obiektu tekstury. Ale jak przekazać obiekt tekstur do FS? GLSL posiada wbudowany typ danych dla obiektów tekstur o nazwie sampler*, który przyjmuje jako przyrostek typ tekstury, na którym chcemy pracować np. sampler1D, sampler3D lub w naszym przypadku sampler2D. Następnie możemy dodać teksturę do FS, deklarując uniform sampler2D, do który później przypisujemy naszą teksturę.
#version 330 core
in vec3 ourColor;
in vec2 TexCoord;
out vec4 color;
uniform sampler2D ourTexture;
void main()
{
color = texture(ourTexture, TexCoord);
}
Aby próbkować kolor tekstury użyjemy wbudowanej funkcji GLSL texture, która jako pierwszy argument przyjmuje uniform sampler’a tekstury, a jako drugi argument przyjmuje współrzędną tekstury. Funkcja texture pobiera odpowiednią wartość koloru, używając wcześniej ustawionych parametrów tekstury. Wynikiem FS jest (filtrowany) kolor tekstury dla danej współrzędnej tekstury (która wcześniej została interpolowana).
Pozostaje tylko aktywowanie tekstury zanim wywołamy funkcję glDrawElements. Następnie obiekt tekstury zostanie automatycznie przypisany do samplera FS:
glBindTexture(GL_TEXTURE_2D, texture);
glBindVertexArray(VAO);
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
Jeśli zrobiłeś wszystko dobrze, powinieneś zobaczyć następujący obraz:
Jeśli prostokąt jest całkowicie biały lub czarny prawdopodobnie popełniłeś po drodze błąd. Sprawdź błędy shader’ów i porównaj swój kod z kodem źródłowym aplikacji.
Jeśli Twój kod nadal nie działa lub obraz jest zupełnie czarny, kontynuuj czytanie i aż dojdziesz do ostatniego przykładu, który powinien działać. W niektórych sterownikach trzeba zawsze przypisać jednostkę tekstury (ang. texture unit) do każdego uniform sampler’a, o czym będziemy mówić dalej w tym samouczku.
Aby poszaleć, możemy również zmieszać powstały kolor tekstury z kolorami wierzchołków. Po prostu mnożymy otrzymany kolor tekstury z kolorem wierzchołka w FS:
color = texture(ourTexture, TexCoord) * vec4(ourColor, 1.0f);
Wynik powinien być mieszanką koloru wierzchołków i koloru tekstury:
Myślę, że można powiedzieć, że nasz pojemnik lubi klimat dyskoteki.
Jednostki tekstur (ang. Texture Units)
Prawdopodobnie zastanawiałeś się, dlaczego zmienna sampler2D jest uniformem, skoro nawet nie przypisaliśmy jej żadnej wartości za pomocą funkcji glUniform. W rzeczywistości, używając funkcji glUniform1i możemy przypisać wartość lokalizacji do sampler’a tekstury, abyśmy mogli korzystać z wielu tekstur na raz w FS. Ta lokalizacja tekstury jest bardziej znana jako jednostka tekstury (ang. Texture Units). Domyślną jednostką tekstury jest wartość 0, która jest domyślną aktywną jednostką tekstur, dlatego nie musieliśmy przypisywać lokalizacji w poprzedniej sekcji; Pamiętaj, że nie wszystkie sterowniki graficzne przypisują domyślną jednostkę tekstury, więc poprzednia część mogła nie renderować się poprawnie.
Głównym celem jednostek teksturowych jest umożliwienie nam używania w naszych shader’ach więcej niż 1 tekstury. Poprzez przyporządkowanie jednostek tekstur do sampler’ów można powiązać wiele tekstur na raz, o ile najpierw uaktywnimy odpowiednią jednostkę tekstury. Podobnie jak w przypadku funkcji glBindTexture możemy aktywować jednostki tekstur za pomocą glActiveTexture przekazując jednostkę tekstury, którą chcemy użyć:
glActiveTexture(GL_TEXTURE0); // Aktywuj jednostkę tekstury przed aktywowaniem obiektu tekstury
glBindTexture(GL_TEXTURE_2D, texture);
Po aktywowaniu jednostki tekstury, kolejne wywołanie funkcji glBindTexture powiąże obiekt tekstury z aktualnie aktywną jednostką tekstury. Jednostka tekstury GL_TEXTURE0 jest zawsze domyślnie aktywna, więc nie musieliśmy włączać żadnych jednostek tekstur w poprzednim przykładzie kiedy korzystaliśmy z glBindTexture.
Sterownik OpenGL powinien wspierać co najmniej 16 jednostek tekstur, których można użyć od GL_TEXTURE0 do GL_TEXTURE15. Są one definiowane w ustawionej kolejności, dzięki czemu możemy uzyskać wartość np. GL_TEXTURE8 poprzez GL_TEXTURE0 + 8, co jest przydatne, gdy musimy w pętli ustawić kilka jednostek tekstur.
Nadal jednak musimy edytować FS, aby przyjął inny sampler. To powinno być stosunkowo proste:
#version 330 core
...
uniform sampler2D ourTexture1;
uniform sampler2D ourTexture2;
void main()
{
color = mix(texture(ourTexture1, TexCoord), texture(ourTexture2, TexCoord), 0.2);
}
Końcowy kolor wyjściowy to kombinacja dwóch próbkowań tekstur. Wbudowana funkcja GLSL mix przyjmuje dwa parametry i interpoluje je liniowo na podstawie trzeciego argumentu. Jeśli trzecia wartość jest 0.0, to zwraca pierwszy argument; jeśli 1.0, to zwraca drugi parametr. Wartość 0.2 zwróci 80% wartości pierwszego argumentu i 20% wartości drugiego argumentu, co powoduje mieszanie koloru obu tekstur.
Teraz chcemy załadować i stworzyć inną teksturę; powinieneś już wiedzieć jak to zrobić. Upewnij się, że tworzysz inny obiekt tekstury, ładujesz inny obraz i zapisujesz dane obrazu do obiektu tekstury za pomocą glTexImage2D. W przypadku drugiej tekstury użyjemy ekspresji twarzy podczas nauki OpenGL.
Aby użyć drugiej tekstury (i pierwszej tekstury), musimy nieco zmienić procedurę renderowania, łącząc oba obiekty tekstur z odpowiednią jednostką tekstury:
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);
Musimy również powiedzieć OpenGL, do której jednostki tekstury należy każdy z samplerów poprzez użycie glUniform1i. Musimy to ustawić tylko raz, więc możemy to zrobić przed wejściem do pętli renderującej:
ourShader.use(); // nie zapomnij aktywować programu cieniującego przed ustawianiem uniform'ów!
glUniform1i(glGetUniformLocation(ourShader.ID, "texture1"), 0); // ustaw ręcznie
ourShader.setInt("texture2", 1); // lub za pomocą klasy Shader
while(...)
{
[...]
}
Poprzez ustawienie samplerów za pomocą glUniform1i upewniamy się, że każdy sampler odpowiada poprawnej jednostce tekstury. Powinieneś uzyskać następujący wynik:
Prawdopodobnie zauważyłeś, że tekstura jest odwrócona do góry nogami! Dzieje się tak, ponieważ OpenGL spodziewa się, że współrzędna 0.0 na osi y znajduje się w dolnej części obrazu, ale zdjęcia zazwyczaj traktują współrzędną 0.0 znajdującą się na górze osi y . Na szczęście, stb_image.h może odwrócić obraz względem osi Y podczas ładowania obrazu. Możemy to zrobić dodając następującą linijkę przed załadowaniem dowolnego obrazu:
stbi_set_flip_vertically_on_load(true);
Po powiedzeniu stb_image.h żeby odwrócił oś y podczas ładowania obrazu, powinieneś otrzymać następujący wynik:
Jeśli widzisz uśmiechnięty pojemnik, zrobiłeś wszystko dobrze. Możesz porównać kod z kodem źródłowym.
Ćwiczenia
Aby bardziej zaznajomić się z teksturami, przed kontynuowaniem zalecam przeanalizowanie tych ćwiczeń.
- Spraw by tylko uśmiechnięta twarz patrzyła w przeciwną stronę / w drugim kierunku, poprzez zmienienie Fragment Shader’a: rozwiązanie.
- Poeksperymentuj z różnymi metodami zawijania tekstur, określając współrzędne tekstury w zakresie od 0.0f do 2.0f zamiast 0.0f do 1.0f. Sprawdź, czy możesz wyświetlać 4 uśmiechnięte twarze na pojedynczym obrazie pojemnika: rozwiązanie, wynik. Możesz również poeksperymentować z innymi metodami zawijania.
- Spróbuj wyświetlić tylko środkowe punkty obrazu tekstury w taki sposób, aby poszczególne piksele były widoczne poprzez zmianę współrzędnych tekstury. Spróbuj ustawić metodę filtrowania na GL_NEAREST, aby zobaczyć piksele bardziej wyraźnie: rozwiązanie.
- Użyj zmiennej uniform, jako trzeciego parametru funkcji mix, aby zmienić wartość widoczności obu tekstur. Użyj klawiszy strzałek w górę i w dół, aby zmienić wartość widoczności tekstury pojemnika i buźki: rozwiązanie.