Wstęp
W tym odcinku kursu poznamy niezbędne wiadomości wstępne dotyczące historii oraz
podstaw budowy i działania biblioteki OpenGL. Czytelnik znajdzie tutaj także uzasadnienie do
konwencji przyjętej w dalszej części kursu.
Historia OpenGL
Specyfikacja pierwszej wersji biblioteki OpenGL (ang. Open Graphics Library) została
opublikowana 1 czerwca 1992 roku na bazie języka GL opracowanego przez firmę Silicon Graphics
Inc. (SGI) na potrzeby stacji graficznych IRIS. Początkiem sukcesu OpenGL stała się jednak dopiero
wersja 1.1, opublikowana 4 marca 1997 roku, którą zaimplementowano m.in. w systemach z rodziny
Microsoft Windows. Kolejne wersje biblioteki (1.2, 1.3, 1.4, 1.5, 2.0, 2.1, 3.0, 3.1, 3.2, 3.3, 4.0, 4.1 i
najnowsza 4.2 z 8 sierpnia 2011 roku) są znakomitą ilustracją rozwoju sprzętu graficznego i tendencji
w programowym generowaniu grafiki.
Przez tych kilkadziesiąt lat jedną z głównych idei twórców OpenGL była zgodność wstecz
biblioteki. W praktyce programy przygotowane dla wcześniejszych wersji biblioteki działały
prawidłowo także na jej najnowszych implementacjach. Drugą zasadą OpenGL jest jej
wieloplatformowość – programy z niej korzystające działają praktycznie na wszystkich współczesnych
systemach operacyjnych. Cechy te są miarą sukcesu OpenGL, która jest obecnie podstawową
niskopoziomową biblioteką graficzną 3D, standardem powszechnie wykorzystywanym przez
producentów oprogramowania użytkowego i gier.
Od początku rozwojem OpenGL zajmowała się ARB (ang. Architecture Review Board), w skład
której wchodzili przedstawiciele najważniejszych firm z branży komputerowej. Taki sposób
wprowadzania zmian w bibliotece zapewnia OpenGL zachowanie niezależności od jednej platformy
sprzętowej lub programowej przy jednoczesnym uwzględnieniu najnowszych osiągnięć w dziedzinie
grafiki komputerowej. Oczywiście współpraca wielu firm, o często sprzecznych interesach, nie zawsze
przebiegała idealnie. Producenci sprzętu graficznego promowali własne rozwiązania,
niekompatybilne z rozwiązaniami konkurencji, a spory wewnątrz ARB powodowały opóźnienia w
przygotowywaniu nowych wersji specyfikacji.
Od 2006 roku rolę opiekuna OpenGL przejął Khronos Group, który rozwija także inne otwarte
standardy programistyczne. Początkowo krytykowany za opóźnienia, Khronos Group w drugiej
połowie 2008 roku wprowadził specyfikację wersji 3.0 i krótko po tym opublikowane zostały kolejne
wersje OpenGL oznaczone numerami 3.1 i 3.2. Ewolucję OpenGL zaproponowaną przez Khronos
Group opiszemy nieco dalej, z kilkuletniej perspektywy wydaje się jednak, że zmiana ta wywarła
pozytywny wpływ na tempo i kierunek rozwoju biblioteki. Tempo zmian widać także na przykładzie
szybkości dalszego rozwoju OpenGL. Wersje 3.3 i 4.0 opublikowano, po raz pierwszy w historii
OpenGL, w tym samym czasie – 11 marca 2010 roku. Krótko potem pojawiła się wersja 4.1 biblioteki.
Natomiast na kolejną i obecnie najnowszą wersję OpenGL 4.2 trzeba było czekać ponad rok.
Rozszerzenia
Jednym z najczęściej dyskutowanych mechanizmów OpenGL są rozszerzenia. Rozszerzenia
określają dodatkowe możliwości implementacji biblioteki i mogą dotyczyć najróżnorodniejszych jej
elementów. Zaletą tego mechanizmu jest dostępność z poziomu OpenGL najnowszych możliwości
sprzętu graficznego, ale jest jednocześnie to także wada. Rozszerzenia proponowane przez niektóre
firmy nie były implementowane przez inne (czasami wynikało to jednak z braku możliwości
technicznych), co czyniło rozszerzenia mechanizmem niepewnym w zastosowaniach komercyjnych.
Mimo tych wad rozszerzenia na trwałe wpisały się w bibliotekę OpenGL i stanowią ważny mechanizm
wspomagający jej rozwój.
OpenGL 3.0 - rewolucja przez ewolucję
Wspomniana wcześniej wersja 3.0 biblioteki OpenGL zapoczątkowała zupełnie nowe
podejście do dotychczas obowiązujących zasad. Twórcy standardu zauważyli bowiem, że dalsze
utrzymywanie tak rozbudowanej i w znacznej części przestarzałej funkcjonalności nie znajduje
praktycznego uzasadnienie. Wyodrębniono liczną grupę tzw. funkcji przestarzałych, która została
wytypowana do usunięcia w przyszłych wersjach specyfikacji. Wskazuje to na generalną tendencję
ewolucji OpenGL – stopniowe usuwanie przestarzałych jej elementów i zastępowanie ich
nowoczesnymi programowalnymi mechanizmami.
Kolejną nowością w bibliotece OpenGL 3.0 jest wymuszenie na twórcach systemów
okienkowych utworzenia funkcji obsługujących nowy kontekst renderingu. Związane jest to z
zupełnie nową w bibliotece OpenGL koncepcją profilowania kontekstów renderingu oraz
planowanym cyklem usuwania z biblioteki przestarzałych mechanizmów. W przyszłości planowane są
profile przeznaczone do rozrywki, tworzenia dokumentów i stron WWW, obsługi aplikacji
desktopowych, czy też dedykowane do urządzeń mobilnych. Warto zauważyć, że ARB zastrzegła
sobie wyłączność do tworzenia profili OpenGL. Sposób obsługi nowego kontekstu renderingu musi
umożliwiać aplikacji wskazanie konkretnego profilu niezbędnego do działania programu. Zauważmy,
że brak nowego kontekstu renderingu przy redukcji funkcjonalności OpenGL, mógłby doprowadzić w
przyszłości do błędów przy uruchamianiu starszych programów.
Początkowo wersja 3.0 OpenGL spotkała się z dość chłodnym przyjęciem ze strony
środowiska programistycznego. Wśród głównych zarzutów pojawiały się tezy o braku pełnej
rezygnacji z przestarzałych funkcji oraz braku modelu obiektowego. Wszystko to było związane z
oczekiwaniami na zupełnie nowe API biblioteki. Całkiem słusznie niezadowolenie wzbudzało także
wielomiesięczne opóźnienie w prezentacji specyfikacji biblioteki.
Analiza decyzji Khronos Group wskazuje jednak, że podjęto bardzo racjonalne decyzje. Na
obecnym etapie rozwoju biblioteki nie była możliwa rezygnacja z całej jej dotychczasowej
funkcjonalności. Zbyt dużo jest bowiem na rynku oprogramowania z tego korzystającego – w grę
wchodzą choćby niezwykle skomplikowane aplikacje inżynierskie i naukowe. Wprowadzenie nowego
kontekstu renderingu, mechanizmu profili oraz redukcji funkcjonalności ułatwi stopniowe
przechodzenie do przyszłych wersji biblioteki.
OpenGL 3.1 – dalszy etap rewolucji przez ewolucję
Kolejna odsłona biblioteki OpenGL pojawiła się bardzo szybko (oczywiście w porównaniu do
poprzednich standardów w tym zakresie) i stanowi kolejny istotny krok w jej rozwoju. Zdecydowano
się na usunięcie wszystkich funkcji określonych w wersji 3.0 jako przestarzałe, choć pozostawiono
jeszcze dodatkową furtkę w postaci rozszerzenia GL_ARB_compatibility, które zawiera wszystkie
usunięte funkcje. Dzięki temu API stał się znacznie mniejsze, co widać choćby po objętości
specyfikacji. Pełna jej wersja obejmująca opis funkcji przestarzałych to blisko 500 stron, a właściwa
zajmuje nieco ponad 350 stron.
Usunięcie przestarzałej funkcjonalności znaczne uprościło bibliotekę i jednocześnie ułatwiło
jej stosowanie. Jednak z ułatwień tych w pełni skorzystają jedynie twórcy nowych aplikacji, którzy od
początku zdecydują się korzystanie wyłącznie z nowoczesnych rozwiązań oferowanych przez OpenGL
3.0/3.1.
OpenGL 3.2 – szybka ewolucja
OpenGL 3.2 to kolejny etap rewolucji przez ewolucję. Specyfikacja została opublikowana
nieco ponad cztery miesiące po upublicznieniu wersji 3.1 i wnosi szereg nowych możliwości do
biblioteki, w tym: tekstury wielopróbkowania, shadery geometrii, obiekty synchronizacji, restart
prymitywu i wierzchołki główne prymitywów. Jedną z bardziej praktycznych zmian jest zdefiniowanie
dwóch profili: podstawowego, obejmującego tylko nieusuniętą funkcjonalność i kompatybilnego,
zgodnego ze wszystkim poprzednimi wersjami OpenGL. Nie zmieniono przy tym praktycznie
funkcjonalności określonych jako przestarzałe, co wskazuje na pewną stabilizację w dalszym kierunku
rozwoju biblioteki.
OpenGL w wersji 3.2 bez rozszerzeń można w pełni porównywać z ówczesnym jej
odpowiednikiem, tj. biblioteką DirectX 10.0, a wraz z zaaprobowanymi przez ARB rozszerzeniami:
GL_ARB_draw_buffers_blend,
GL_ARB_sample_shading,
GL_ARB_texture_cube_map_array,
GL_ARB_texture_gather i GL_ARB_texture_query_lod z biblioteką DirectX w wersji 10.1.
OpenGL 3.3 – ostatnie podejście do zgodności z DirectX 10.0
Specyfikacja biblioteki OpenGL 3.3 to ostatnie spojrzenie na możliwości oferowane prze karty
graficzne zgodne w biblioteką DirectX 10.0. Wprowadzone zamiany, jak bezpośrednia adresacja
atrybutów wierzchołków w języku GLSL, zamiana składowych tekstury przed próbkowaniem, liczniki
czasu, czy też nowe formaty atrybutów wierzchołków i formatów tekstur, służą głownie ułatwieniu
przenoszenia programów pomiędzy DirectX a OpenGL. Znamienna jest także data opublikowania
specyfikacji OpenGL 3.3, która zbiegła się z prezentacją OpenGL 4.0. W ten sposób dokonano
podziału wprowadzonych modyfikacji biblioteki na te obsługiwane przez karty graficzne zgodne z
DirectX 10.0 i te wymagające kart następnej generacji, obsługujących DirectX 11.0.
OpenGL 4.0 – obsługa kolejnej generacji sprzętu
Wersja 4.0 biblioteki OpenGL to kolejny duży krok w jej rozwoju. Autorzy nadrobili zaległości
w stosunku do bezpośredniego konkurenta i szybko przedstawili odpowiednik DirectX 11.0, z
najważniejsza nowością: sprzętową teselacją (kafelkowaniem) prymitywów. Wśród innych nowości
można wymienić dodanie pełnej obsługi 64-bitowych typów danych, nowe formaty tekstur
buforowych, nowe formaty kompresji tekstur oraz rozbudowę mechanizmów transformacji
sprzężonych zwrotnie. Jednocześnie odpowiednio zmodyfikowano GLSL dodając dwa nowe rodzaje
programów cieniowania kontrolujące teselację, które operują na ścieżkach (zbiorach) wierzchołków
wraz z ich atrybutami: shader kontroli teselacji (ang. tesselation control shader) i shader ewaluacji
teselacji (ang. tessellation evaluation shader). Równolegle z wprowadzeniem technik dostępnych w
DirectX 11.0 w OpenGL 4.0 dodano mechanizmy znane z DirectX 10.1, m.in. tablice tekstur
sześciennych oraz rozszerzone tryby mieszania kolorów.
OpenGL 4.1 – integracja z OpenGL ES 2.0
Wersja 4.1 biblioteki OpenGL została opublikowana w krótkim odstępie po wersji 4.0.
Najważniejsza zmianą jest wprowadzenie kompatybilności z biblioteką OpenGL ES 2.0, czyli wersją
OpenGL przeznaczoną na platformy mobilne. Ten nieco zaskakujący kierunek zmian nie jest jednak
przypadkowy – OpenGL ES 2.0 stało się podstawą technologii WebGL udostępniającej trójwymiarowe
API graficzne w przeglądarkach internetowych. Zatem integracja OpenGL ES 2.0 w OpenGL 4.1 ułatwi
zadanie twórcom implementacji WebGL w przeglądarkach internetowych. Ponadto wersja 4.1
OpenGL zawiera m.in. obsługę 64-bitowych danych atrybutów wierzchołków i możliwość
przechowywania shaderów w postaci binarnej.
OpenGL 4.2 – szereg nowych funkcjonalności
Najnowsza obecnie wersja biblioteki OpenGL 4.2 została opublikowana nieco ponad rok od
wersji ją poprzedzającej. Wprowadzono w niej kilka istotnych nowości: liczniki atomowe, atomowe
operacje zapisu/odczytu na wybranych elementach określonej warstwy tekstury, rozbudowa
transformacji sprzężonych zwrotnie o możliwość przechwytywania danych wierzchołków po wyjściu z
teselatora, możliwość modyfikacji wybranego podzbioru danych skompresowanej tekstury bez
konieczności jej ponownego ładowania, ładowanie 8- i 16-bitowych danych do 32-bitowych
zmiennych w celu przyspieszenia transferu danych między shaderami, możliwość pobrania
wewnętrznego formatu danych tekstury oraz umożliwienie współpracy shaderów modyfikujących
głębię z wcześniejszym testem głębokości. Całość wprowadzonych zmian została ujęta w
rozszerzenia, których część może być także obsługiwana przez karty graficzne starszej generacji.
GLSL
Język programów cieniowania OpenGL GLSL (ang. OpenGL Shading Language), zwanymi
najczęściej shaderami, został wprowadzony w wersji 2.0 biblioteki, a od wersji 3.1 stanowi
podstawowy element potoku renderingu zarówno na etapie przetwarzania wierzchołków jak i
fragmentów. Sam język GLSL sukcesywnie ewoluował wraz z kolejnymi wersjami OpenGL. Aktualnie
jego najnowszą wersją jest 4.20, która odpowiada możliwościom tzw. Shader Model 5.0 języka HLSL z
biblioteki DirectX 11.0. Profil podstawowy biblioteki OpenGL 4.2 obsługuje także wcześniejsze wersje
GLSL, począwszy od wersji 1.40 (OpenGL 3.1).
Język GLSL jest opisany w odrębnej specyfikacji, która tworzy spójną całość ze specyfikacją
podstawowego API biblioteki OpenGL.
Powiązania z systemami okienkowymi
Biblioteka OpenGL wymaga wsparcia API systemu okienkowego do tworzenia i zarządzania
kontekstami graficznymi, renderingu w oknie oraz obsługi innych zasobów. Wszystkie liczące się
współczesne systemy operacyjne posiadają API obsługujące bibliotekę OpenGL. W systemie X
Window, dostępnym głównie pod Unix i Linux, jest to GLX. Warto także wspomnieć, że
implementacje GLX istnieją również w systemach Microsoft Windows, MacOS X i kilku innych
platformach, na których jest dostępny X Serwer. OpenGL w systemach Microsoft Windows
obsługiwany jest przez WGL. Quartz, warstwa graficzna systemu MacOS X, zawiera kilka interfejsów
API obsługujących OpenGL. Są to CGL, AGL i NSGLView. Natomiast na platformy mobilne
przeznaczona jest biblioteka EGL, która współpracuje z OpenGL ES 2.0, ale może także obsłużyć
standardowy OpenGL.
Oczywiście stosowanie rozwiązań specyficznych dla danego systemu operacyjnego powoduje,
że danego programu nie można skompilować i uruchomić w innym systemie operacyjnym bez
dokonania szeregu zmian w kodzie źródłowym. Rozwiązanie tego problemu stanowią biblioteki
oferujące jeden, niezależny od systemu operacyjnego, interfejs do obsługi okien i komunikatów.
Pierwszą biblioteką tego typu była biblioteka AUX (ang. Auxiliary Library), jednak bodaj największą
popularność zdobyła biblioteka GLUT (ang. OpenGL Utility Toolkit), opracowana i rozwijana w latach
1994-1998 przez Marka J. Kilgarda. Pomimo sędziwego wieku jest ona ciągle powszechnie stosowana.
Typy danych
Typy danych w obsługiwane przez bibliotekę OpenGL przedstawia Tabela 1. Specyfikacja nie
określa jakiego rodzaju typy danych są użyte w konkretnej implementacji, zawiera jedynie minimalne
wymagania związane z precyzją i wykonywaniem operacji na całkowitych i zmiennoprzecinkowych
typach danych. W szczególności implementacja może używać typów danych o większej ilości bitów
niż minimalne wskazane we wspomnianej wyżej tabeli. Typy GLintptr, GLsizeiptr i GLsync
mają ilość bitów opisaną jako prtbits, co oznacza minimalną ilość bitów niezbędną w danej
implementacji do umieszczenia wskaźnika. Typy te muszą umożliwić zapamiętanie dowolnego
adresu. Trzeba także wyraźnie podkreślić, że typy danych OpenGL nie są typami danych
występującymi w języku C (pomimo częściowej zgodności nazw). Zatem przykładowo, typ GLint
niekoniecznie jest równoznaczny z typem int w C. Ponadto, poza wymienionymi typami, funkcje
OpenGL korzystają z typu pustego GLvoid.
Typ GL
Minimalna ilość
bitów
Opis
GLboolean
1 lub więcej
typ logiczny
GLbyte
8
liczba całkowita ze znakiem w kodzie uzupełnień do 2
GLubyte
8
liczba całkowita bez znaku
GLchar
8
znaki tworzące ciągi
GLshort
16
liczba całkowita ze znakiem w kodzie uzupełnień do 2
GLushort
16
liczba całkowita bez znaku
GLint
32
liczba całkowita ze znakiem w kodzie uzupełnień do 2
GLuint
32
liczba całkowita bez znaku
GLfixed
32
dwie 16-bitowe liczby całkowite ze znakiem w kodzie uzupełnień
do 2 (odpowiednik liczb zmiennoprzecinkowych)
GLint64
64
liczba całkowita ze znakiem w kodzie uzupełnień do 2
GLuint64
64
liczba całkowita bez znaku
GLsizei
32
nieujemna liczba całkowita
GLenum
32
typ wyliczeniowy
GLintptr
ptrbits
liczba całkowita ze znakiem w kodzie uzupełnień do 2
GLsizeiptr
ptrbits
nieujemna liczba całkowita
GLsync
ptrbits
uchwyt obiektu synchronizacji
GLbitfield
32
pole bitowe
GLhalf
16
liczba zmiennoprzecinkowa połówkowej precyzji zakodowana
jako skalar bez znaku
GLfloat
32
liczba zmiennoprzecinkowa
GLclampf
32
liczba zmiennoprzecinkowa ograniczona do przedziału
GLdouble
64
liczba zmiennoprzecinkowa podwójnej precyzji
GLclampd
64
liczba zmiennoprzecinkowa podwójnej precyzji ograniczona do
przedziału
Tabela 1 Typy danych w bibliotece OpenGL.
Arytmetyka zmiennoprzecinkowa
W przypadku liczb zmiennoprzecinkowych (GLfloat i GLdouble) specyfikacja OpenGL nie
określa sposobu ich reprezentacji, zawiera jedynie minimalne wymagania dla implementacji
biblioteki. W szczególności implementacja może stosować arytmetykę zmiennoprzecinkową zgodną
ze standardem IEEE 754. Wymagania nie obejmują obsługi dodatniej i ujemnej nieskończoności
( oraz , oznaczenia matematyczne odpowiednio i ) oraz symboli nieoznaczonych
(ang. not a number), np.
. Nieco inaczej sprawa wygląda w przypadku języka GLSL, gdzie
stosowne są liczby zmiennoprzecinkowe zgodne z wymienionym wyżej standardem IEEE 754.
Nieco inaczej sytuacja wygląda w przypadku liczb zmiennoprzecinkowych połówkowej
precyzji (GLhalf), gdzie specyfikacja OpenGL określa format liczb. 16-bitowe liczby
zmiennoprzecinkowe ze znakiem mają 1 bit znaku ( ), 5 bitów wykładnika ( ) oraz 10 bitów mantysy
( ). Wartość takiej liczby jest określana następująco:
Jeżeli liczba typu GLhalf jest interpretowana jako 16-bitowa liczba całkowita bez znaku ,
to wartość znaku, wykładnika i mantysy wynosi odpowiednio:
Warto także zwrócić uwagę, że niektóre formaty pikseli korzystają z liczb
zmiennoprzecinkowych bez znaku korzystające z 11 lub 10 bitów. Formaty te opiszemy bliżej w
odcinku kursu OpenGL poświęconemu teksturom.
Składnia poleceń
Polecenia OpenGL są funkcjami lub procedurami. Różne grupy poleceń wykonują tę samą
operację, lecz różnią się sposobem w jaki dostarczane są do nich argumenty. Ogólnie rzecz biorąc,
deklaracja polecenia posiada formę:
rtype Name{1|2|3|4}{b|s|i|i64|f|d|ub|us|ui|ui64}{v}( [args ,] T arg1, … ,
T argN [, args] );
Polecenia OpenGL są zbudowane z nazwy, po której następuje, w zależności od konkretnego
przypadku, maksymalnie do 4 znaków. rtype jest typem zwracanym przez funkcję. Nawiasy {}
dołączają szereg znaków (lub zestawów znaków), z których tylko jeden jest używany. Pierwszy znak
{1|2|3|4} oznacza liczbę wartości wskazanego typu, które muszą być przedstawione do
polecenia. Drugi znak lub zestaw znaków {b|s|i|i64|f|d|ub|us|ui|ui64} wskazuje
konkretne rodzaje argumentów: 8-bitowe liczby całkowite, 16-bitowe liczby całkowite, 32-bitowe
liczby całkowite, 64-bitowe liczby całkowite, liczby zmiennoprzecinkowe pojedynczej precyzji lub
liczby zmiennoprzecinkowe podwójnej precyzji – patrz Tabela 2. Zauważmy, że wskazując na typ bez
znaku dodajemy literę u na początku tego nazwy typu (na przykład unsigned byte jest skracany
do ubyte). Ostatni znak {v}, jeśli jest obecny, wskazuje, że polecenie zawiera wskaźnik do tablicy
(wektora) wartości, a nie serię poszczególnych argumentów.
Litera
Odpowiadający typ OpenGL
b
GLbyte
s
GLshort
i
GLint
i64
GLint64
f
GLfloat
d
GLdouble
ub GLubyte
us GLushort
ui GLuint
ui64 GLuint64
Tabela 2 Powiązanie sufiksów poleceń OpenGL z typami argumentów. Patrz Tabela 1 z definicjami typów OpenGL.
Argumenty poleceń umieszczone w nawiasach [args ,] i [, args] mogą, ale nie muszą
być obecne. N argumentów od arg1 do argN ma typ T, który odpowiada jednemu ze znaków lub
parze znaków wskazanych w Tabela 2. Jeśli ostatnim znakiem nie jest v, to N jest podane przez znaki
{1|2|3|4}
(jeśli ich nie ma, to liczba argumentów jest ustalona). Jeśli ostatnim znakiem jest v, to
jest obecny tylko argument arg1 i jest nim tablica N wartości wskazanego typu. Argumenty, których
typ jest stały (tzn. nie wskazany przez sufiks polecenia) są jednym z typów danych OpenGL zawartych
w Tabela 1 lub wskaźnikami do jednego z tych typów.
Przykładowo grupa funkcji glUniform* w specyfikacji OpenGL jest opisywana następująco:
void Uniform{1234}{if}( int location, T value );
co wskazuje osiem deklaracji funkcji, przedstawionych poniżej w notacji ANSI C:
void Uniform1i( int location, int value );
void Uniform1f( int location, float value );
void Uniform2i( int location, int v0, int v1 );
void Uniform2f( int location, float v0, float v1 );
void Uniform3i( int location, int v0, int v1, int v2 );
void Uniform3f( int location, float v0, float v2, float v2 );
void Uniform4i( int location, int v0, int v1, int v2, int v3 );
void Uniform4f( int location, float v0, float v1, float v2,
float v3 );
W dalszej części kursu będziemy używać pełnych deklaracji funkcji w języku ANSI C, jednak
prezentacja zasad budowy poleceń OpenGL ułatwi Czytelnikowi samodzielne zgłębianie specyfikacji
biblioteki.
Obsługa błędów
Biblioteka OpenGL zawiera mechanizmy obsługi błędów. Jednak z założenia wykrywanych
jest tylko część sytuacji spośród zbioru warunków, które mogłyby być uważane za błędy. Stanowi to
kompromis na rzecz efektywności, ponieważ w wielu przypadkach kontrola błędów miałby
niekorzystny wpływ na wydajność programu wolnego od błędów.
Informację o kodzie bieżącego błędu zwraca funkcja:
GLenum glGetError( void );
Zestawienie zwracanych kodów błędów zawiera Tabela 3. Działanie OpenGL jest
niezdefiniowane tylko w przypadku wystąpienia błędu o kodzie GL_OUT_OF_MEMORY. W
pozostałych przypadkach błąd nie powoduje przerwania wykonywania programu, nie jest
wykonywana jedynie funkcja odpowiedzialna za jego powstanie. Polecenie takie na ma wpływu na
stan OpenGL lub zawartość bufora ramki. Jeśli polecenie generujące błąd zwraca wartości, zwracana
jest wartość zero. Jeśli polecenie generujące błąd zmienia wartości poprzez wskaźnik do argumentu,
nie są dokonywane zmiany tych wartości.
Kod błędu
Opis
Czy błędne polecenie
jest ignorowane?
GL_INVALID_ENUM
Argument GLenum
poza zakresem
Tak
GL_INVALID_VALUE
Argument liczbowy poza
zakresem
Tak
GL_INVALID_OPERATION
Nielegalna operacja w
bieżącym stanie
Tak
GL_INVALID_FRAMEBUFFER_OPERATION Niekompletny obiekt
bufora ramki
Tak
GL_OUT_OF_MEMORY
Brak pamięci do
wykonania polecenia
Nieokreślone
GL_NO_ERROR
Brak błędu
-
Tabela 3 Zestawienie kodów błędów OpenGL.
Kiedy błąd zostanie wykryty, ustawiana jest flaga błędu i rejestrowany jest jego kod. Jeżeli
występują kolejne błędy nie wpływają one na ten zarejestrowany kod. Dopiero, gdy wywołana jest
funkcja glGetError, zwracany jest kod błędu i czyszczona jest flaga tak, aby kolejny błąd mógł być
zarejestrowany. Jeżeli glGetError zwraca GL_NO_ERROR oznacza to, że od ostatniego
wywołania glGetError (lub od kiedy GL został zainicjowany) nie był wykryty żaden błąd.
Implementacja może przechowywać więcej niż jedną informację o błędzie. W takim przypadku
wywołanie glGetError zwraca kody kolejnych zarejestrowanych błędów, tak długo aż nie zostanie
zwrócona wartość GL_NO_ERROR.
Typowy kod odczytujący błędy zgłaszane przez bibliotekę OpenGL ma postać poniższej pętli:
GLenum error;
while( ( error = glGetError() ) != GL_NO_ERROR )
{
// obsługa błędu
}
Ogólne warunki generowania błędów przedstawione są poniżej:
Jeśli polecenie wymagające wartości wyliczeniowej określonej, jako stała symboliczna
otrzymuje wartość, która nie należy do dopuszczalnych dla tego polecenia, generowany jest
błąd GL_INVALID_ENUM.
Jeśli przekazywana jest ujemna liczba, a typ argumentu jest określony, jako GLsizei lub
GLsizeiptr, zostanie wygenerowany błąd GL_INVALID_VALUE.
Jeśli ubocznym efektem wykonania polecenia jest wyczerpanie pamięci, może być
wygenerowany błąd GL_OUT_OF_MEMORY.
W innych wypadkach, błędy generowane są tylko na warunkach, które są wyraźnie opisane w
specyfikacji biblioteki OpenGL.
Operacje OpenGL i potok renderingu
OpenGL koncentruje się jedynie na renderingu do bufora ramki oraz czytaniu wartości
zapisanych w tym buforze. Biblioteka nie ma wsparcia dla innych urządzeń, takich jak myszy i
klawiatury, stąd trzeba używać innych mechanizmów w celu przetworzenia informacji
wprowadzanych przez użytkownika.
Rysunek 1 przedstawia schemat działania OpenGL. Z lewej strony diagramu wprowadzane są
polecenia OpenGL. Niektóre z nich określają obiekty geometryczne, które mają zostać narysowane,
inne kontrolują obsługę obiektów przez różne etapy renderingu. Polecenia zawsze są przetwarzane w
kolejności, w jakiej zostały przekazane do OpenGL, a ewentualne opóźnienie realizacji skutków
danego polecenia nie mogą mieć wpływu na wyniki późniejszych poleceń. To samo dotyczy operacji
pobierania zmiennych stanu zapytania i odczytu pikseli, które zwracają stan odpowiadający
kompletnemu wykonaniu wszystkich wcześniej wywoływanych poleceń OpenGL (poza przypadkami
wyraźnie określonymi w specyfikacji). OpenGL wiąże dane w chwili wywoływania polecenia, a
wszelkie późniejsze zmiany danych nie mają wpływu na jego wynik.
Rysunek 1 Diagram blokowy potoku renderingu OpenGL (źródło: specyfikacja OpenGL).
Pierwszy etap potoku renderingu operuje na prymitywach geometrycznych: punktach,
segmentach linii i wielokątach, które są opisane przez wierzchołki. W tym etapie wierzchołki są
przekształcane i oświetlane, a prymitywy są obcinane do bryły widoku w celu przygotowania do
następnego etapu, rasteryzacji. Możliwe jest także programowe lub sprzętowe generowanie nowych
prymitywów, a także usuwanie istniejących. Rasteryzator generuje szereg fragmentów - elementów
bufora ramki używanych do dwuwymiarowego opisu punktów, segmentów linii lub wielokątów.
Każdy tak wyprodukowany fragment jest przekazywany do następnego etapu, gdzie wykonywane są
operacje na poszczególnych fragmentach przed finalnym zapisem w buforze ramki. Operacje na
buforze ramki obejmują m.in. obsługę wartości bufora głębokości, mieszanie kolorów oraz
maskowanie i inne operacje logiczne na wartościach fragmentów. Dane bufora ramki mogą być
również odczytywane lub kopiowane pomiędzy różnymi częściami bufora ramki.
OpenGL wykorzystuje dwa rodzaje buforów ramek: domyślny bufor ramki obsługiwany i
kontrolowany wyłącznie przez system okienkowy (np. za pomocą jednej z opisanych wcześniej
bibliotek) oraz obiekty bufora ramki tworzone i kontrolowane przez aplikacje.
Biblioteka OpenGL wykorzystuje model działania klient-serwer. W typowym przypadku
klientem OpenGL jest aplikacja wykorzystująca polecenia OpenGL, a serwerem OpenGL aktualnie
używana implementacja biblioteki (np. w sterowniku karty graficznej), przetwarzająca polecenia
przesłane przez klienta. Serwer OpenGL może, ale nie musi działać na tym samym komputerze co
klient OpenGL.
Biblioteka OpenGL została zaprojektowana jako biblioteka wieloplatformowa, z założenia
przeznaczona do obsługi wielu rodzajów sprzętu graficznego. Stąd specyfikacja OpenGL zawiera opis
zalecanych rozwiązań dla określonych poleceń, z wyraźnie wskazanymi możliwymi od nich
odstępstwami. Wspomniane dopuszczalne wariacje implementacji OpenGL powodują, że
poszczególne implementacje nie muszą być zgodne "piksel do piksela", tzn. ten sam zestaw danych
źródłowych i poleceń nie musi w różnych implementacjach dać identycznego końcowego rezultatu w
buforze ramki.
Zmienne stanu i maszyna stanu
Zmienne stanu (ang. state) są ważnym wewnętrznym elementem OpenGL opisującym
ustawienia biblioteki. Wiele zmiennych stanu jest dwustanowych, inne mają wartości całkowite lub
zmiennoprzecinkowe. Wszystkie zmienne stanu tworzą tzw. maszynę stanów (ang. state machine),
która steruje zachowaniem OpenGL.
Wyróżniamy dwa rodzaje zmiennych stanu. Pierwszy typ, zwany zmiennymi serwera OpenGL,
znajduje się w serwerze OpenGL. Większość zmiennych stanu OpenGL wchodzi w skład tej kategorii.
Drugi typ, zwany zmiennymi klienta OpenGL, znajduje się w kliencie OpenGL. Każdy egzemplarz
kontekstu OpenGL zawiera jeden pełny zestaw zmiennych stanu serwera OpenGL. Natomiast każde
połączenie od klienta do serwera zawiera odrębny zbiór zmiennych stanu zarówno klienta jak i
serwera OpenGL.
Specyfikacja przewiduje możliwość współdzielenia niektórych zmiennych stanu w ramach
różnych kontekstów renderingu. Tą tematyką zajmiemy się na końcu kursu przy opisie obsługi wielu
kontekstów renderingu w ramach jednej aplikacji OpenGL.
Zmienne stanu modyfikowane są, w sposób jawny lub nie, przez polecenia biblioteki OpenGL,
które będziemy poznawać w dalszych odcinkach kursu. Natomiast do odczytu zmiennych stanu służy
bardzo liczna grupa funkcji glGet*, z których najbardziej podstawowe są następujące funkcje:
void glGetBooleanv( GLenum pname, GLboolean *params );
void glGetIntegerv( GLenum pname, GLint *params );
void glGetInteger64v( GLenum pname, GLint64 *params );
void glGetFloatv( GLenum pname, GLfloat *params );
void glGetDoublev( GLenum pname, GLdouble *params );
Parametr pname określa, którą wartość maszyny stanów OpenGL chcemy pobrać (wszystkie
zmienne stanu przedstawiamy w odrębnym odcinku kursu), a params wskaźnik na zwracaną
wartość. W zależności od rodzaju pobieranej zmiennej tablica params może zawierać pojedynczą
zmienną lub tablicę zmiennych. Program musi zapewnić odpowiednią ilość miejsca w pamięci.
Zauważmy, że rodzaj zwracanej lub zwracanych wartości jednoznacznie określa końcowa część nazwy
funkcji.
Podobna grupa funkcji obsługuje tzw. indeksowane zmienne stanu, których wartości mogą
różnić się dla poszczególnych indeksów zawartych w parametrze index:
void glGetBooleani_v( GLenum target, GLuint index,
GLboolean *data );
void glGetIntegeri_v( GLenum target, GLuint index, GLint *data );
void glGetInteger64i_v( GLenum target, GLuint index,
GLint64 *data );
void glGetFloati_v( GLenum target, GLuint index, GLfloat *data );
void glGetDoublei_v( GLenum target, GLuint index, GLdouble *data );
Dostępne są jeszcze dwie funkcje pobierające informacje o włączeniu lub wyłączeniu danej
właściwości maszyny stanu OpenGL (dwuwartościowe zmienne stanu). Pierwsza z nich obsługuje
pojedyncze właściwość wskazaną w parametrze cap, druga właściwość indeksowaną, której indeks
zawiera parametr index (właściwość może mieć różną wartość dla różnych indeksów).
GLboolean glIsEnabled( GLenum cap );
GLboolean glIsEnabledi( GLenum target, GLuint index );
Włączanie i wyłącznie zmiennych stanu określających dwustanowe właściwości maszyny
stanu umożliwiają poniższe funkcje:
void glDisable( GLenum cap );
void glEnable( GLenum cap );
void glEnablei( GLenum target, GLuint index );
void glDisablei( GLenum target, GLuint index );
które w wersji indeksowej działają na zmiennej o indeksie wskazanym w parametrze index.
Podczas odczytu i zapisu zmiennych stanu OpenGL może zachodzić konieczność dokonania
konwersji (np. zmienna stanu jest typu całkowitego a odczytujemy dane typu
zmiennoprzecinkowego). W takich przypadkach OpenGL dokonuje konwersji stosując następujące
zasady:
dla zmiennej stanu typu GLboolean całkowita lub zmiennoprzecinkowa liczba o wartości 0
jest konwertowana na wartość GL_FALSE, a każda niezerowa liczba na wartość GL_TRUE,
dla zmiennej stanu typu całkowitego lub typu wyliczeniowego GLenum, wartości logiczne
GL_FALSE i GL_TRUE konwertowane są odpowiednio do wartości 0 i 1; liczby
zmiennoprzecinkowe są zaokrąglane do najbliższej liczby całkowitej,
dla zmiennej stanu typu zmiennoprzecinkowego, wartości logiczne GL_FALSE i GL_TRUE
konwertowane są odpowiednio do wartości 0,0 i 1,0; liczby całkowite są bezpośrednio
konwertowane do liczby zmiennoprzecinkowej.
Wykorzystane w powyższym opisie stałe GL_TRUE i GL_FALSE to odpowiedniki słów
zarezerwowanych true i false z języka C++. Stałe te mają odpowiednio wartość 1 i 0.
Przyjęte w kursie konwencje i zasady
Programy w kursie zasadniczo wymagają dostępności biblioteki OpenGL w wersji 4.2, ale w
wielu przypadkach wystarczy dostępność wersji 4.1 lub 4.0. Przy ich pisaniu wykorzystano nowe
funkcje zarządzające kontekstem OpenGL, co powoduje, że programy nie mogą być bezpośrednio
uruchamiane na starszych niż 3.0 wersjach biblioteki. Jednakże część z nich, po odpowiednich
modyfikacjach, może działać z OpenGL 2.0/2.1. Programy przykładowe przy tworzeniu okien
korzystają bezpośrednio z Motif (systemu UNIX i Linux) oraz API WIN32 (systemy Microsoft
Windows). Fragmenty programu specyficzne dla danego systemu operacyjnego są oddzielone od
elementów korzystających z biblioteki OpenGL, co ułatwi Czytelnikowi ewentualną migrację
programu na wybraną platformę systemową lub programową.
Wszystkie programy napisane są przy użyciu języka C++ i kompilowane za pomocą
kompilatorów: Microsoft Visual C++ 2008 EE i GCC 4.4 w systemie Linux Mandriva 2010. Programy
testowano na karcie graficznej NVidia GeForce GTX 460. W przypadku programów napisanych w
OpenGL ES 2.0 używana jest biblioteka EGL i emulator pochodzący z pakietu PowerVR Insider SDK.
Do kompilacji programów nie są wymagane żadne dodatkowe zewnętrzne biblioteki. Jednak
najczęściej używane funkcje zostały pogrupowane w odrębne pliki, które mogą stanowić przydatną
podręczną bibliotekę.