9.0 KiB
Executable File
II Teksturowanie
II.0 Obejrzyj plik Texture.h aby zapoznać się z nowym interfejsem do obsługi tekstur.
II.1 Przygotuj shadery i załaduj teksturę:
a) W kolejnych zadaniach będziemy potrzebować trzecią parę shaderów: shader_4_tex.vert i shader_4_tex.frag są one gotowymi shaderami z poprzednich zadań.
- Wczytaj shadery i przechowaj adres programu pod zmienną globalną
programTex
(tzn. powtórz dla nich punkt 6.a, ale zamiastprogramSun
nazwij zmiennąprogramTex
). - Prześlij globalne uniformy do
programTex
.
b) ładowanie tekstury:
- Pod
namespace texture
znajdują się zmienne globalne typuGluint
, które będą przechowywać adresy tekstur. W funkcjiinit()
załaduj tekstury z folderu textures przy pomocyCore::LoadTexture
. Ta funkcja zwraca adres do wczytanej tekstury, przypisz je do zmiennych znamespace texture
. - (odwołanie się do
namespace
wykonuje się za pomocą ::, czyli by odwołać się dogrid
wnamespace
texture
należy napisaćtexture::grid
)
II.2. Stwórz funkcje odpowiadająca za rysowanie teksturowanych obiektów:
- Skopiuj funkcję
drawObject
. Nazwij kopiędrawObjectTexture()
i zmodyfikują ją tak, aby przyjmowała jako parametr identyfikator tekstury, a nie wektor koloru. (Usuń w niej linijkę odpowiadającą za przesyłanie koloru, żeby uniknąć błędu komunikacji, później dodamy na jej miejsce ładowanie tekstury).
II.3. Zmodyfikuj shader tak, aby nakładał teksturę na obiekt:
a) Narysuj jedną z planet za pomocą drawObjectTexture()
, użyj programTex
jako programu.
b) Prześlij współrzędne mapowania tekstur z vertex shadera do fragment shadera
- Współrzędne tekstur to kolejny (po pozycjach i wektorach normalnych) atrybut wierzchołków - są dostępne w vertex shaderze pod nazwą
vertexTexCoord
- Prześlij je znanym już sposobem do fragment shadera (zmienna
out
w vertex shaderze i odpowiadająca jej zmiennain
we fragment shaderze)
c) Prześlij teksturę do fragment shadera:
- Stwórz zmienną typu
uniform sampler2D
(nazwij ją na przykładcolorTexture
) we fragment shaderze - analogicznie do innych zmiennych typu uniform, służy ona do przesyłania informacji bezpośrednio z kodu C++ do shadera - Po stronie kodu C++ użyj funkcji
Core::SetActiveTexture
wewnątrzdrawObjectTexture()
aby ustawić zmiennąsampler2D
na wczytaną wcześniej teksturę
d) Użyj wartości uzyskanej z tekstury zamiast koloru (objectColor
) do pokolorowania obiektu:
- Wykonaj próbkowanie tekstury we współrzędnych otrzymanych przez fragment shader:
vec4 textureColor = texture2D(~nazwaZmiennejSampler2D, ~nazwaZmiennejWspolrzedneTekstury)
" (vec4
zawiera kolor RGBA) - Użyj pierwszych trzech współrzędnych (RGB) uzyskanego wektora jako nowego koloru bazowego piksela
II.4. Pobaw się mechanizmem teksturowania:
- Przemnóż jedną lub obie ze współrzędnych mapowania przez 5 i sprawdź co się stanie
- Wypróbuj pozostałe tekstury: grid_color.png, earth.png i earth2.png.
- Tekstury Ziemi wyświetlają się "do góry nogami". Napraw to.
- Jeśli chcesz mieć kilka planet o różnych teksturach możesz skorzystać z link lub link2
II.5*. Teksturowanie proceduralne
a) Stwórz czwartą parę plików z shaderami (np. shader_proc_tex.vert i shader_proc_tex.frag). Następnie zainicjalizuj program jak w I.6.a) i II.1.a) (nazwij go np programProcTex
). Do wywołania rysowania wykorzystuj funkcję drawObject
(Żeby nie tracić oteksturowanych planet, możesz rysować za ich pomocą wyłącznie statek).
b) Prostym sposobem proceduralnego teksturowania, jest uzależnienie koloru od pozycji piksela w przestrzeni lokalnej (użycie przestrzeni świata spowodowałoby, że wzór na obiekcie zmieniałby się przy poruszaniu go).
- Prześlij z vertex shadera do fragment shadera pozycję wierzchołka w przestrzeni lokalnej (czyli tej, w której wyrażone są atrybuty wierzchołka - nie trzeba więc wykonywać żadnej transformacji macierzowej)
- We fragment shaderze oblicz sinus współrzędnej y pozycji piksela
- Jeżeli sinus jest większy od zera, to ustaw bazowy kolor obiektu na wybrany kolor, a jeśli jest mniejszy od zera, to na inny kolor
- Możesz przesłać te kolory przy użyciu zmiennych uniform z kodu C++ - pozwoli to rysować różne obiekty z różnymi parami kolorów
- Poeksperymentuj z innymi metodami teksturowania proceduralnego podanymi na wykładzie
III Normal Mapping
W tej części będziemy dalej modyfikować shadery shader_4_tex poprzez implementację normal mappingu
Obliczenia dla map normalnych należy wykonywać w przestrzeni stycznych. Przestrzeń styczna jest wyliczana dla każdego punktu w obiekcie. Jej celem jest takie przekształcenie przestrzeni, żeby wektor normalny był wektorem jednostkowym (0,1,0).
Do wyliczenia przestrzeni stycznej potrzebujemy dla każdego wierzchołka oprócz wektora normalnego wektor styczny i bistyczny (tangent i bitangent). Są one wyliczane przez bibliotekę Assimp
.
III.0 Wykonaj kopię shaderów shader_4_tex.vert shader_4_tex.frag
III.1 Przenieś obliczenia światła do przestrzeni stycznej
-
Oblicz macierz TBN.
Macierz TBN to macierz 3x3 wyznaczana przez wektory tangent, bitangent i normal, służy do przenoszenia wektorów z przestrzeni świata do przestrzeni stycznej.
1. W vertex shaderze przekrztałć wektory
vertexNormal
,vertexTangent
ivertexBitangent
do przestrzeni świata (przemnóż macierz modelu przez te wektory, tak jak to robiliśmy wcześniej z wektorem normalnym, z uwzględnieniem zmiennej w=0) i zapisz wyniki odpowiednio w zmiennychnormal
,tangent
ibitangent
. 2. Stwórz macierz 3x3 TBN jako transpose(mat3(tangent, bitangent, normal)). Macierz transponujemy, aby szybko uzyskać jej odwrotność (możemy tak zrobić przy założeniu, ze jest ortogonalna).
-
Przenieś wektor światła i wektor widoku do przestrzeni stycznych
-
Musimy przekształcić wektor światła (L) i wektor widoku (V) do przestrzeni stycznych. Co ważne, zrobimy to w vertex shaderze. W tym celu przenieś potrzebne dane dotyczące światła i kamery (uniformy
lightPos
icameraPos
) z fragment shadera do vertex shadera. -
Oblicz wektor
viewDir
jako znormalizowana różnicecameraPos
ifragPos
(tu jeszcze działamy w przestrzeni świata). Analogicznie obliczlightDir
jako różnicęlightPos
ifragPos
-
Przekształć wektory
viewDir
ilightDir
do przestrzeni stycznej mnożąc je przez macierz TBN. Wynik zapisz w zmiennychviewDirTS
ilightDirTS
odpowiednio. -
Przekaż
viewDirTS
ilightDirTS
do fragment shadera. (zadeklaruj je jako zmienneout
)(Sufiks TS oznacza tangent space. Wazne jest, aby oznaczac (np. dopisujac cos do nazwy zmiennej) w jakiej przestrzeni znajduja sie uzywane wektory, tak aby poprawnie wykonywac obliczenia. Trzeba zawsze zwracac uwage na to, w jakiej przestrzeni dzialamy.)
-
-
Przekształć fragment shader, by obsługiwał tangent space
- Nie potrzebujemy już we fragment shaderze informacji na temat pozycji fragmentu i wektora normalnego geometrii, skasuj wiec zmienne przekazujące te wektory pomiędzy shaderami.
- wektora
lightDir
powinniśmy użyć wektoralightDirTS
(należy go dodatkowo znormalizować), a jako wektor widoku V powinniśmy użyć wektoraviewDirTS
(również należy go znormalizować). Jako wektora N użyj na razie wektora vec3(0,0,1).
Efekt finalny powinien wyglądać tak samo jak w przed jakąkolwiek zmianą. Następnym krokiem będzie wykorzystanie map normalnych
III.2 Wykorzystaj normalmapy
1. Chcemy wczytywać normalne z tekstury, w tym celu dodaj we **fragment shaderze** dodatkowy sampler do czytania map normalnych, nazwij go `normalSampler`. Pobierz z niego wektor normalny analogicznie, jak czytamy kolor zwykłej tekstury z samplera `textureSampler` i zapisz go w zmiennej `N`.
2. Ponieważ w teksturze wartości są w przedziale [0,1], musimy jeszcze przekształcić je do przedziału [-1,1]. W tym celu przemnóż wektor N przez 2 i odejmij 1.
Na koniec warto jeszcze znormalizować wektor normalny, aby uniknąć błędów związanych z precyzja lub kompresja tekstury.
3. Wczytaj pliki zawierające mapy normalnych w kodzie C++ W tym celu załaduj przy użyciu funkcji `Core::LoadTexture` mapy normalnych dla wszystkich modeli. Maja one taką sama nazwę jak zwykle tekstury, tyle ze z suffiksem "_normals".
4. Zmodyfikuj na koniec funkcje `drawObjectTexture`. Dodaj do niej nowy argument `GLuint normalmapId`, który będzie służył do przekazywania id tekstury zawierającej mapę normalnych. Przy użyciu funkcji `Core::SetActiveTexture` załaduj `normalmapId` jako `normalSampler` i ustaw jednostkę teksturowania nr 1.
Argument odpowiadający za normalne w w miejscach wywołania funkcji `drawObjectTexture`.