109 lines
8.9 KiB
Markdown
109 lines
8.9 KiB
Markdown
|
# 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 zamiast `programSun` nazwij zmienną `programTex`).
|
||
|
- Prześlij globalne *uniformy* do `programTex`.
|
||
|
|
||
|
b) ładowanie tekstury:
|
||
|
- Pod `namespace texture` znajdują się zmienne globalne typu `Gluint`, które będą przechowywać adresy tekstur. W funkcji `init()` załaduj tekstury z folderu **textures** przy pomocy `Core::LoadTexture`. Ta funkcja zwraca adres do wczytanej tekstury, przypisz je do zmiennych z `namespace texture`.
|
||
|
- (odwołanie się do `namespace` wykonuje się za pomocą ::, czyli by odwołać się do `grid` w `namespace` `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 zmienna `in` we *fragment shaderze*)
|
||
|
|
||
|
c) Prześlij teksturę do fragment shadera:
|
||
|
- Stwórz zmienną typu `uniform sampler2D` (nazwij ją na przykład `colorTexture`) 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ątrz `drawObjectTexture()` 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](https://www.solarsystemscope.com/textures/) lub [link2](https://stevealbers.net/albers/sos/sos.html)
|
||
|
|
||
|
|
||
|
### 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
|
||
|
|
||
|
1) 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` i `vertexBitangent` 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 zmiennych `normal`, `tangent` i `bitangent`.
|
||
|
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).
|
||
|
|
||
|
2. Przenieś wektor światła i wektor widoku do przestrzeni stycznych
|
||
|
1. 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` i `cameraPos`) z **fragment shadera** do **vertex shadera.**
|
||
|
|
||
|
2. Oblicz wektor `viewDir` jako znormalizowana różnice `cameraPos` i `fragPos` (tu jeszcze działamy w przestrzeni świata). Analogicznie oblicz `lightDir` jako różnicę `lightPos` i `fragPos`
|
||
|
|
||
|
3. Przekształć wektory `viewDir` i `lightDir` do przestrzeni stycznej mnożąc je przez macierz **TBN**. Wynik zapisz w zmiennych`viewDirTS` i `lightDirTS` odpowiednio.
|
||
|
|
||
|
4. Przekaż `viewDirTS` i `lightDirTS` do fragment shadera. (zadeklaruj je jako zmienne `out`)
|
||
|
|
||
|
> (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.)
|
||
|
|
||
|
3. Przekształć **fragment shader**, by obsługiwał **tangent space**
|
||
|
1. 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.
|
||
|
2. wektora `lightDir` powinniśmy użyć wektora `lightDirTS` (należy go dodatkowo znormalizować), a jako wektor widoku V powinniśmy użyć wektora `viewDirTS` (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`.
|
||
|
|
||
|
|
||
|
|
||
|
|
||
|
|
||
|
|
||
|
|
||
|
|
||
|
|
||
|
|
||
|
|