# 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!

:::
:::{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$.

:::
## 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:


:::
:::{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.
:::