Tech Blog – MorphOS: wprowadzenie do TinyGL

Uwaga! Artykuł pochodzi z 2014 roku i może być częściowo nieaktualny.

Biblioteka TinyGL w swojej pierwotnej wersji została napisana przez Fabrice Bellarda jako okrojona wersja standardu OpenGL, przeznaczona dla urządzeń nie posiadających sprzętowej akceleracji 3D, przede wszystkim z myślą o grach. To jednak przeszłość. Rozwojem aktualnie znajdującej się w MorphOS-ie wersji zajmuje się Michał „kiero” Woźniak, a współpracują z nim Mark „bigfoot” Olsen i Frank „cyfm” Mariak. Aktualnie TinyGL wymaga sprzętowego wsparcia 3D i działa z układami graficznymi ATi (obecnie AMD) Radeon z rodziny R200 i R300.

Jeżeli chodzi o zgodność ze standardem OpenGL, biblioteka znajduje się obecnie gdzieś pomiędzy OpenGL 1.1OpenGL 2.1. Zdecydowana większość funkcji TinyGL to ścisłe odpowiedniki tych z Open GL, dzięki czemu możliwe jest w miarę bezproblemowe portowanie programów z innych systemów. W czasie poznawania Tiny GL można więc odnosić się śmiało do przylinkowanej wcześniej dokumentacji OpenGL 2.1, trzeba się jednak liczyć z tym, że ta czy inna możliwość nie jest (jeszcze) zaimplementowana. Można też wzbogacać swoją wiedzę kursami OpenGL dla innych platform, niemniej mogą one operować zaawansowanymi funkcjami z wersji 3 lub 4. Aby zachować ciągłość lektury nie będę w tym tekście co chwila odsyłał do dokumentacji i nie będę uciekał od omówienia podstawowych funkcji, z drugiej jednak strony nie będę omawiał wszystkich funkcji i ich rozlicznych parametrów – właśnie od tego jest dokumentacja.

Nazewnictwo funkcji

Standard OpenGL stosuje uporządkowany system przedrostków w nazwach funkcji, wynikających z podziału ich zastosowań. Oto te przedrostki:

Oprócz tej systematyki zauważymy jeszcze inną, mianowicie użycie dużych lub małych liter w przedrostkach. OpenGL zawsze używa małych liter. Z kolei funkcje w API biblioteki TinyGL mają przedrostki z dużych liter, natomiast małoliterowe wersje są zdefiniowane jako makra. Wynika to z faktu istnienia tak zwanego kontekstu OpenGL, a nawet dwóch kontekstów. Kontekst OpenGL to struktura zawierająca w sobie wszystkie dane związane z renderowaną sceną, zarówno podane przez nas, jak i wewnętrzne dane niezbędne bibliotece. Elementem tego kontekstu jest z kolei kontekst GLA, gromadzący dane związane z wyświetleniem sceny, a więc docelowym oknem czy ekranem, czy nawet sterownikiem karty graficznej. Zawartość tych kontekstów zazwyczaj nie interesuje nas, jako użytkowników TinyGL, niemniej musimy mieć świadomość ich istnienia.

Skoro kontekst jest taki ważny, może zaskakiwać fakt, że jakoś go nie widać w parametrach funkcji OpenGL. Po prostu standard traktuje go jako swego rodzaju parametr domyślny nie objęty specyfikacją. TinyGL rozwiązuje ten problem w specyficzny sposób. Funkcje znajdujące się bezpośrednio w API biblioteki (z „dużymi” przedrostkami) przyjmują kontekst jako jawny, pierwszy parametr. To jednakże powodowałoby problemy przy portowaniu kodu z innych platform, na których problem kontekstu rozwiązano inaczej. Osoba portująca program musiałaby albo mozolnie zmieniać wszystkie wywołania ręcznie, dodając kontekst i zmieniając wielkość liter, albo próbować robić to automatycznie. W obu przypadkach ryzyko popełnienia trudnych do znalezienia błędów jest wysokie. Dlatego wszystkie oficjalne nazwy funkcji z „małymi” przedrostkami są zdefiniowane jako makra, które wywołują wersje z jawnym kontekstem używając nazwy __tglContext. Nazwa ta musi być w naszym kodzie zdefiniowana albo jako zmienna, albo jako makro. Oto przykład, zacznijmy od niezbyt eleganckiego rozwiązania, a więc trzymania kontekstu w zmiennej globalnej:

void *__tglContext;

Kontekst stworzymy w następujący sposób:

__tglContext = GLInit();

Teraz możemy wywoływać funkcje na dwa alternatywne sposoby, zgodny z innymi platformami, albo bezpośrednio z jawnym kontekstem:

glMatrixMode(GL_PROJECTION); /* standard */ GLMatrixMode(__tglContext, GL_PROJECTION); /* bezpośrednie wywołanie API */

Jako że nadmiar zmiennych globalnych nie jest zalecany, zobaczmy jak sprawa wygląda, gdy kontekst trzymamy w jakiejś strukturze i wskaźnik na tę strukturę wszędzie przekazujemy jako parametr funkcji w naszym programie:

struct DaneGry { void *gl; /* tu trzymamy kontekst */ /* inne dane */ };

Załóżmy, że wskaźnik na strukturę DaneGry wszędzie przekazujemy jako argument funkcji w naszym programie nazwany d. Wtedy sposób korzystania z funkcji bezpośrednich wygląda następująco:

void RysujCośTam(struct DaneGry *d, ...) { GLMatrixMode(d->gl, GL_PROJECTION);

Jeżeli zaś chcemy trzymać się standardu, musimy zdefiniować __tglContext:

#define __tglContext d->gl /* umieszczone zazwyczaj w głównym pliku nagłówkowym programu */ void RysujCośTam(struct DaneGry *d, ...) { glMatrixMode(GL_PROJECTION);

Najprostszym podejściem do sprawy jest zlinkowanie programu ze statyczną biblioteką libGL. W tym przypadku wystarczy dodać opcję −lGL przy linkowaniu programu, oraz zainkludować GL/gl.h. Wtedy automatycznie zostanie otwarta biblioteka systemowa tinygl.library, automatycznie też zostanie stworzony i zainicjalizowany kontekst OpenGL (nie trzeba ręcznie wywoływać GLInit()). Do zrobienia pozostaje jedynie dołączenie kontekstu GLA, czyli wywołanie funkcji GLAInitializeContext(). Zazwyczaj przy pracy z libGL używamy standardowych nazw funkcji z przedrostkami z małych liter. Ten sposób pracy z OpenGL pod MorphOS-em jest szczególnie polecany przy przenoszeniu programów z innych systemów, albo przy pracy nad projektem wieloplatformowym. W innej sytuacji, gdy np. chcemy być po prostu „bliżej systemu”, albo chcemy mieć więcej kontekstów, musimy się nimi zająć sami.

W tekście i przykładowych programach będę pracował bezpośrednio na funkcjach tinygl.library, jest to, moim zdaniem, bardziej przejrzyste. Poza tym niektóre funkcje GLA, a więc specyficzne dla TinyGL, nie mają rzecz jasna standardowych małoliterowych wersji, trzymanie się więc API jest bardziej jednolite. Z drugiej strony myśląc o przeniesieniu kodu na inne platformy należy używać nazw standardowych.

OpenGL a przestrzeń

Chcąc wyświetlić cokolwiek przy pomocy OpenGL, rozmieszczamy obiekty, złożone z trójkątów i czworokątów, w wyobrażonej przestrzeni trójwymiarowej. Następnie OpenGL pozwala nam zajrzeć do tej przestrzeni przez powierzchnię ekranu komputera, będącą swego rodzaju „oknem” czy też „obiektywem kamery”. W uproszczeniu można powiedzieć, że cała zabawa z OpenGL polega na rozmieszczeniu obiektów na trójwymiarowej scenie, a następnie wydaniu polecenia narysowania rzutu tej sceny na prostokąt ekranu. Wszystkie związane z tym obliczenia, a więc samo rzutowanie, wyliczenia kolorów, nakładanie tekstur, oświetlanie, obcinanie wielokątów do granic okienka i tak dalej, są wykonywane przez bibliotekę, zazwyczaj z dużym wsparciem jednostki obliczeniowej karty graficznej (GPU). Jeżeli robimy animację (grę, czy demo), to po narysowaniu sceny zmieniamy współrzędne obiektów (często również obserwatora) i rysujemy następną klatkę i tak dalej.

Środek układu współrzędnych OpenGL znajduje się na środku viewportu czyli prostokąta, na którym OpenGL kreśli rzut sceny. W trybie pełnoekranowym viewportem jest ekran, w trybie okienkowym – wnętrze okienka, albo jego prostokątny fragment. Osie x i y położone są jak w matematyce, zatem na osi x wartości rosną w prawo, na osi y w górę, a więc odwrotnie niż typowo w grafice 2D. Oś z jest prostopadła do powierzchni ekranu, z tym, że wartości rosną w stronę obserwatora. To oznacza, że wszystkie obiekty widziane przez viewport mają ujemne wartości współrzędnej z, tym bardziej ujemne, im obiekt jest dalej.

Układ współrzędnych obserwatora OpenGL

Dla porządku trzeba zaznaczyć, że taki układ współrzędnych jest typowy (gdy korzystamy ze standardowej perspektywy), ale nie jedyny możliwy. Manipulując macierzą projekcji można prawie dowolnie przesuwać punkt zerowy, obracać układem, nachylać osie, zmieniać proporcje i tak dalej.

Ciasteczka i dane osobowe