# OpenGL i Grafika 3D :::{note} To jest bardzo eksperymentalna wersja skryptu, większość tekstu na ten moment jest wygenerowana przez LLM na podstawie moich rozwiązań zadań i notatek do nich. ::: ## Wprowadzenie do OpenGL: `gl_lib.py` Dzisiaj przenosimy się w trzeci wymiar. Do wyświetlania grafiki będziemy używać biblioteki OpenGL, a konkretnie pomocniczej biblioteki `gl_lib.py`, która upraszcza interakcję z systemem. W grafice 3D wszystko budujemy z trójkątów. Każdy punkt (wierzchołek) w przestrzeni jest opisywany przez **współrzędne jednorodne** (rzutowe), czyli cztery liczby $(x, y, z, w)$. :::{important} Do folderu z rozwiązaniami dzisiejszych zadań pobierz bibliotekę `gl_lib.py` [stąd](https://gist.github.com/Quazyrog/b4c3d78dec471cb1d8c43ee7590efb4c). Jest tam też działająca biblioteka do macierzy z funkcją `mat_mul`, gdybyś nie miał jej z wcześniejszych zajęć. Gdyby nie działała, potrzebujesz biblioteki [PyOpenGL](https://pypi.org/project/PyOpenGL/), zainstaluj ją (i zależności): ``` sudo apt install libglut-dev python3 -m venv _env source _env/bin/activate ``` ::: ### Dlaczego cztery współrzędne? Używamy czterech współrzędnych zamiast trzech z dwóch głównych powodów: 1. **Translacja jako mnożenie**: Pozwala to na zapisanie przesunięcia (translacji) jako operacji mnożenia macierzy, co ujednolica wszystkie przekształcenia geometryczne. 2. **Perspektywa i rzutowanie**: Czwarta współrzędna, $w$, odgrywa kluczową rolę w rzutowaniu obiektów 3D na płaski ekran 2D. Współrzędną $w$ możemy interpretować jako **odwrotność skali** punktu. Aby przejść z zapisu jednorodnego $(x, y, z, w)$ do standardowych współrzędnych 3D $(x', y', z')$, wykonujemy dzielenie: $$x' = \frac{x}{w}, \quad y' = \frac{y}{w}, \quad z' = \frac{z}{w}$$ - Jeśli $w = 1$, to współrzędne $(x, y, z)$ odpowiadają bezpośrednio pozycji w przestrzeni. - Jeśli zwiększymy $w$ (np. $w=2$), to po podzieleniu przez nie, punkt "skurczy się" i znajdzie bliżej środka układu współrzędnych (efekt oddalenia). - Jeśli $w$ dąży do zera, punkt oddala się do nieskończoności. W grafice komputerowej punkty o $w=0$ reprezentują kierunki (wektory), które nie mają konkretnej pozycji, a jedynie zwrot. Większość naszych dzisiejszych macierzy będzie zachowywać $w=1$, ale warto pamiętać, że to właśnie manipulacja tą czwartą liczbą pozwala uzyskać efekt perspektywy (im coś jest dalej, tym bardziej "ściska się" w stronę środka ekranu). ### Układ współrzędnych OpenGL używa specyficznego układu współrzędnych: - Wszystkie widoczne obiekty muszą mieścić się w przedziale $[-1, 1]$ dla każdej z osi $x, y, z$. Punkty poza tym obszarem zostaną "obcięte". - Oś $x$ rośnie w prawo, oś $y$ rośnie w górę (podobnie jak w kartezjańskim układzie 2D). - Oś $z$ określa głębię. Im mniejsza (bardziej ujemna) wartość $z$, tym przedmiot jest **bliżej** widza. Obiekty o mniejszym $z$ będą zasłaniać te o większym $z$ (jest to tzw. z-buffering). Przeanalizuj plik `example.py`, aby zobaczyć, jak tworzone są kształty i jak działa prosta animacja. Zwróć uwagę na to, który trójkąt zasłania pozostałe. ## Wymagania z poprzednich zajęć Do realizacji dzisiejszych zadań niezbędna będzie Twoja implementacja z poprzednich warsztatów. Upewnij się, że w folderze z zadaniami znajduje się plik `matrices.py` zawierający: - **Klasę `Matrix`**: obsługującą tworzenie macierzy, pobieranie i ustawianie wartości elementów (`get`, `set`) oraz przechowującą wymiary (`nrows`, `ncols`). - **Funkcję `mat_mul(A, B)`**: wykonującą mnożenie macierzy przez macierz lub macierzy przez wektor (pamiętaj, że nasza klasa `Vector` również udostępnia metody `nrows`, `ncols` i `get`). W dzisiejszych zadaniach będziemy używać głównie macierzy o wymiarach $4 \times 4$ oraz wektorów $4 \times 1$ (opakowanych w klasę `Vector` gl_lib). ## Dokumentacja biblioteki `gl_lib.py` Zanim przejdziemy do zadań, zapoznajmy się z interfejsem biblioteki, z której będziemy korzystać. ### Klasa `Vector` Klasa ta reprezentuje punkt lub wektor w przestrzeni 4D. Posiada pola `x, y, z, w`. Co istotne, klasa ta została zaprojektowana tak, aby mogła być traktowana jako **macierz o wymiarach $4 \times 1$**, co pozwala na bezpośrednie używanie jej w operacjach mnożenia macierzy: - `nrows()` — zwraca liczbę wierszy (zawsze 4). - `ncols()` — zwraca liczbę kolumn (zawsze 1). - `get(row, col)` — pozwala pobrać wartość danej współrzędnej (gdzie `row` to kolejno 0 dla `x`, 1 dla `y`, itd.). ### Klasa `Triangle` Będziemy z niej korzystać do rysowania: - Tworzenie: `t = Triangle(v1, v2, v3, rgb(r, g, b))` - gdzie `v1,v2,v3` to obiekty klasy `Vector`, a `r,g,b` to liczy od 0 do 255 oznaczające składowe koloru trójkąta (czerwoną, zieloną i niebieską). - Rysowanie `t.draw()`. - Dostęp do wierzchołków: `t.p1, t.p2, t.p3`. **Przykład rysowania trójkąta:** ```python # Lista trzech wierzchołków tworzy jeden trójkąt Triangle( Vector(-0.5, -0.5, 0.10), Vector(+0.5, -0.5, 0.10), Vector( 0.0, 0.5, 0.10), rgb(255, 0, 0) # czerwony ).draw() ``` ### Funkcja `set_render_function(render_function, width, height)` Rejestruje Twoją funkcję, która będzie odpowiedzialna za rysowanie każdej klatki animacji. Funkcja ta zostanie wywołana przez bibliotekę automatycznie wiele razy na sekundę. Twoja funkcja renderująca (np. `def render(time):`) przyjmuje jeden parametr — **upływ czasu w sekundach** od momentu uruchomienia programu. Możesz go użyć do tworzenia animacji (np. uzależniając kąt obrotu od czasu). ## Analiza przykładu `example.py` Uruchom plik `example.py` i przyjrzyj się wynikowi. Program ten ilustruje kilka kluczowych zasad działania OpenGL. Poniżej znajduje się pełny kod przykładu z komentarzami: ```python from gl_lib import Vector, Triangle, set_render_function, rgb, run import math def anim(T): # Czerwony trójkąt - statyczny, środek głębi (z = 0.1) # Znajduje się "pomiędzy" niebieskim a żółtym. Triangle( Vector(-0.5, -0.5, 0.10), Vector(+0.5, -0.5, 0.10), Vector( 0.0, 0.5, 0.10), rgb(255, 0, 0) ).draw() # Żółty trójkąt - animowany # Zmienia swoją współrzędną Z jednego z wierzchołków w czasie (funkcja sin). # Dzięki temu "przebija" się przez czerwony trójkąt. Triangle( # Te dwa wierzchołki mają bardziej dodatnie współrzędne - są bliżej dalej Vector(-0.5, -0.5, 0.15), Vector(+0.5, -0.5, 0.15), # Rusza się Vector( 0.0, 0.4, 0.05 + 0.1 * math.sin(T)), rgb(255, 255, 0) ).draw() # Niebieski trójkąt - najbliżej widza (z = -0.1) # W OpenGL mniejsze Z oznacza obiekt bliżej monitora. # Niebieski trójkąt zawsze zasłania pozostałe w tym przykładzie. Triangle( Vector(-0.25, -0.25, -0.1), Vector(+0.25, -0.25, -0.1), Vector( 0.0, 0.2, -0.1), rgb(0, 0, 255) ).draw() if __name__ == "__main__": # Rejestrujemy funkcję anim i otwieramy okno 800x600 set_render_function(anim, 800, 600) ``` 1. **Układ współrzędnych**: Zwróć uwagę, że wszystkie wierzchołki mają współrzędne z przedziału $[-1, 1]$. 2. **Z-buffering (kolejność rysowania)**: - Niebieski trójkąt ma współrzędną $z = -0.1$. - Czerwony trójkąt ma $z = 0.1$. - Ponieważ $-0.1 < 0.1$, niebieski trójkąt znajduje się **bliżej** monitora i zasłania czerwony, mimo że w kodzie mógłby być zdefiniowany wcześniej lub później. 3. **Przenikanie obiektów**: Żółty trójkąt zmienia swoją współrzędną $z$ wierzchołka w czasie. Możesz zaobserwować, jak "przechodzi" on przez czerwony trójkąt — w miejscach, gdzie jego $z$ jest mniejsze od $0.1$, jest widoczny z przodu, a gdy $z$ staje się większe, znika "za" czerwonym kształtem. ## Reprezentacja obiektów 3D Obiekty w 3D są reprezentowane przez listę trójkątów. :::{topic} Zadanie 1: Macierz identycznościowa i rysowanie Utwórz plik `zadanie_01.py`. Zacznij od wklejenia tam poniższego kodu: ```python from gl_lib import Vector, Triangle, set_render_function, rgb, run from matrices import Matrix, mat_mul import math def identity(): m = Matrix(4, 4) for i in range(4): m.set(i, i, 1) return m def square(): v0 = Vector(-0.5, -0.5, 0.0) v1 = Vector( 0.5, -0.5, 0.0) v2 = Vector(-0.5, 0.5, 0.0) v3 = Vector( 0.5, 0.5, 0.0) c = rgb(255, 255, 255) return [ Triangle(v0, v1, v2, c), Triangle(v1, v2, v3, c), ] def transform_point(mat, pt): ... # TODO def draw(mat, triangles): ... # TODO def render(time): draw(Identity(), square()) if __name__ == "__main__": set_render_function(render, 800, 600) ``` Twoim zadaniem jest: 1. Napisanie funkcji `transform_point`, która przemnoży wektor `pt` przez macierz `mat` i zwróci wynik **jako wektor**. 2. Uzupełnienie funkcji `draw`, która dla wszystkich trójkątów z listy `triangles` narysuje te trójkąty po przekształceniu przez macierz `mat`. (Tzn.: przemnoży wszystkie wierzchołki każdego trójkąta przez `mat` i narysuje wynik). **Nie zmieniaj trójkątów z listy `triangles` — w kolejnych zadaniach będziemy wołać funkcję `draw` wiele razy dla tej samej listy i różnych macierzy! ![Wynik zadania 1](images/Zadanie_01.png) ::: :::{note} Możesz (bardzo słusznie) zastanawiać się, dlaczego narysowany kwadrat nie jest kwadratowy. Powodem jest omówione wcześniej ograniczenie, że wszystkie rysowane obiekty muszą znajdować się w sześcianie (clipping space). Sześcian jest mapowany na okienko programu, które nie jest prostokątne, w więc kształt kwadratu jest zniekształcony przez rozmiar okna. Na razie możesz to "naprawić" (jeśli Ci to przeszkadza) zmieniając wywołanie funkcji `run()`, żeby okno było kwadratowe. W przyszłości (w dalszej części skryptu, hopefully) dowiesz się, jak naprawić to na poziomie przekształceń nakładanych na wyświetlane obiekty. ::: ## Przekształcenia w 3D Podobnie jak w 2D, możemy używać macierzy do przesuwania i skalowania obiektów. Dzięki temu możemy zdefiniować jeden model (np. drzewo) i rysować go wielokrotnie w różnych miejscach i rozmiarach. Macierz translacji (przesunięcia) o wektor $[t_x, t_y, t_z]$ ma postać: $$T = \begin{pmatrix} 1 & 0 & 0 & t_x \\ 0 & 1 & 0 & t_y \\ 0 & 0 & 1 & t_z \\ 0 & 0 & 0 & 1 \end{pmatrix}$$ Macierz skalowania o współczynnik $s$ (wzdłuż wszystkich osi): $$S = \begin{pmatrix} s & 0 & 0 & 0 \\ 0 & s & 0 & 0 \\ 0 & 0 & s & 0 \\ 0 & 0 & 0 & 1 \end{pmatrix}$$ :::{topic} Zadanie 2: Skalowanie i translacja W pliku `zadanie_02.py` zaimplementuj funkcje `translate(x, y, z)` oraz `scale(s)`. Następnie zmodyfikuj funkcję `render`, aby wyświetlić siatkę $3 \times 2$ małych kwadratów w różnych miejscach ekranu, korzystając z jednej listy zwróconej przez funkcję `square()`. **Uwaga:** Pamiętaj o kolejności mnożenia macierzy! Aby najpierw przeskalować, a potem przesunąć, obliczamy $M = T \cdot S$. ![Wynik zadania 2](images/Zadanie_02.png) ::: ## Modelowanie sześcianu Sześcian ma 8 wierzchołków i 6 ścian. Każda ściana kwadratowa musi zostać podzielona na dwa trójkąty. :::{topic} Zadanie 3: Budowa sześcianu W pliku `zadanie_03.py` stwórz funkcję `cube()`, która zwróci listę trójkątów tworzących sześcian. 1. Dodaj 8 punktów odpowiadających wierzchołkom sześcianu o boku 1 (od -0.5 do 0.5 na każdej osi). 2. Dodaj 6 ścian, każda w innym kolorze, zbudowana z dwóch trójkątów. Użyj czytelnych nazw dla zmiennych przechowujących numery wierzchołków, np. `v_left_top_front`. Poniższy rysunek pomocniczy pokazuje rozmieszczenie wierzchołków oraz przykładowy podział ścian na trójkąty: ![Schemat sześcianu z opisem wierzchołków](images/cube_labeled.png) ![Wynik zadania 3](images/Zadanie_03.png) ::: :::{note} Możesz się zastanawiać, dlaczego każdy z sześcianów jest narysowany tak, jakby był widziany dokładnie na wprost, i nie widać w nim żadnych ścian poza przednią. Rzut perspektywiczny również należy do rzeczy, które rozwiązuje się przy pomocy przekształcania obiektów na scenie przez odpowiednią macierz i jest (będzie) to opisane w dalszej części skryptu. ::: ## Obroty w 3D Obroty w 3D wykonujemy wokół jednej z osi układu współrzędnych. Wokół osi X: $$R_x(\alpha) = \begin{pmatrix} 1 & 0 & 0 & 0 \\ 0 & \cos\alpha & -\sin\alpha & 0 \\ 0 & \sin\alpha & \cos\alpha & 0 \\ 0 & 0 & 0 & 1 \end{pmatrix}$$ Wokół osi Y: $$R_y(\alpha) = \begin{pmatrix} \cos\alpha & 0 & \sin\alpha & 0 \\ 0 & 1 & 0 & 0 \\ -\sin\alpha & 0 & \cos\alpha & 0 \\ 0 & 0 & 0 & 1 \end{pmatrix}$$ Wokół osi Z: $$R_z(\alpha) = \begin{pmatrix} \cos\alpha & -\sin\alpha & 0 & 0 \\ \sin\alpha & \cos\alpha & 0 & 0 \\ 0 & 0 & 1 & 0 \\ 0 & 0 & 0 & 1 \end{pmatrix}$$ :::{topic} Zadanie 4: Animowane obroty W pliku `zadanie_04.py` zaimplementuj funkcje `rotate_x`, `rotate_y` i `rotate_z`. W funkcji `render` spraw, aby sześciany obracały się wokół różnych osi z prędkością zależną od czasu (`time`). Sprawdź, czy sześcian wygląda poprawnie z każdej strony. ::: ## Składanie przekształceń - Układ Słoneczny Największą zaletą macierzy jest możliwość ich składania. Jeśli chcemy, aby Księżyc krążył wokół Ziemi, a Ziemia wokół Słońca, możemy to osiągnąć przez odpowiednie mnożenie macierzy pozycji. :::{topic} Zadanie 5: Układ Słoneczny W pliku `zadanie_05.py` stwórz animację układu słonecznego: 1. **Słońce:** Znajduje się w środku, obraca się wokół własnej osi. 2. **Planeta:** Krąży wokół Słońca. Jej macierz to połączenie skalowania, obrotu własnego, translacji (odległość od Słońca) i obrotu orbitalnego. 3. **Księżyc:** Krąży wokół planety. Jego macierz musi uwzględniać pozycję planety (ale bez jej własnego obrotu i skalowania!). Zastanów się, w jakiej kolejności mnożyć macierze, aby uzyskać pożądany efekt "przyczepienia" jednego obiektu do drugiego. :::