OpenGL i Grafika 3D¶
Informacja
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)\).
Ważne
Do folderu z rozwiązaniami dzisiejszych zadań pobierz bibliotekę gl_lib.py stąd.
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, 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:
Translacja jako mnożenie: Pozwala to na zapisanie przesunięcia (translacji) jako operacji mnożenia macierzy, co ujednolica wszystkie przekształcenia geometryczne.
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 klasaVectorrównież udostępnia metodynrows,ncolsiget).
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 (gdzierowto kolejno 0 dlax, 1 dlay, itd.).
Klasa Triangle¶
Będziemy z niej korzystać do rysowania:
Tworzenie:
t = Triangle(v1, v2, v3, rgb(r, g, b))- gdziev1,v2,v3to obiekty klasyVector, ar,g,bto 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:
# 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:
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)
Układ współrzędnych: Zwróć uwagę, że wszystkie wierzchołki mają współrzędne z przedziału \([-1, 1]\).
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.
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.
Informacja
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}\)$
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.
Informacja
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}\)$
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.