Przestrzenie potoku graficznego
W trakcie wykładu zostały opisane kolejne przestrzenie potoku graficznego. W tej części zajęć przejdziemy kolejne jego etapy.
Pierwszy krok, czyli przejście do World Space
już wykonujemy za pomocą macierzy transformacji. Następnym krokiem będzie stworzenie macierzy projekcji i macierzy widoku. Zaczniemy od macierzy projekcji. Będziemy modyfikować macierz, którą wysyła funkcja createPerspectiveMatrix
(macierz jest transponowana dalej, więc zapisuj ją tak, jak tu widzisz). Zanim zaczniemy domnóż macierz perspektywy do macierzy transformacji obiektów. W tych ćwiczeniach skupimy się na matematycznych konceptach i ominiemy kwestie techniczne. Dogłębny opis (choć trochę nieaktualny) można znaleźć pod linkiem
Słownik pojęć
Zbiór pojęć, które będą wykorzystywane w trakcie zajęć. Służy jako przypomnienie zagadnień z wykładu:
- Przestrzeń Świata(World Space): Przestrzeń, w której obiekty są umieszczone względem globalnego układu odniesienia, zanim zostaną przekształcone przez kamery lub projekcje. Przejście do tej przestrzeni jest wykonywane za pomocą macierzy modelu, która jest inna dla każdego obiektu w świecie.
- Przestrzeń Widoku (View Space): Przestrzeń, w której obiekty są przedstawiane względem kamery. W tej przestrzeni kamera znajduje się w punkcie odniesienia, a wszystkie obiekty są przedstawiane względem tej kamery. Przejście do tej przestrzeni jest wykonywane za pomocą macierzy widoku.
- Przestrzeń Obcięcia (Clip Space): Przestrzeń po transformacji perspektywicznej, ale przed homogenizacją. W tej przestrzeni obiekty są przycinane, aby pasowały do bryły kanonicznej. Przejście do tej przestrzeni jest wykonywane za pomocą macierzy projekcji.
- Macierz widoku (View Matrix): Macierz reprezentująca pozycję i orientację kamery w przestrzeni 3D.
- Macierz projekcji (Projection Matrix): Macierz używana do transformacji współrzędnych obiektu z przestrzeni widoku do przestrzeni obcięcia.
- Homogenizacja: Proces przekształcania współrzędnych 3D na współrzędne 4D, aby umożliwić przekształcenia w przestrzeni projekcji.
- Z-fighting: Problem w grafice komputerowej, gdy dwa lub więcej obiektów zajmuje dokładnie to samo miejsce w przestrzeni kamery, prowadząc do * nieprawidłowego renderowania.
- Bryła kanoniczna (Canonical Volume): Standardowa bryła, do której obiekty są przekształcane przed przycięciem i renderowaniem.
Macierz perspektywy
Nim przejdziemy dalej odkomentuj rysowanie prostopadłościanu.
Rozważmy mnożenie dowolnej macierzy przez wektor kolumnowy:
Po homogenizacji otrzymamy wektor:
Zadanie 1
zmodyfikuj macierz perspektywy w taki sposób.
Wartość współrzędnej $z$ jest równa -1 dla każdego parametru. Co spowoduje, że nie będzie wiadomo, który wierzchołek bliżej, a który dalej i otrzymam zjawisko znane jako z-fighting. By tego uniknąć. Musimy zmapować współrzędną $z$. Przypomnijmy, że przy arbitralnej macierzy wartość współrzędnej $z$ będzie następującej postaci:
Zauważ, że te wartość zmienia się zgodnie ze wzorem $-\left(\frac{(n + f)}{(n - f)}+ \frac{(2 n f)}{z(n - f)}\right)$ czyli zmienia się to asymptotycznie, co można zobaczyć na wykresie.
Zadanie 1b*
Rozwiąż samodzielnie ten układ równań.
Zadanie 2
Dodaj zmienne lokalne n
i f
w funkcji, ustal im jakieś arbitralne wartości i dodaj rzutowanie $z$ zgodnie ze wzorem, który otrzymaliśmy. Spróbuj ustawić taką wartość f
, żeby tylna ściana sześcianu zniknęła.
Uzyskana macierz daje nam rzutowanie perspektywiczne, ale możemy ją jeszcze rozbudować o zmianę kąta widzenia, a także naprawić problem z nieprawidłowym skalowaniem się ekranu przy zmianie jego proporcji. Obie te czynności sprowadzają się do tego samego, mianowicie chcemy zmienić kształt bryły widzenia w osiach $x$ i $y$. By tego dokonać, musimy zmienić wartość parametrów $m_{11}$ i $m_{22}$, to one odpowiadają za skalowanie w tych osiach. Parametry te ściskają lub rozszerzają przestrzeń w tych osiach, więc zmniejszenie wartości zwiększy kąt widzenia w danej osi.
Zacznijmy od kąta widzenia. Można go zmienić zwyczajnie ustawiając zamiast $1$ dowolną inną wartość $S$ parametrów $m_{11}$ i $m_{22}$. Jednak jeśli chcemy uzyskać faktyczne parametry oparte na polu widzenia, musimy skorzystać ze wzoru:
$$S=\frac{1}{\tan(\frac{fov}{2}*\frac{\pi}{180})}$$
Zadanie 3
Dodaj do createPerspectiveMatrix
argument fov, który będzie ustalał kąt widzenia.
Zadanie 4
Prawidłowe skalowanie okna uzyskamy poprzez mnożenie $m_{22}$ przez stosunek szerokości do wysokości ekranu. W zadaniu jest zmienna globalna aspectRatio
. W funkcji framebuffer_size_callback
nadpisz tą zmienną właściwym stosunkiem (pamiętaj, że dzielenie liczb stałoprzecinkowych w c++ nie uwzględnia ułamków, dlatego musisz je najpierw rzutować na typ float
). Następnie prześlij ja do createPerspectiveMatrix
jako dodatkowy parametr i wykorzystaj przy renderowaniu sceny.
Zadanie 4b*
Po dodaniu skalowania okna, poszerzanie kwadratowego okna będzie zmniejszać kąt widzenia na osi pionowej. Natomiast wydłużanie go będzie go zwiększać. Zaproponuj rozwiązanie, które sprawi, że poszerzanie kwadratowego okna będzie zwiększać kąt widzenia w osi poziomej i jednocześnie wydłużanie kwadratowego okna będzie zwiększać kąt widzenia w osi pionowej.
Macierz widoku
Celem macierzy widoku jest wprowadzenie pojęcia kamery, jako obiektu, który możemy ustawić i poruszać nim w przestrzeni. Na taką macierz składa się pozycja kamery kierunek patrzenia oraz jej orientacja: wektory cameraDir
oraz cameraUp
i cameraSide
. W tym celu potrzebujemy jeden wektor, który będzie określał pozycję początku układu współrzędnych (czyli pozycję kamery). Oraz 3 wektory ortonormalne, które będą rozpinać przestrzeń (odpowiedzialne za kierunek i orientację). Ponieważ te wektory są ortogonalne, możemy je rekonstruować za pomocą dwóch wektorów, jeden będzie nam wskazywać kierunek patrzenia (cameraDir
), drugi górę (cameraUp
).
Układ współrzędnych kamery
W kodzie jest zaimplementowana obsługa klawiatury, klawisze W i S przesuwają kamerę do przodu i do tyłu, natomiast A i D obracają ją na boki. Robią to poprzez modyfikacje zmiennych globalnych cameraDir
i cameraPos
, jednak, żeby kamera faktycznie działała, trzeba uzupełnić funkcję createCameraMatrix
i dodać jej wynik do transformacji obiektów.
Zadanie 5
Zmień wartości n i f odpowiadające za bliższy i dalszy plan przy tworzeniu macierzy perspektywy na n=0.1 i f=100.0
Uzupełnij funkcję createCameraMatrix
. Najpierw oblicz wektor skierowany w bok za pomocą iloczynu wektorowego między cameraDir
a wektorem $[0,1,0]$. Wektor może być długości innej niż 1, dlatego znormalizuj go. Zapisz wynik do cameraSide
. Podobnie oblicz cameraUp
jako znormalizowany iloczyn wektorowy między cameraSide
i cameraDir
.
Wektor normalizuje się za pomocą funkcji:
glm::normalize
Macierz kamery złożona jest z iloczynu macierzy obrotu i macierzy translacji. By otrzymać pierwszą z nich, korzystamy z ortonormalności bazy, dzięki temu wystarczy zapisać wektory cameraSide
, cameraUp
i -cameraDir
wierszami.
Zauważ, że
cameraDir
musi być odwrócony tak jak na obrazku, ma być zwrócony do kamery, nie od niej. Macierz wygląda następująco:
W macierzy transformation
umieść pomnożone przez siebie macierze perspektywy (wynik funkcji createPerspectiveMatrix
) i kamery (wynik funkcji createCameraMatrix
) w odpowiedniej kolejności. Jako efektem otrzymamy pełen potok graficzny, czyli kamerę, którą możemy poruszać klawiszami, z poprawnym rzutowaniem perspektywicznym.
Zadanie 6
Wyświetl dwa dodatkowe prostopadłościany w różnych pozycjach i orientacjach.
Zadanie 7*
Zmodyfikuj ustawienia tak, żeby kamera zawsze była zwrócona w punkt wybrany punkt $p$ $(0,0,0)$. W funkcji processInput
zakomentuj obsługę klawiszy A i D. Zamiast tego na końcu tej funkcji ustaw cameraDir
jako znormalizowana różnicę między punktem $p$ a cameraPos
. Jako obsługę klawiszy R i F dodaj przesuwanie kamery w górę i w dół.
Assimp - ładowanie modeli**
Wprowadzenie do Assimp:
Assimp
, skrót od “The Open Asset Import Library”, to biblioteka, która ułatwia wczytywanie modeli z różnych formatów plików. Dzięki temu, niezależnie od formatu źródłowego modelu 3D, możemy łatwo wczytać go do naszej aplikacji i wyrenderować. W tym rozdziale nauczymy się, jak korzystać z Assimp do wczytywania modeli.
W tym zadaniu przećwiczymy ładowanie modeli z plików, wykorzystamy do tego bibliotekę assimp (The Open Asset Import Library ), która zapewnia wspólny interfejs dla różnych typów plików.
Funkcja loadModelToContext
pobiera ścieżkę do pliku z modelem i wczytuje go przy użyciu importera assimp.
const aiScene* scene = import.ReadFile(path, aiProcess_TriangulateaiProcess_Triangulate | aiProcess_CalcTangentSpace);
Importer przyjmuje ścieżkę i flagi preprocesingu, które mówią, jakie operacje ma wykonać importer przed przekazaniem nam pliku. W naszym przypadku dokonuje triangularyzacji (zamienia wszystkie wielokąty na trójkąty) i oblicza przestrzeń styczną (o której będzie mowa później).
Wywołaj funkcję dla ścieżki do statku ./models/spaceship.obj i zmiennej globalnej
Core::RenderContext sphereContext
. Dodaj breakpoint po załadowaniu sceny i obejrzyj jak wygląda struktura załadowanego obiektu.
Załadowany obiekt posiada szereg pól jak na przykład tekstury, oświetlenia, materiały, węzły (Node) czy modele. Węzły odpowiadają za hierarchię elementów w modelu, co ułatwia jego animację, wykorzystamy to w późniejszych zajęciach. Nasze pliki składają się z tylko jednego modelu, dlatego nie musimy się na tym skupiać i wywołujemy tylko pierwszy model, do którego odwołujemy się za pomocą scene->mMeshes[0
]
. Wywołaj context.initFromAiMesh
z nim jako argumentem.
Zadanie 8**
Jeśli tego nie zrobiłeś wywołaj metodę context.initFromAiMesh
z argumentem scene->mMeshes[0]
w funkcji init
, po wczytaniu modelu. Metoda nie jest kompletna, uzupełnij ją o ładowanie indeksów, wierzchołków, normalnych i współrzędnych tekstur do bufora. Współrzędne tekstur i indeksy zostały przekonwertowane do odpowiedniego formatu i znajdują się w zmiennych std::vector<aiVector2D> textureCoord
i std::vector<unsigned int> indices
odpowiednio. Pozostałe są dostępne jako atrybuty aiMesh
, mianowicie mesh->mVertices
zawiera wierzchołki a mesh->mNormals
normalne.
Dodatkowo mesh->mNumVertices
zawiera liczbę wierzchołków.
zawierają rozmiary buforów.
Utwórz jedną duża tablicę/vector, która zawiera informacje o wierzchołkach, normalnych i współrzędnych tekstur. Powinna mieć ona format jak na poniższym obrazku:
Gdy załadujesz kontekst, wykorzystaj w renderScene
funkcję Core::DrawContext(Core::RenderContext& context)
do narysowania obiektów. Rozmieść statek i kulę w przestrzeni za pomocą macierzy transformacji i obrotu.