5/2014 (24)
www•programistamag•pl
Cena 22.90 zł (w tym VAT 8%)
Index: 285358
Robot Framework • Obsługa wyświetlaczy na Raspberry Pi • Wzorce silników zdarzeń w C++
Python i profilowanie Agile a komunikacjaZarządca zawartości
Jak profilować
i optymalizować
aplikacje w Pythonie
Psychologiczny
aspekt komunikacji
z zespołem
Fundamentalny
wzorzec projektowy
w silnikach gier
Co programista grafiki 3D
powinien wiedzieć o macierzach
DOMENY | E-MAIL | HOSTING | STRONY WWW | SERWERY
*Ceny nie zawierają VAT (23%). Umowa zawierana na 12 miesięcy płatne z góry. Podana miesięczna cena promocyjna obowiązuje przez cały okres umowy.
Niniejszy materiał promocyjny nie stanowi oferty w rozumieniu kodeksu cywilnego. Ogólne warunki handlowe i regulamin promocji na www.1and1.pl. 1and1.pl
22 116 27 77
MIESIĄC
30 DNI
NA PRÓBĘ
TELEFON
PORADA
SPECJALISTY
PEWNOŚĆ
DZIĘKI GEO-
REDUNDANCJI
Wszystko w komplecie
■ darmowa domena .pl
■ nielimitowana powierzchnia, transfer,
konta e-mail i bazy danych MySQL
■ system Linux lub Windows
Centrum aplikacji
■ ponad 140 popularnych aplikacji (Drupal™,
WordPress , Joomla!™, TYPO3, Magento®...)
Nowość! Teraz także w wersji próbnej
■ wsparcie eksperta od aplikacji
Potężne narzędzia
■ NetObjects Fusion® 2013 1&1 Edition
■ 1&1 Kreator Stron Mobilnych
■ Linux: PHP 5.5, Perl, Python, Ruby
■ Windows: ASP.NET 4.5, ASP MVC 4,
dedykowane pule aplikacji
Skuteczny marketing
■ 1&1 SEO Ekspert
■ 1&1 SiteAnalytics
Nowoczesna technologia
■ Maksymalna dostępność dzięki georedundancji
■ Ponad 300 Gbit/s przepustowości
■ Gwarantowana wydajność nawet 2 GB RAM
■ 1&1 CDN powered by CloudFlare®
■ Skaner bezpieczeństwa 1&1 SiteLock
WIODĄCE APLIKACJE JESZCZE LEPSZE!
MOCNE PAKIETY
DLA ZAWODOWCÓW
zł/mies.*
4,90już od
NOWY
HOSTING
4,już od
spis treści / edytorial
PROGRAMOWANIE GRAFIKI
Macierze w grafice 3D..................................................................................................................
Jacek Matulewski
4
PROGRAMOWANIE GIER
Wzorce Programowania Gier: Zarządca Zawartości................................................................
Rafał Kocisz
22
PROGRAMOWANIE SYSTEMÓW OSADZONYCH
Zwizualizuj to sam. Obsługa wyświetlaczy na Raspberry Pi...................................................
Karol Poczęsny
32
PROGRAMOWANIE SYSTEMOWE
Jak napisać własny debugger w systemie Windows – część 4..............................................
Mateusz "j00ru" Jurczyk
38
TESTOWANIE I ZARZĄDZANIE JAKOŚCIĄ
Wprowadzenie do testowania w Robot Framework (Robot)................................................
WojciechTański
46
Profilowanie aplikacji w języku Python......................................................................................
AdamWołk
50
LABORATORIUM BOTTEGA
Wzorce silników zdarzeń w C++. Część I: Wzorzec Reaktor i podstawowa implementacja
Roman Ulan
60
Brakujący element Agile. Część 4: Emocje w komunikacji......................................................
Paweł Badeński
64
PLANETA IT
Zakodowana pasja.........................................................................................................................
Łukasz Sobótka
70
STREFA CTF
Zdobyć flagę… ASIS CTF Quals 2014 – Random Image..........................................................
Gynvael Coldwind
72
KLUB LIDERA IT
Jak całkowicie odmienić sposób programowania, używając refaktoryzacji (część 9).......
Mariusz Sieraczkiewicz
76
KLUB DOBREJ KSIĄŻKI
TDD. Sztuka tworzenia dobrego kodu.......................................................................................
Rafał Kocisz
78
Odrobina matematyki
Zacznijmy od tezy: Programowanie to matematyka.
To zdanie często wywołuje sprzeciw ze strony programistów nastawionych na aplikacje
biznesowe – i słusznie. Wielu utalentowanych matematyków na przestrzeni wielu, wielu
lat zrobiło wystarczająco dużo, aby statystyczny programista aplikacji biznesowych mógł
zająć się tym, co jest dla niego istotne. Nie wolno jednak zapomnieć o tym, że termin
„programista” ma również dziesiątki innych znaczeń. Bardzo gorący (bo majowy) numer
Programisty, za sprawą Jacka Matulewskiego (specjalne podziękowania dla autora za wło-
żoną pracę nad tym wydaniem!) nasycony został bardzo dużą ilością macierzy, rzutów i
funkcji trygonometrycznych. Nie jest to jednak zwykły, czysto matematyczny wywód
oderwany od rzeczywistości. „Macierze w grafice 3D” to bogaty wykład przemyślany pod
kątem jak największej przydatności dla wszystkich, którzy interesują się aspektami zwią-
zanymi z grafiką 3D. Czy programowanie to matematyka? Zapraszamy do ukształtowania
własnego poglądu, zaraz po zapoznaniu się z najnowszym wydaniem magazynu, który
możemy dostarczyć doTwoich rąk (lub naTwój monitor).
Magazyn Programista istnieje również dzięki Tobie, drogi Czytelniku. Cały czas jeste-
śmy ciekawiWaszych opinii: http://fb.me/ProgramistaMagazyn
Z poważaniem, Redakcja
Wydawca/ Redaktor naczelny:
Anna Adamczyk
annaadamczyk@programistamag.pl
Redaktor prowadzący:
Łukasz Łopuszański
lukaszlopuszanski@programistamag.pl
Korekta:
Tomasz Łopuszański
Kierownik produkcji:
Krzysztof Kopciowski
bok@keylight.com.pl
DTP:
Krzysztof Kopciowski
Dział reklamy:
reklama@programistamag.pl
tel. +48 663 220 102
tel. +48 604 312 716
Prenumerata:
prenumerata@programistamag.pl
Współpraca:
Michał Bartyzel
Mariusz Sieraczkiewicz
Michał Leszczyński
Marek Sawerwain
Łukasz Mazur
Rafał Kułaga
Sławomir Sobótka
Michał Mac
Gynvael Coldwind
Bartosz Chrabski
Adres wydawcy:
Dereniowa 4/47
02-776 Warszawa
Druk:
Drukarnia Kontakt
ul. Gospodarcza 5a
05-092 Łomianki
Nakład: 5000 egz.
Redakcja zastrzega sobie prawo do skrótów i opracowań
tekstów oraz do zmiany planów wydawniczych, tj. zmian
w zapowiadanych tematach artykułów i terminach
publikacji, a także nakładzie i objętości czasopisma.
O ile nie zaznaczono inaczej, wszelkie prawa do
materiałów i znaków towarowych/firmowych
zamieszczanych na łamach magazynu Programista są
zastrzeżone. Kopiowanie i rozpowszechnianie ich bez
zezwolenia jest Zabronione.
Redakcja magazynu Programista nie ponosi
odpowiedzialności za szkody bezpośrednie i pośrednie,
jak również za inne straty i wydatki poniesione
w związku z wykorzystaniem informacji prezentowanych
na łamach magazynu Programista.
Magazyn Programista wydawany jest
przez Dom Wydawniczy Anna Adamczyk
Zamów prenumeratę magazynu Programista
przez formularz na stronie:
http://programistamag.pl/typy-prenumeraty/
lub zrealizuj ją na podstawie faktury Pro-
forma. W spawie faktur Pro-Forma prosimy
kontktować się z nami drogą mailową:
redakcja@programistamag.pl.
Prenumerata realizowana jest także
przez RUCH S.A. Zamówienia można
składać bezpośrednio na stronie:
www.prenumerata.ruch.com.pl
Pytania prosimy kierować na adres e-mail:
prenumerata@ruch.com.pl lub kontaktując
się telefonicznie z numerem: 801 800 803
lub 22 717 59 59, godz.: 7:00 – 18:00 (koszt
połączenia wg taryfy operatora).
4 / 5 . 2014 . (24) /
programowanie grafiki
Jacek Matulewski
Poniższy artykuł jest rozdziałem książki pt. Podstawy grafiki 3D. Nowocze-
sny OpenGL przygotowywanej dla Wydawnictwa Naukowego PWN. Książ-
ka ukaże się jesienią tego roku.
Werteksy przesyłane do karty graficznej wyrażone powinny być w czterowy-
miarowym układzie współrzędnych modelu. Macierze świata i widoku, ewen-
tualnie jedna macierz model-widok, transformują współrzędne werteksu
z lokalnego układu obiektu do układu współrzędnych kamery – to jest ten
układ, który zazwyczaj przedstawiany jest na rysunkach wprowadzających do
OpenGL; z osią OZ skierowaną do widza, osią OX skierowaną w prawo i osią
OY – do góry. Układ modelu może mieć względem niego dowolne położenie
i orientację - macierze świata i widoku mogą być bowiem iloczynem macierzy
opisujących obroty, translacje, skalowania, odbicia czy pochylenia. Macierze
opisujące te wszystkie przekształcenia omówione zostaną niżej w tym arty-
kule. Za dalsze przekształcenia, tj. transformację z układu odniesienia kamery
do układu przycinania, odpowiada macierz rzutowania, od wyprowadzenia
której zaczniemy. Wyprowadzenie nie jest matematycznie bardzo trudne, ale
dość zawiłe. Warto zatem czytać artykuł z ołówkiem w ręku.
Macierze rzutowania
Macierze w grafice 3D
Nowoczesny OpenGL uruchamiany w profilu rdzennym zmusza programistę do
rzeczy, o których mógł wcześniej nie myśleć: pisania shaderów, tworzenia buforów
werteksów i definiowania macierzy. Oczywiście, że nadal można korzystać z bibliotek
czy gotowych rozwiązań pobranych z Internetu. Można to jednak wykorzystać jako
impuls do nauki GLSL lub rachunku macierzy. Poniższy artykuł ma być pomocą w re-
alizacji drugiego z tych impulsów. Nie rozpocznę jednak od wyjaśniania, czym są ma-
cierze – skupię się na zagadnieniach, których często brak w typowych podręcznikach
algebry, a charakterystycznych dla grafiki 3D. Artykuł wymaga od czytelnika pewnej
wiedzy o grafice 3D, choćby obycia ze współrzędnymi jednorodnymi.
Rysunek 1. Bryły widzenia w układzie kamery w przypadku perspektywy (frustum)
i rzutu izometrycznego oraz po transformacji do układu NDC (sześcian w prawym
górnym rogu)
Macierzrzutowaniaodpowiedzialnajestzatransformacjęzukładuwspółrzęd-
nych kamery (lub jak kto woli – oka) do układu przycinania (ang. clip coordi-
nate system). To nie jest układ docelowy. Uzupełnieniem tego przekształcenia
jest bowiem tzw. dzielenie perspektywiczne, tj. podzielenie współrzędnych x,
y i z przez współrzędną skalowania w. W efekcie następuje transformacja do
trójwymiarowego układu unormowanych współrzędnych urządzenia (NDC),
w którym bryła widzenia przyjmuje kształt sześcianu (Rysunek 1). Kształt
sceny wyznacza charakter rzutowania. Dokładniej rzecz ujmując, skoro sce-
na w układzie NDC ma kształt sześcianu, to odwrócenie podziału perspek-
tywicznego i użycie macierzy odwrotnej do macierzy rzutowania, powinno
ten sześcian przekształcić w kształt oryginalnej sceny. W przypadku rzuto-
wania izometrycznego, w którym przekształcenia wszystkich współrzędnych
są liniowe, bryła widzenia w układzie kamery będzie prostopadłościanem.
W przypadku rzutu perspektywicznego – będzie miała kształt frustum.
Zgodnie z konwencją OpenGL przyjmijmy, że ekran ułożony jest w taki
sposób, że w układzie odniesienia kamery znajduje się w płaszczyźnie pro-
stopadłej do osi OZ (osi optycznej wirtualnej kamery), a jego lewa i prawa
krawędź znajdują się w x = l i x = r, natomiast dolna i górna w y = b i y = t
(zwykle l i b są ujemne, a r i t – dodatnie, ale to nie jest konieczne). Ekran usta-
wiony jest o n od kamery, a tylna płaszczyzna ograniczająca scenę o f. Ponie-
waż oś OZ skierowana jest w kierunku do widza z zerem w położeniu kamery
(Rysunek 1), ekran znajduje się w z=–n, a tylna płaszczyzna odcinania w z=–f.
6 / 5 . 2014 . (24) /
programowanie grafiki
Wartości współrzędnej z przyjmują wartości ujemne (n i f są dodatnie).Te war-
tości, a więc kolejno l, r, b, t, n i f są argumentami funkcji glFrustum i glOrtho
tradycyjnego OpenGL (nieskuteczne w trybie rdzennym). Jak wspomniałem,
w efekcie rzutowania i następującego po nim dzielenia przez współrzędną w
uzyskujemy wartości współrzędnych w układzie NDC, w którym scena staje
się sześcianem o boku 2. W tym układzie współrzędne x, y i z przyjmują war-
tości od -1 do 1, przy czym poszczególne współrzędne są transformowane
w taki sposób, aby zachodziły relacje:
(1)
Zwróćmy uwagę, że w przypadku współrzędnej z zmianie ulega kierunek osi
OZ (pamiętajmy, że f > n). W układach przycinania i NDC wartość współrzęd-
nej z oddaje bowiem głębię, czyli odległość werteksu od kamery, i rośnie, gdy
przesuwamy się od ekranu do tylnej„ściany”frustum. Przekształcenia wszyst-
kich współrzędnych z układu kamery do układu przycinania są liniowe, a więc
zadane przez:
(2)
Uprzedzając fakty, zdradzę jednak, że o ile przekształcenie współrzęd-
nych x i y do układu NDC także jest liniowe, to nie musi tak być w przypad-
ku współrzędnej z, a mówiąc konkretniej: nie jest tak w przypadku rzutu
perspektywicznego.
We wzorze (2) stosuję oznaczenia, w których indeks c oznacza współ-
rzędną w układzie przycinania (ang. clipping), a e – współrzędną w układzie
kamery (od ang. eye, czyli oko). To typowa konwencja grafiki 3D, którą posta-
nowiłem tutaj zachować, pomimo że od„oka”wolę używać terminu„kamera”.
Macierz rzutowania izometrycznego
Rysunek 2. Rzut z kierunku +y na płaszczyznę OXZ
Wyprowadźmy najpierw wzór macierzy rzutowania izometrycznego. Po pierw-
sze dlatego, że jest prostsza – wszystkie przekształcenia są w niej liniowe, a po
wtóre – wyprowadzenie jej można traktować jako wstęp do wyprowadzenia
macierzy rzutowania perspektywicznego.
Skupmy się na początek na współrzędnej x. Wiemy, że dla wartości xe = l
powinniśmy uzyskać xc = -1, natomiast dla xe = r otrzymamy xc = 1. Mamy za-
tem dwa warunki, które pozwolą nam znaleźć wartości stałych ax i bx:
(3)
Z drugiego równania znajdujemy wartość stałej którą wsta-
wiamy do pierwszego, co pozwoli nam ustalić, że:
(4)
Jeżeli r > l, to stała ax jest dodatnia. Jest ona odpowiedzialna za przeskalowanie
układów odniesienia – równa się wobec tego stosunkowi szerokości płaszczy-
zny odcinającej w bliży (ekranu) w układach przycinania (równa 2) i kamery
(równa r – l). Druga stała opisuje różnicę położenia początku układu współrzęd-
nych w obu układach – jeżeli r i l mają wartości symetryczne względem zera
tj. l = –r, to bx równa jest zeru. W efekcie wzór transformacji przyjmuje postać:
(5)
Sprawdźmy, czy wszystko jest w porządku, podstawiając za xe wartości
l, (l+r)/2 i r. Uzyskamy kolejno: –1, 0 i 1. Analogiczne rozumowanie prowadzi
do znalezienia wzoru na transformację współrzędnej y:
(6)
i współrzędnej z:
(7)
W tym trzecim przypadku minus przy pierwszym wyrazie związany jest ze
zmianą kierunku osi OZ. W przypadku transformacji rzutowania izometrycz-
nego, dzielenie perspektywiczne nie powinno wprowadzać żadnych zmian.
Dlatego współrzędna wc powinna mieć wartość równą jedności. Z tego wyni-
ka, że transformacja do współrzędnych przycinania, a potem do współrzęd-
nych NDC polega jedynie na ich przeskalowaniu.
We wzorach (5) – (8) kryje się jednak pewien problem: jest nim wolny wy-
raz niezależący od żadnej współrzędnej układu kamery. Taki wyraz uniemoż-
liwia zapis przekształcenia za pomocą macierzy! Sprawę ratuje jednak „nad-
miarowa”współrzędna w. Jeżeli założymy, że w układzie kamery współrzędna
we musi mieć wartość równą 1, to wówczas przekształcenie do współrzęd-
nych przycinania i NDC (w przypadku rzutu izometrycznego te dwa układy są
identyczne) można zapisać:
(8)
co z kolei można wyrazić w zwarty sposób za pomocą macierzy:
(9)
7/ www.programistamag.pl /
Macierze w grafice 3D
Jeszcze raz przypomnę, że formalnie rzecz biorąc macierz ta przeprowa-
dza współrzędne w układzie kamery do współrzędnych przycinania. Jednak
w tym przypadku dzielenie perspektywiczne nie zmienia ich wartości, bo
wc = we = 1, zatem współrzędne NDC mają taką samą wartość, co współrzęd-
ne przycinania.
Jeżeli założymy, że scena jest symetryczna względem osi optycznej kame-
ry, co jest częste w praktyce, czyli r = –l = w/2 (wielkość w = r – l to szerokość
ekranu) oraz t = –b = h/2 (h = t – b to wysokość ekranu), to macierz rzutowania
izometrycznego znacznie się uprości:
(10)
Zakładając dodatkowo, że w = 2 (l = –1, r = 1), h = 2 (t = 1, b = –1) i n = 1, uzy-
skamy postać:
(11)
Macierz rzutowania perspektywicznego
W rzutowaniu izometrycznym bryła widzenia ma kształt prostopadłościanu,
zatem jej boczne krawędzie są do siebie równoległe i jednocześnie równoległe
do osi OZ. W przypadku rzutu perspektywicznego jest inaczej – wszystkie te
krawędzie leżą na prostych, które przecinają się w jednym punkcie nazywanym
ogniskiemrzutowania.Topunkt,wktórymznajdujesiękamera–początekukła-
du współrzędnych kamery. W rzutowaniu izometrycznym położenie kamery
jest umowne. Z istnienia ogniska i ze zbiegania promieni widzenia do jednego
punktu wynika, że obiekt mniejszy, ale znajdujący się bliżej, może przesłaniać
obiekt większy, ale dalszy (por. Rysunek 3). Zastanówmy się wobec tego, jak
w miarę oddalania od kamery zmienia się wielkość obiektów, które na ekranie
mają ten sam rozmiar albo odwrotnie: jak rozmiar obrazu na ekranie w rzucie
perspektywicznym zależy od jego odległości od ekranu i kamery.W rzutowaniu
izometrycznym rozmiar obrazu zrzutowanego na ekran obiektu jest taki sam,
jak samego werteksu – współrzędne dowolnego werteksu po zrzutowaniu,
nazwijmy je xp i yp, mają te same wartości, co współrzędne xe i ye (Rysunek 2).
Rysunek 3. Rzut frustum z kierunku +y. Zaznaczony werteks Pe nie musi
znajdować się w płaszczyźnie OXZ zawierającej oś optyczną
Spójrzmy na kamerę i frustum„z góry”, tj. z kierunku +y. Pokazuje to Rysu-
nek 3 prezentujący płaszczyznę OXZ, który pozwoli nam sprawdzić, jak współ-
rzędna xp obrazu werteksu na ekranie powinna zależeć od współrzędnej ze
oryginalnego werteksu. Rozważmy w tym celu punkt Pe znajdujący się w ob-
rębie sceny. Z podobieństwa trójkątów OPeRe i OPpRp wynika, że
(12)
Pamiętajmy, że ze i zp = –n są mniejsze od zera. Te trójkąty nie muszą leżeć
w płaszczyźnie OXZ, zatem długości odcinków ORe i ORp wcale nie są równe ze
i zp, ale ich stosunek równy jest stosunkowi tych współrzędnych. Identyczne
proporcje można zapisać dla zmiennej y. Z tego wynika, że zrzutowany na
ekran obiekt ma rozmiary:
(13)
Już teraz widać, że rozmiar ten maleje wraz ze zwiększaniem wartości –ze,
czyli odległości od kamery. To właśnie jest perspektywiczne pomniejszenie
dalszych obiektów. Transformacja do układu NDC powinna je uwzględniać.
Właśnie dlatego w przypadku rzutowania perspektywicznego przekształce-
niom skalowania (8) nie podlegają współrzędne xe i ye oryginalnego werteksu,
a współrzędne xp i yp obrazu zrzutowanego na ekran.
Połączmy zatem oba przekształcenia. W tym celu musimy złożyć wzory
(5) – (7). Uzyskamy w ten sposób transformację współrzędnych werteksu
z układu kamery bezpośrednio do współrzędnych NDC:
(14)
Zwróćmy uwagę, że o ile we wzorach (14) zawartość nawiasów może być zapi-
sana za pomocą macierzy z elementami, które zależą jedynie od parametrów
frustum (stałe l, r, b i t), to współczynnik przed nawiasem zależy od współrzędnej
transformowanego werteksu. Takiego przekształcenia nie zapiszemy za pomocą
macierzy! A to oznacza, wbrew temu, co się zwykle mówi, że sama macierz rzu-
towania perspektywicznego wcale nie opisuje rzutowania perspektywicznego.
Zasadniczą pracę w tym względzie musi wziąć na siebie dzielenie perspekty-
wiczne, czyli redukcja współrzędnych jednorodnych w układzie przycinania do
współrzędnych kartezjańskich NDC z jednoczesnym podzieleniem współrzęd-
nych xc, yc i zc przez współrzędną wc. Jednak aby to zadziałało, musimy zadbać o
to, aby we współrzędnej wc znalazła się wartość współrzędnej ze. To oznacza, że
wzory na przekształcenie układu współrzędnych do układu przycinania, a więc
bez dzielenia perspektywicznego, powinny wyglądać następująco:
(15)
Jak widać do współrzędnej wc przeniosłem także minus, który widoczny jest
we wzorach (14). Nadal nie mamy jeszcze wzoru na transformację współrzęd-
nej z. Wiemy, że wartość zc nie powinna zależeć od xe i ye, jest natomiast linio-
wą funkcją ze (por. wzór 2):
8 / 5 . 2014 . (24) /
programowanie grafiki
(16)
Musimy także pamiętać o tym, że przy przejściu do współrzędnych NDC
wszystkie współrzędne, w tym także współrzędna zc, zostaną podzielone
przez wc = –ze, co jest konieczne, aby wprowadzić perspektywę:
(17)
przy czym az musi być ujemne, bo oś OZ w układzie NDC zmienia kierunek.
Przekształcenie współrzędnej ze do zNDC, w odróżnieniu od transformacji do
układu przycinania (16), traci charakter liniowy (por. Rysunek 4).
Rysunek 4.Współrzędna zNDC w funkcji współrzędnej ze z układu kamery do
układu NDC dla n = 1 i f = 10
Ponadto wiemy, że dla ze =–n powinniśmy uzyskać zNDC =–1, a dla ze =–f powin-
niśmy otrzymać zNDC = 1. Podstawiając te wartości do wzoru (17), otrzymujemy:
(18)
Proste przekształcenia pozwalają na znalezienie wartości az i bz:
(19)
Transformacja współrzędnej z z układu kamery do układu współrzędnych
NDC przyjmuje wobec tego postać
(20)
Nie usuwam minusów z tego wzoru, ponieważ minus przed nawiasem jest już
zafiksowany w dzieleniu perspektywicznym (ustaliliśmy wcześniej, że wc = –ze).
Pojawia się tu ponownie problem związany z wolnym wyrazem wewnątrz
nawiasu. I tym razem możemy go rozwiązać, zakładając, że współrzędna we
w układzie kamery musi mieć wartość równą 1. Wówczas można napisać:
(21)
W ten sposób ustaliliśmy przekształcenia wszystkich współrzędnych i mo-
żemy zapisać kompletny wzór na macierz rzutowania perspektywicznego:
(22)
Taki właśnie wzór implementuje funkcja glFrustum tradycyjnego OpenGL,
czy funkcja frustum z biblioteki GLM.
Sprawdźmy jeszcze, jak przekształcenie (21) zmieni wartość współrzęd-
nych ze = –n, – (n+f)/2 i f z układu odniesienia kamery. Pamiętając o wykre-
sie z Rysunku 3, nie należy spodziewać się „ładnych” wyników dla wartości
pośrednich między –n, a –f, w szczególności punkt o składowej ze leżącej
dokładnie pośrodku między płaszczyznami bliży i dali, w nowych współrzęd-
nych nie będzie miał współrzędnej zNDC równej zeru. Podstawiając te wartości
współrzędnej ze kolejno do wzoru (21), otrzymamy wyniki: –1, (f–n)/(f+n) i +1.
Wartość zerową zNDC otrzymamy natomiast dla ze = –2nf/(f+n).
Fakt, że przekształcenie współrzędnej z nie jest liniowe, nie jest tak dużym
problemem, jak mogłoby się wydawać. Po przeprowadzeniu transformacji
współrzędna ta służy bowiem jedynie do przeprowadzenia testu głębi. A do
tego ważne jest zachowanie kolejności odległości punktów od kamery. Wa-
runkiem jest więc jedynie, aby transformacja była zmienna monotonicznie,
a nie aby zachowywała proporcje odległości punktów od kamery.
Podobnie, jak zrobiliśmy w przypadku macierzy rzutowania izometrycznego,
sprawdźmy, jak wygląda macierz rzutowania perspektywicznego dla symetrycz-
nie ustawionej sceny (stosuję te same oznaczenia, co w przypadku wzoru (10)):
(23)
Ponadto dla w = 2, h = 2 i n = 1 otrzymamy
(24)
Różnice funkcji glFrustum i gluPerspective
Rysunek 5.Widziany z boku (z kierunku +x) obszar przed ekranem
9/ www.programistamag.pl /
Macierze w grafice 3D
Biblioteka pomocnicza GLU (od ang. OpenGL Utility Library) zawiera często
używaną funkcję gluPerspective. Efektem jej wywołania jest, podobnie jak
w przypadku glFrustum, pomnożenie bieżącej macierzy przez macierz rzu-
towania perspektywicznego. Różni się jednak przyjmowanymi argumentami:
gluPerspective(ϕ, w/h, n, f);
Oiledwaostatnieparametrynifsątakiesame,jakwprzypadkuglFrustum,tzn.
są odległością bliży i dali od kamery, to dwa pierwsze się różnią. Argument drugi
to stosunek szerokości w do wysokości h viewportu (ang. aspect ratio), a φ to kąt
pionowego pola widzenia wyrażony w stopniach (ang. nazwa tego parametru to
fovy,odfieldofviewy).ZRysunku5wynika,żekątówmożnazwiązaćzrozmiarem
ekranu i odległością ekranu od kamery, korzystając z funkcji trygonometrycz-
nych. Weźmy pod uwagę górny trójkąt z rysunku rozciągnięty między trzema
punktami o zaznaczonych na rysunku współrzędnych (dla wygody narysowany
także z prawej strony). Jego wysokość równa jest połowie wysokości ekranu h/2,
a długość podstawy równa odległości kamery od ekranu n. Zatem:
(25)
Argumentów funkcji gluPerspective jest mniej i są bardziej intuicyjne. Proszę
jednak zauważyć, że możliwe jest to dzięki założeniu, że scena jest ustawiona
symetrycznie względem kamery (jak we wzorze (23)). Założenie symetrii sceny
oznacza, że samodzielnie implementując funkcję z takimi parametrami, jak w
gluPerspective,możemywykorzystaćfunkcjęodpowiadającąglFrustum,ale
nie odwrotnie. Znajdźmy zatem wartości parametrów funkcji glFrustum, znając
wartości parametrów funkcji gluPerspective. Z założenia symetrii wynika, że
(26)
Ponadto możemy obliczyć wysokość ekranu, korzystając ze wzoru (25), czyli:
(27)
Z kolei znając wysokość ekranu i jego proporcję, możemy łatwo znaleźć także
jego szerokość, która równa jest iloczynowi tych dwóch wartości. Nie jestem
jednak przekonany, czy wykorzystanie odpowiednika funkcji glFrustum do
implementacji odpowiednika gluPerspective jest opłacalne, czy nie ła-
twiej jest użyć po prostu wzoru (23).
Współrzędne viewportu
Układ współrzędnych NDC nie jest jeszcze ostatnim układem, jaki jest stoso-
wany w grafice 3D. Na końcu potoku renderowania wykonywana jest jeszcze
jedna liniowa transformacja: zmienne x i y są przeskalowywane w taki spo-
sób, aby obejmowały zakres obszaru klienta, w którym znajduje się viewport,
i zaokrąglane do liczb całkowitych (jednostką jest teraz piksel), a głębokość
przekształcana jest do zakresu od 0 do 1:
(28)
To układ współrzędnych viewportu (ang. viewport coordinates). Wielkości
X i Y wyznaczają lewy dolny róg viewportu, a W i H to odpowiednio jego sze-
rokość i wysokość (we współrzędnych obszaru klienta okna). Zakres zmiennej
z w układzie okna można kontrolować za pomocą funkcji glDepthRange,
domyślnie jest to właśnie zakres od 0 do 1. Korzystając ze współrzędnych
zv (skonwertowanych do liczb całkowitych), przeprowadzany jest test głębi,
a współrzędne xv i yv używane są do zapełnienia bufora ramki, w którym przy-
gotowywany jest obraz widoczny na ekranie monitora.
Przykład
Sprawdźmy, jak ten ciąg przekształceń wygląda w praktyce. Załóżmy, że na
scenie znajduje się trójkąt o wierzchołkach w punktach A = (–1, –1, 0, 1),
B = (1, –1, 0, 1) i C = (0, 1, 0, 1). To są współrzędne w lokalnym układzie od-
niesienia. Załóżmy, że macierz świata jest jednostkowa, a macierz widoku
taka, że obiekt odsunięty jest od kamery o 3. W układzie kamery punkty mają
zatem współrzędne Ae = (–1, –1, –3, 1), Be = (1, –1, –3, 1) i Ce = (0, 1, –3, 1).
Jeżeli do ustawienia macierzy w tradycyjnym OpenGL użylibyśmy nieobecnej
w trybie rdzennym funkcji glFrustum z argumentami
glFrustum(-1.0f, 1.0f, 0.71f*-1.0f, 0.71f*1.0f, 1.0f, 10.0f);
to stałe określające bryłę widzenia równe są l = –1, r = 1, n = 1 i f = 10.Wartości
b i t zależą od rozmiaru okna tak, aby proporcja wirtualnego ekranu na rzutni
była taka sama, jak proporcja viewportu, a tym samym trójkąt widoczny na
ekranie miał takie same kąty, co ten wirtualny. Dla okna o rozmiarze 800×600
w Windows 7, obszar klienta, którego całą powierzchnię zajmuje viewport,
ma wielkość W×H = 782×555.To oznacza, że proporcja H/W będzie miała war-
tość 0.709718645 (to jej odwrotność nazywana jest aspect ratio). Na potrzeby
tego ćwiczenia zaokrąglimy ją do 0.71, co jest pewnie zbyt zgrubne, biorąc
pod uwagę fakt, że piksel może mieć szerokość mniejszą niż jedną tysięczną
szerokości viewportu. Wobec tego b = –0.71, a t = 0.71. Macierz rzutowania
perspektywicznego będzie zatem miała postać:
(29)
Transformacja punktów A , B i C do współrzędnych przycinania wygląda
następująco:
(30)
10 / 5 . 2014 . (24) /
programowanie grafiki
Ponieważ wszystkie trzy punkty leżą na jednej płaszczyźnie prostopadłej do
osi optycznej kamery ze = –3, to werteksy różnią się tylko współrzędnymi x i y.
Tak pozostaje w układzie przycięcia. To się nie zmieni również po przejściu do
układu NDC, czyli po podzieleniu przez wc = –ze = 3. Gdyby trójkąt znajdował
się jeszcze dalej od płaszczyzny bliży, np. w ze = –5.5 (środek między bliżą a
dalą), to współrzędna zc wzrosłaby do 4.49. Rośnie, bo w układzie przycięcia
zmienia się kierunek osi z. Dla ze = –10, tj. gdyby trójkąt znajdował się w płasz-
czyźnie dali, otrzymujemy zc = 10. Wróćmy jednak do ze = –3. W układzie NDC
współrzędne punktów będą wówczas równe:
(31)
Wartości współrzędnych xc i yc punktów ANDC i BNDC wskazują, że trójkąt powi-
nien zajmować mniej więcej jedną trzecią szerokości okna i prawie połowę
wysokości. I tak jest w rzeczywistości (por. Rysunek 6).
Rysunek 6. Ostateczne współrzędne pikseli odpowiadających punktom A, B i C
Sprawdźmy jeszcze, jak zmieniałaby się współrzędna zNDC przy zmianie współ-
rzędnej ze. Za współrzędną ze podstawmy wartości, dla których przed chwilą
obliczaliśmy współrzędną przycinania zc. Dla ze = –5.5 otrzymaliśmy zc = 4.49,
czyli zNDC = 4.49/5.5 = 0.81(63). Dla ze = 10, zc = 10, więc, jak należało się spo-
dziewać zNCD = 1.
Ostatnim przekształceniem, za które, podobnie jak za podział perspekty-
wiczny także nie jesteśmy odpowiedzialni, przygotowując shadery dla karty
graficznej, jest przejście do układu współrzędnych viewportu (kontrolują je
funkcje glDepthRange i glViewport). A ponieważ X = 0 i Y = 0 oraz W = 782
i H = 555 (nasz viewport zajmuje cały obszar klienta), to przekształcenia te opi-
sane są wzorami:
(32)
W efekcie otrzymujemy (por. Rysunek 6)
(33)
Sprawdźmy także, jak zmieni się obraz trójkąta, jeżeli rzut perspektywiczny
zastąpimy rzutem izometrycznym, a więc jeżeli zamiast funkcji glFrustum,
użyjemy funkcji glOrtho z tymi samymi argumentami, tj.:
glOrtho(-1.0f, 1.0f, 0.71f*-1.0f, 0.71f*1.0f, 1.0f, 10.0f);
dla odwrotności aspect ratio równej 0.709718645 ≈ 0.71. Wówczas macierz
rzutowania przyjmie postać:
(34)
Korzystając z tej macierzy, możemy przetransformować punkty Ae, Be i Ce
z układu kamery do układu przycinania:
(35)
W tym przypadku odległość werteksów od ekranu w żaden sposób nie wpływa
na uzyskany obraz (współrzędne xc i yc), ma jedynie znaczenie przy teście głębi.
11/ www.programistamag.pl /
Macierze w grafice 3D
Konwersja do układu NDC jest trywialna, bo wc = 1:
(36)
Zwróćmy uwagę, że współrzędne y wszystkich punktów wykraczają poza
zakres (–1, 1). To oznacza, że wierzchołki trójkąta znajdą się poza ekranem
(Rysunek 7). I nic tu nie pomoże zwiększanie odsunięcia kamery od ekranu.
Sprawdźmy jeszcze, jak zmieniałaby się współrzędna zc = zNDC, gdybyśmy prze-
sunęli trójkąt w głąb do ze = –5.5 i ze = –10. Podstawiając te wartości do wzoru
(37)
otrzymamy odpowiednio 0 i 1. W odróżnieniu od przypadku rzutu perspekty-
wicznego, w rzucie izometrycznym transformacja współrzędnej z jest liniowa.
Rysunek 7. Obraz w przypadku rzutowania izometrycznego. Część trójkąta
nie mieści się w obrębie ekranu
I wreszcie przejście do współrzędnych viewportu (wzory 32) daje nam współ-
rzędnepikseliodpowiadającychwierzchołkomtrójkątawidocznegonaekranie:
(38)
Czyli rzeczywiście współrzędne y dolnych wierzchołków są mniejsze od
zera, a górnego większa od wysokości viewportu, tj. 555 (por. Rysunek 7).
Macierz świata
Macierz świata odpowiada za transformacje współrzędnych lokalnego układu
współrzędnych modelu do układu sceny. Układ ten może być dowolnie zo-
rientowany – zwykle związany jest z trwałym podłożem widocznym na scenie,
jeżeli takie jest obecne. Z tego powodu nie ma żadnych specjalnych funkcji,
które by pozwalały taką macierz zbudować. Będzie to natomiast dla nas okazją,
aby przyjrzeć się macierzom podstawowych przekształceń: translacji, skalowa-
nia, odbicia, pochylenia i obrotu. Poza pierwszym, wszystkie te przekształcenia
mogą być zapisane za pomocą macierzy 3×3. Tylko translacja wymaga macie-
rzy we współrzędnych jednorodnych. I od niej właśnie zaczniemy.
Translacja
Ciekawą własnością współrzędnych jednorodnych jest to, że umożliwiają
opisanie przesunięcia o dowolny wektor za pomocą macierzy. W zwykłych
współrzędnych kartezjańskich to nie jest możliwe – tam operacja przesunię-
cia opisywana jest przez dodanie wektorów:
(39)
Analogiczny wynik, z tym, że dla czterowymiarowych wektorów, może-
my uzyskać, jeżeli na współrzędne werteksu zadziałamy macierzą, w której
czwarta kolumna zawierać będzie współrzędne wektora translacji:
(40)
Zakładając, że w = 1, w wyniku działania macierzy otrzymamy wektor prze-
sunięty we współrzędnych x, y i z o wektor . Macierzą odwrotną do macierzy
translacji o wektor jest macierz translacji o wektor .
W tradycyjnym OpenGL funkcja glTranslate mnoży bieżącą macierz przez
macierz (39). Jej argumentami są wielkości ∆x, ∆y i ∆z.
Skalowanie i odbicia względem płaszczyzn
układu współrzędnych
Najprostszym przekształceniem jest skalowanie. Opisuje je macierz:
(41)
Działając nią na dowolny wektor, uzyskamy wektor, w którym poszczególne
współrzędne będą przemnożone przez odpowiednie współczynniki sx, sy lub sz:
(42)
12 / 5 . 2014 . (24) /
programowanie grafiki
W efekcie bryła, której macierz świata zawiera takie skalowanie, zostanie
w kierunku x powiększona sx razy. Analogicznie w kierunkach y i z. Jeżeli war-
tości współczynników są mniejsze od jedności (ale dodatnie), bryła zostanie
zmniejszona. Dla ujemnych wartości uzyskamy złożenie skalowania i odbicia
względem płaszczyzn wyznaczonych przez osie bieżącego układu współ-
rzędnych. Dla sx < 0 będzie to odbicie względem płaszczyzny OYZ.
Wielkości sx, sy i sz są argumentami funkcji glScale tradycyjnego OpenGL,
która mnoży bieżącą macierz przez macierz (41).
Macierz może również zawierać współczynnik sw (zamiast jedynki w ostat-
nim wierszu). Wówczas, w efekcie dzielenia perspektywicznego wszystkie
współrzędne wektora zostaną zmniejszone sw razy!
Obroty
Wróćmy na chwilę do zwykłej dwuwymiarowej płaszczyzny, na której obro-
ty są jednoznaczne. W takim przypadku jest tylko jedna możliwa oś obrotu
– prostopadła do tej płaszczyzny. Macierz obrotu równa jest wówczas
(43)
gdzie φ to kąt obrotu wokół środka układu współrzędnych skierowany w kie-
runku przeciwnym do ruchu wskazówek zegara (Rysunek 8, lewy). Ten wzór
podaję jako oczywisty, ale jego wyprowadzenie wcale nie jest takie trywialne.
Macierz (43) to macierz obrotu punktu w nieruchomym układzie odniesienia.
Warto to sprecyzować, bo czasem macierz obrotu dotyczy tzw. transformacji
pasywnej, w której to nie punkt jest obracany, a układ odniesienia. Macierz taka
jest wówczas transponowana względem macierzy (43) (por. Rysunek 8, prawy).
Rysunek 8. Obrót obiektu w układzie odniesienia oraz obrót układu względem
obiektu.Wzór (43) dotyczy sytuacji z lewego rysunku
W trzech wymiarach sprawa się komplikuje, bo oś obrotu możemy wy-
brać dowolnie. Pół biedy, jeżeli jest ona jedną z osi kartezjańskiego układu
współrzędnych. Wówczas macierze obrotu przyjmują proste formy, które są
prostym uogólnieniem wzoru (43). Najprościej to zobaczyć, jeżeli oś obrotu
będzie osią OZ.Wówczas zmiany wartości współrzędnych dotyczą tylko płasz-
czyzny OXY i macierz obrotu przyjmuje postać:
(44)
Sprawdźmy, jak macierz ta transformuje dowolny wektor położenia punktu
we współrzędnych jednorodnych:
(45)
Zmieniają się zatem współrzędne punktu w taki sposób, że:
(46)
Sprawdźmy dla przykładu, jak obraca się punkt położony początkowo na
osi OX: (1,0,0,1). W wyniku obrotu otrzymamy wektor o współrzędnych
. To oznacza, że składowa maleje w miarę zwięk-
szania kąta (jeżeli γ < π), a składowa y rośnie (dla γ < π/2). Zatem rzeczywiście
punkt obraca się w stronę osi OY, tj. w kierunku przeciwnym do wskazówek
zegara.
Jeżeli założymy, że , a to możemy łatwo sprawdzić, to odle-
głość obróconego punktu od punktu (0,0) nie zmienia się:
(47)
Analogicznie możemy zbudować macierz obrotu wokół osi OX. Wówczas
zmianie ulegają tylko współrzędne y i z:
(48)
Nieco inaczej wygląda macierz obrotu wokół osi OY:
(49)
Zmienia się element, przy którym jest znak minus. Przyczyna jest prosta. Za-
kładamy, że obrót punktu o dodatni kąt jest przeciwny do wskazówek zegara.
W przypadku osi OZ oznacza to, że obrót punktu znajdującego się w pierw-
szej ćwiartce następuje od osi OX do osi OY. W przypadku osi OX – od osi OY
do osi OZ. Natomiast w przypadku obrotu wokół osi OY zmiana następuje od
osi OZ do osi OX (Rysunek 9). Ta odwrócona kolejność osi powoduje, że ma-
cierz obrotu wygląda jak transponowana.
13/ www.programistamag.pl /
Macierze w grafice 3D
Rysunek 9. Kierunki obrotów wokół osi OX, OY i OZ
Trzy kąty obrotu wokół osi układu współrzędnych związanego z obraca-
nym obiektem, nazywane kątami Cardana, to typowy sposób opisu orientacji
samolotów (Rysunek 10). Kąt odpowiadający obrotom wokół osi pionowej
nazywa się kurs, a jego zmiana to odchylenie (ang. odpowiednio heading
i yaw). Obrót wokół osi skierowanej wzdłuż samolotu to przechylenie (roll),
a wokół osi poziomej, prostopadłej do kierunku lotu to nachylenie (pitch).
Te terminy stosowane są także w grafice 3D. Nachylenie (pitch) zmieniamy
wówczas, gdy wolant ciągniemy do siebie lub pchamy od siebie. Wówczas
samolot zadziera lub obniża nos. Jeżeli wolant przechylimy na bok, spowo-
dujemy przechylenie samolotu (jedno skrzydło będzie wówczas wyżej od
drugiego) – zmienia się kąt określany angielskim terminem roll, czyli właśnie
Rysunek 10. Jak opisać zmianę orientacji modelu?
Używając tego ostatniego wzoru do implementacji, oszczędzimy na kilku
mnożeniach.
W fizyce bryły sztywnej zamiast kątów Cardana używa się raczej kątów
Eulera. Różnią się tym od kątów Cardana, że jedna z osi obrotów wybierana
(50)
czona jest mniej więcej przez ostatni kręg kręgosłupa). Możemy także, choć w
niezbyt dużym zakresie, przechylać głowę na boki.
Wróćmy jednak do samolotu. Ustalmy, że osie układu zorientowane są
w taki sposób, że oś OZ wyznacza kierunek dziobu samolotu, a oś OY wska-
zuje jego górę. W konsekwencji oś OX przebiega wzdłuż jego prawego skrzy-
dła. Wówczas kąt γ to przechylenie (roll), kąt β– nachylenie (pitch), a kąt α
– odchylenie (yaw). Wszystkie trzy kąty mogą służyć do ustalenia zmiany orien-
tacji samolotu. Ważna jest przy tym, i należy to szczególnie mocno podkreślić,
kolejność obrotów wokół poszczególnych osi. Najczęściej używana konwencja
to rozpoczęcie od przechylenia, potem nachylenie i wreszcie odchylenie, a więc
użycie macierzy: . Macierz, jaką wówczas uzyskamy, to
przechylenie. Wreszcie naciśnięcie orczyka zmienia
pozycję steru kierunkowego, a co za tym idzie, kurs
samolotu, czyli heading lub yaw. Mylące w tym mode-
lu jest to, że lekkie położenie na boku rzeczywistego
samolotu (tj. roll) w obecności grawitacji i powietrza
również powoduje zmianę jego kierunku – dzieje się
tak ze względu na zmianę kierunku siły nośnej, któ-
ra jest prostopadła do płaszczyzny skrzydeł. Zamiast
samolotu lepiej byłoby zatem wyobrażać sobie waha-
dłowiec w stanie nieważkości. Możemy też użyć jako
przykładu własnej głowy. Możemy ją skręcać w lewo
i prawo (oś obrotu wyznaczona jest przez kręgosłup),
możemy spoglądać w dół i w górę (oś obrotu wyzna-
spośród osi lokalnego układów współrzędnych zostanie powtórzona. Dzię-
ki wcześniejszym obrotom, przy drugim obrocie nie jest ona już jednak tak
samo zorientowana. Klasycznym zestawem jest kolejność osi: OZ, OX i obró-
cona oś OZ. Oznacza to macierz:
(51)
14 / 5 . 2014 . (24) /
programowanie grafiki
W przypadku użycia kątów Eulera grozi nam jednak tak zwana bloka-
da przegubowa (ang. gimbal lock). Gdy obrót układu (lub bryły) składa się
z trzech następujących po sobie obrotów wokół niezależnych prostopadłych
osi, to problem ów pojawi się wtedy, gdy pierwszy obrót wykonamy o kąt
90 stopni. Wówczas nowa oś obrotu OX pokrywa się z pierwotną osią OY, co
zmniejsza ilość stopni swobody i powoduje, że obroty wokół drugiej osi zmie-
niają jedynie „polaryzację” układu. Doświadczymy tego, gdy podniesiemy
głowę tak, żeby spoglądać pionowo w górę, a następnie obracamy się wokół
własnej osi. Punkt, na który patrzymy wówczas, się nie zmieni. Inna odmiana
tego samego problemu pojawi się, gdy obrót o kąt prosty wykonany zostanie
jako drugi z trzech obrotów. Wówczas trzeci obrót jest wykonywany wokół tej
samej osi co pierwszy i skutek jest podobny. Drugą stroną tego samego pro-
blemu jest fakt, że ten sam ostateczny obrót można osiągnąć za pomocą wie-
lu zestawów trzech kątów Eulera.To bardzo utrudnia porównywanie obrotów
i ich interpolację (np. przy automatycznym ruchu kamery). Rozwiązaniem jest
użycie macierzy obrotu lub kwaternionów.
Obrót wokół dowolnej osi
W praktycznych zastosowaniach bardzo ważna jest możliwość obracania mo-
deli wokół wskazanej osi. To zadanie realizuje funkcja glRotate tradycyjnego
OpenGL. Wyprowadzenie wzoru przeprowadzę we współrzędnych kartezjań-
skich, bo i bez współczynnika skalowania wzory będą bardzo długie. Załóżmy
oś obrotu wyznaczoną przez jednostkowy werktor , przechodzącą przez
początek kartezjańskiego układu współrzędnych O = (0,0,0) i dowolny punkt
P = (x, y, z), który chcemy obrócić o kąt θ przeciwnie do wskazówek zegara.
Rysunek 11. Obrót punktu wokół osi
Zastanówmy się, w jaki sposób zapisać, że
punkt P wskazywany przez wektor wodzą-
cy został obrócony o kąt θ (Rysunek 11).
Jeżeli wektor rozłożymy na część równo-
ległą i prostopadłą do osi obrotu: , to część równoległa nie ule-
gnie żadnej zmianie, natomiast część prostopadła będzie podlegać prawom
obrotu dwuwymiarowego. W efekcie jeżeli obrócony punkt wskazywany jest
przez wektor to:
(52)
Dwuwymiarowa przestrzeń obrotu prostopadłej części wektora, w której zapisa-
na została macierz w drugim wzorze, wyznaczona jest przez wektor i wektor
doniegoprostopadły,ajednocześnieprostopadłydowektorów i .Możnago
zatem wyznaczyć, korzystając z iloczynu wektorowego . Załóżmy, że oś
obrotu skierowana jest w kierunku z. Wówczas dwuwymiarowa przestrzeń pro-
stopadła może być zapisana za pomocą współrzędnych x i y:
(53)
To oznacza, że:
(54)
Ostatecznie obrócony wektor dany jest wzorem:
(55)
Jak, korzystając z tego wzoru, napisać macierz obrotu, której elementy będą
zawierały współrzędne wektora i kąt θ? Przede wszystkim
musimy przepisać wzór (55) w taki sposób, aby opisywał transformację całe-
go wektora .Wykorzystajmy fakt, że iloczyn wektorowy wektorów i jest
równy zeru. Wobec tego . Składową równoległą wektora
rozłóżmy na dwie części
(56)
Wówczas możemy przepisać wzór (55) jako:
(57)
Przechodząc do zapisu macierzowego, wykorzystajmy operator gwiazdki do
zapisu iloczynu wektorowego oraz iloczyn tensorowy (diadyczny) do oblicze-
nia składowej równoległej wektora. W efekcie uzyskamy:
(58)
(59)
Uzyskaliśmy w ten sposób przekształcenie wektora , które, po zsumowaniu
trzech wyrazów w nawiasie klamrowym, równoważne jest macierzy:
15/ www.programistamag.pl /
Macierze w grafice 3D
(60)
ku ujemnych wartości x (w obróconym układzie współrzędnych). Realizuje to
macierz będąca iloczynem trzech macierzy przekształceń: translacji, obrotu
i ponownie translacji:
W efekcie uzyskujemy macierz, w której łatwo rozróżnić część dotyczącą ob-
rotu oraz część dotyczącą przesunięcia. Przesunięcie to jest złożeniem prze-
sunięcia do środka obrotu i do nowego położenia obiektu.
Pochylenie
Mniej intuicyjnym przekształceniem, i stosunkowo rzadko używanym, jest
pochylenie (ang. shear lub skew). Nie ma odpowiadającej mu funkcji w tra-
dycyjnym OpenGL. Mówimy o nim w zasadzie tylko dlatego, że często jest
niechcianym efektem błędnego zdefiniowania macierzy świata lub pojawia
się w wyniku kumulacji błędów numerycznych. Pochylenie jest jedynym
z omawianych w tej części przekształceń, które nie zachowuje kątów.
Zmodyfikujmy macierz jednostkową w taki sposób, aby poza jedynkami
na diagonali, któryś z pozostałych elementów także był różny od zera. Oto
przykład:
(62)
Jeżeli współrzędne wektora przemnożymy przez taką macierz, uzyskamy:
(63)
Sprawdźmy tę macierz, choćby dla jednego prostego przypadku. Załóżmy oś
obrotu skierowaną wzdłuż osi OZ: . Wówczas macierz znacznie
się upraszcza, bo wszystkie iloczyny różnych składowych wektora są równe
zeru, i uzyskujemy macierz postaci:
Uzyskaliśmy więc, pomijając brak współrzędnej w, macierz ze wzoru (44).
Macierze obrotu a kwaterniony jednostkowe
Poza kątami Cardana lub Eulera oraz macierzami, istnieje jeszcze trzeci
uznany sposób opisu obrotów – kwaterniony jednostkowe. Z ich pomocą
można łatwo zapisać obrót wokół osi wyznaczonej przez wektor o zadany
kąt, tzn. można wykazać ich pełną równoważność z macierzą (59). Warto
także zaznaczyć, że kwaterniony pozwalają na zapisanie równań ruchu bryły
sztywnej.Więcej na ten temat w książce Grafika, fizyka, metody numeryczne.
Symulacje fizyczne z symulacją 3D wydanej przez PWN w 2010 roku.
Złożenie obrotów i translacji – obrót wokół
dowolnego punktu
Wszystkie macierze obrotów, które wyżej przedstawiłem, opisują obroty wo-
kół środka bieżącego układu odniesienia. A co w przypadku, gdy chcemy wy-
konać obrót wokół osi przesuniętej względem początku układu współrzęd-
nych? W takim wypadku konieczne jest złożenie macierzy obrotu z macierzą
translacji:
(61)
gdzie to macierz obrotu, a jest macierzą translacji o wektor . Najpierw
przesuwamy się do punktu, wokół którego ma być wykonywany obrót, na-
stępnie wykonywany jest ów obrót. Wreszcie następuje przesunięcie z po-
wrotem, ale w nowym, obróconym układzie (Rysunek 12). Nie wracamy więc
do tego samego punktu, a do punktu obróconego względem punktu, do któ-
rego przesunęła nas macierz .
Rysunek 12. Złożenie dwóch translacji i obrotu, czyli obrót względem wyznaczo-
nego punktu
Sprawdźmy na prostym przykładzie, jak wygląda macierz takiego przekształ-
cenia złożonego z trzech przekształceń, a konkretnie, jak będzie wyglądała
macierz, która realizuje obrót wokół osi równoległej do OZ, ale przesuniętej w
kierunku +x o . Oczywiście opisuje ją nadal macierz 4×4. Aby ją otrzymać,
musimy przesunąć lokalny układ współrzędnych o w kierunku dodatnich
wartości x, czyli do położenia osi obrotu. Następnie obrócić ów układ o kąt
wokół nowej osi OZ i wreszcie przesunąć go z powrotem o , tj. w kierun-
16 / 5 . 2014 . (24) /
programowanie grafiki
W efekcie współrzędne x zaczną zależeć od współrzędnej y sprzed trans-
formacji i bryły zostaną pochylone o kąt, którego tangen-
sem jest wartość pxy (Rysunek 13).
Rysunek 13. Pochylenie
Najłatwiej to zrozumieć, myśląc o wersorach, czyli wektorach jednostkowych
skierowanych wzdłuż osi układu współrzędnych,„zaszytych”w macierzy prze-
kształcenia (zob. ramka poniżej). Pomyślmy, że nowy układ współrzędnych
nie ma już prostopadłych osi. Kąt między nimi zmalał bowiem o kąt równy
W efekcie zniekształceniu ulega cała scena.
Zacznijmy od zrozumienia, czym tak naprawdę jest macierz przekształ-
cenia (dla uproszczenia o rozmiarach 3×3). Powinna przeprowadzić jeden
układ współrzędnych O w inny układ współrzędnych O’ (ograniczenie roz-
miaru powoduje, że początki obu układów pozostają w tym samym punk-
cie). Aby to lepiej poczuć, sprawdźmy, co się stanie, gdy macierzą zadzia-
łamy na wersory nieruchomego układu współrzędnych O.
W wyniku uzyskaliśmy trzy kolumny macierzy przekształcenia. Kolumny te
przechowują więc współrzędne wersorów układu O’, ale wyrażone wzglę-
dem układu O. Dla przykładu .
Współrzędną można oczywiście pochylić poza płaszczyznę OXY, uzależ-
niając ją także od współrzędnej z. Pozwoli na to macierz:
(64)
Analogicznie można wprowadzić pochylenie w osi OY lub OZ.Wystarczy tylko
ustawić odpowiednie wartości elementów macierzy:
(65)
Pierwszy wiersz odpowiada za pochylenie osi OX, drugi OY, a trzeci OZ.
Rzut na płaszczyznę
Jedną z macierzy, która może się przydać, a której odpowiednika także nie
ma w tradycyjnym OpenGL, jest macierz opisująca rzutowanie punktu na
płaszczyznę. Wykorzystuje się ją chociażby do prostego generowania cieni.
Macierz taka może być zapisana tylko we współrzędnych jednorodnych.
Rysunek 14. Schemat rzutowania punktu na płaszczyznę
My jednak zaczniemy od prostych wzorów we współrzędnych kartezjańskich.
Załóżmy, że płaszczyzna, na którą rzutujemy punkt (rzutnia, Rysunek 14), opi-
sana jest wzorem:
(66)
gdzie d to odległość płaszczyzny od początku układu współrzędnych, a to
trójwymiarowy wektor normalny do płaszczyzny. Rzutowany punkt i śro-
dek rzutowania (w przykładzie generowania cienia środkiem rzutowania jest
punktowe źródło światła) wyznaczają prostą (promień):
(67)
Natejprostejmusioczywiścieleżećrównieżrzutowegopunktu(Rysunek14).Szu-
kanym punktem jest więc przecięcie prostej i płaszczyzny.Wobec tego podstaw-
my wzór punktu na prostej do wzoru wyznaczającego punkty na płaszczyźnie:
(68)
i wyznaczmy konkretną wartość współczynnika k, dla którego punkt na pro-
stej znajduje się także na płaszczyźnie:
(69)
Jeżeli wstawimy jawną postać tego współczynnika do wzoru na prostą, otrzy-
mamy gotowy przepis na obliczenie rzutu punktu:
(70)
17/ www.programistamag.pl /
Macierze w grafice 3D
Dwa pierwsze wyrazy licznika tworzą podwójny iloczyn wektorowy. Ta
informacja na niewiele nam się jednak przyda. Ważniejsze jest, że skróciły
się dwa wyrazy, w których pojawiałyby się iloczyny składowych wektora. Nie
oznacza to jeszcze, że położenie rzutu zależy liniowo od położenia rzutowa-
nego punktu (co pozwalałoby na zapisanie rzutowania za pomocą macierzy).
Współrzędne rzutowanego punktu pojawiają się niestety w mianowniku po-
wyższych wzorów:
(71)
gdzie , i to współrzędne wektora , a , i – wektora .
Wszystkie współrzędne rzutu skalowane są identycznym współczynni-
kiem o wartości . To sugeruje, że aby pozbyć się mianownika,
można przejść do współrzędnych jednorodnych, w których można go będzie
przenieść do licznika współrzędnej w. Po rozszerzeniu układu współrzędnych
uzyskamy:
(72)
Wektory i uzupełniamy o czwartą współrzędną w: , .
Zgodnie z własnością współrzędnych jednorodnych wolne wyrazy opisujące
translację o wektor znajdą się w czwartej kolumnie macierzy – wiążę je ze
składową w wektora określającego położenie rzutowanego punktu. Zauważ-
my, że jeżeli wprowadzimy dodatkowo oznaczenie i uporząd-
kujemy wzory, to całe przekształcenie ujawni interesującą symetrię:
(73)
Nowe oznaczenie jest zgodne z definicją płaszczyzny we współrzędnych jed-
norodnych, która określona jest iloczynem skalarnym w tych współrzędnych:
(74)
Nic już nie stoi na przeszkodzie, aby transformację punktu :
do jego rzutu
zapisać za pomocą macierzy:
(75)
Użyta w elementach diagonalnych liczba równa jest iloczynowi skalarnemu
we współrzędnych jednorodnych wektora określającego położenie źródła
światła (lub ogólniej: środka rzutowania) i wektora normalnego do rzutni:
(76)
Rozszerzenie współrzędnych kartezjańskich do współrzędnych jednorod-
nych, poza tym, że daje możliwość zapisu rzutowania za pomocą macierzy,
ma także inną, sygnalizowaną już, zaletę. W tych współrzędnych możemy
przesunąć źródło światła do nieskończoności. Wprawdzie powyższe rozwa-
żania zakładają, że współrzędne w źródła światła i rzutowanego punktu są
równe jedności ( , ), jednak wyprowadzona w ten sposób
macierz rzutowania daje poprawne rezultaty również dla dowolnych war-
tości tych elementów, także gdy . Uzyskujemy wówczas rzut rów-
noległy, który z tego punktu widzenia jest szczególnym przypadkiem rzutu
perspektywicznego.
Macierz widoku
Macierz widoku określa transformację z układu sceny do układu kamery.
Postaramy się odtworzyć macierz tworzoną przez wygodną funkcję glu-
LookAt z biblioteki GLU służącą do określania położenia i orientacji kamery.
Argumentami tej funkcji są trzy wektory określone w układzie współrzędnych
sceny: wektor wskazujący położenie kamery E, punkt, na który kamera jest
skierowana C i, tzw. wektor polaryzacji (symbol od ang. up – góra).
void gluLookAt(GLdouble eyeX, GLdouble eyeY, GLdouble eyeZ,
GLdouble centerX, GLdouble centerY, GLdouble centerZ,
GLdouble upX, GLdouble upY, GLdouble upZ);
Punkty E i C wyznaczają wektor (od ang. forward – do przodu) i zarazem oś
optyczną kamery. Wzdłuż tej osi (od punktu C do E) położona będzie oś OZ w
układzie współrzędnych kamery, choć skierowana w przeciwną stronę. Kamera
skierowana na punkt C może być dowolnie obrócona wokół osi optycznej i dla-
tego konieczny jest wektor polaryzacji, który wyznacza jej orientację. Rysunek
15, który ilustruje tę sytuację, nie pokazuje jednak sytuacji ogólnej, choć bar-
dzo często spotykanej. Zakłada bowiem, że kamera skierowana jest na środek
układu współrzędnych sceny, zatem C = O = (0,0,0), a wektor
jest skierowany wzdłuż osi OY. Z Rysunku 15 widać, że macierz tworzoną przez
funkcję gluLookAt można wyprowadzić przez złożenie dwóch obrotów:
pierwszego wokół osi OY o kąt β, i drugiego wokół nowej obróconej osi OX'
o kąt α, oraz translacji do punktu E, czyli w naszym szczególnym przypadku
o wektor . Łatwiej jednak znaleźć ją, traktując ją jako transformację ukła-
du współrzędnych i pamiętając, że kolumny macierzy 3×3 stanowią wersory
nowego układu współrzędnych wyrażone w starym układzie współrzędnych,
18 / 5 . 2014 . (24) /
programowanie grafiki
a zatem wersory osi OX, OY i OZ układu współrzędnych kamery widziane z ukła-
du współrzędnych związanego ze sceną. W czwartej kolumnie wstawimy poło-
żenie punktu E, dzięki czemu macierz będzie także uwzględniała przesunięcie
początku układu współrzędnych do położenia kamery.
Rysunek 15. Położenie i orientacja kamery w układzie współrzędnych sceny
Po transformacji do układu współrzędnych kamery, oś OZ będzie wyznaczona
przez wektor (skierowany od centrum do kamery). Oś OX będzie jedno-
cześnie prostopadła do osi OZ i wektora (wektory i nie mogą być
równoległe!).Wskazywać będzie ją wektor (od ang. right, czyli prawo).Wek-
tor jednoznacznie wyznacza polaryzację kamery, ale niekoniecznie jest
prostopadły do wektorów i , dlatego należy znaleźć wektor , który
ten warunek będzie spełniał, pozostając w płaszczyźnie napiętej na wekto-
rach i (płaszczyzna zaznaczona na Rysunku 15, lewym). To oznacza, że
należy wykonać następującą ogólną konstrukcję:
1. oblicz wektor
2. zapisz znormalizowaną wartość tego wektora ,
3. oblicz wektor prostopadły do wektorów i , korzystając z iloczy-
nu wektorowego (zwrot wyniku wyznacza reguła śruby
prawoskrętnej),
4. znormalizuj wektor ,
5. oblicz wektor prostopadły do wektorów i (wektor
będzie jednostkowy, bo oba czynniki są jednostkowe, a jednocześnie pro-
stopadłe do siebie).
Wersorami nowego układu współrzędnych kamery (wyrażonymi we współ-
rzędnych sceny) są zatem: , i , co oznacza, że
macierz widoku obracająca współrzędne z układu sceny do układu kamery
(jeszcze bez przesunięcia) powinna wyglądać następująco:
(77)
A co z przesunięciem? W przypadku, w którym C = O = (0,0,0), mamy dwie
równoważne opcje: możemy zacząć serię przekształceń od przesunięcia ka-
mery do punktu E, tj. o wektor , a potem wykonać obroty, lub najpierw
wykonać obroty, a dopiero po nich przesunąć scenę w kierunku przeciwnym
do osi OZ w nowym układzie kamery o odległość równą długości wektora ,
tj. o wektor :
(78)
W tym drugim przypadku macierz widoku wygląda znacznie lepiej, ale to pierw-
szy przypadek jest ogólniejszy i działa dla dowolnego położenia punktu C:
(79)
W przypadku, w którym C = O = (0,0,0), wektor , a ponadto jest
prostopadły do wektorów i . W efekcie pierwsze dwa wyrazy czwartej ko-
lumny równe są zeru, a trzeci staje się długością wektora ze zmienionym
znakiem i tym samym wzór (79) przechodzi w (78).
Sprawdźmy,jakkonstrukcjamacierzywidokuwyglądawpraktyce.Najpierw
rozważmy prosty przykład, w którym kamera znajduje się w E= (0,0,3) i jest skie-
rowana jest na punkt C = (0,0,0) (pozycje w układzie sceny). Wektor polaryzacji
równy . Zatem scena i kamera są rozsunięte o 3 jednostki wzdłuż
osi OZ. Wówczas powyższa konstrukcja będzie przebiegać następująco:
1.
2.
3.
,
4. Wektor jest jednostkowy ( )
5.
.
Wektory , i są skierowane od-
powiedniowzdłużosiOX,OYiprzeciwniedoosiOZwukładziekamery.Sąwięcna
pewnodosiebieprostopadłe.Wefekciemacierzwidokuniebędzieobracałaukła-
du sceny, a będzie go jedynie przesuwać. Będzie to więc zwykła macierz translacji:
19/ www.programistamag.pl /
Macierze w grafice 3D
(80)
To oczywiście bardzo prosty przykład. Sprawdźmy więc, co się stanie, jeżeli
przesuniemy kamerę do punktu E = (–1,1,1), nie zmieniając punktu C = (0,0,0),
na który kamera jest skierowana.To bardziej odpowiada sytuacji z Rysunku 15.
Tym razem konstrukcja przebiega następująco:
1.
2.
,
3.
4.
, a zatem po unormowaniu
5.
.
Wektor , choć nietrywialny, jest jednak jednostkowy, co łatwo sprawdzić,
obliczając jego normę:
(81)
Sprawdźmy także, czy wektory:
, i
są do siebie prostopadłe. Najłatwiej zrobić to, obliczając ich iloczyny skalarne:
(82)
Łatwo się przekonać, że macierz, którą możemy dzięki tym wektorom oraz
wektorowi skonstruować, a więc
(83)
jest ortonormalna.
Złożenie macierzy widoku,
świata i rzutowania
Sprawdźmy teraz, jak trójkąt o wierzchołkach A = (–1, –1, 0, 1), B = (1, –1, 0,
1) i C = (0, 1, 0, 1) z wcześniejszego przykładu będzie widoczny na ekranie,
jeżeli macierzą widoku będzie macierz odpowiadająca kamerze ustawionej
w punkcie E = (–1,1,1) skierowana na punkt O = (0,0,0) z wektorem polary-
zacji równym i zastosujemy rzutowanie perspektywiczne
z parametrami ekranu takimi samymi, jak we wcześniejszym przykładzie. Ma-
cierz świata pozostanie macierzą jednostkową. Wiemy już, że macierz widoku
w tym przypadku równa jest:
20 / 5 . 2014 . (24) /
programowanie grafiki
(84)
a macierz rzutowania to:
(85)
Zatem iloczyn macierzy świata, widoku i rzutowania równy jest:
(86)
Zastosujmy tę macierz na współrzędnych punktów A, B i C (w układzie
własnym modelu), aby uzyskać współrzędne tych punktów w układzie
przycinania:
(87)
Następnie wykonajmy dzielenie perspektywiczne:
(88)
I wreszcie przechodzimy do współrzędnych viewportu (wzór (32)), zaokrągla-
jąc współrzędne x i y tak, żeby wskazywały konkretny piksel na ekranie (por.
Rysunek 16):
(89)
Rysunek 16. Na rysunku widoczne są współrzędne wierzchołków trójkąta w
układzie współrzędnych viewportu
Bardzo dziękuje Karolinie Matulewskiej za pomoc w redagowaniu tekstu i upo-
rządkowaniu wzorów.
Jacek Matulewski
Fizyk zajmujący się na co dzień optyką kwantową i układami nieuporządkowanymi na Wydziale
Fizyki, Astronomii i Informatyki Stosowanej UMK w Toruniu. Od 1998 r. interesuje się programo-
waniem dla systemu Windows, w szczególności platformą .NET i językiem C#. Autor serii książek
poświęconych programowaniu. Większość ukazała się nakładem wydawnictwa Helion. Wierny
użytkownik kupionego w połowie lat osiemdziesiątych "komputera osobistego" ZX Spectrum 48k.
22 / 5 . 2014 . (24) /
programowanie gier
Rafał Kocisz
Tych czytelników, którzy po raz pierwszy mają do czynienia z cyklem Wzor-
ce Programowania Gier, odsyłam do numeru 11/2013, w którym pojawił się
pierwszy artykuł z niniejszej serii (opisujący wzorzec: Szkielet Aplikacji);
czytając wstęp do tego artykułu, poznacie ideę cyklu oraz strukturę po-
szczególnych jego części.
Pozostałych czytelników zapraszam na wspólną podróż do pełnej przy-
gód krainy programowania gier.
Wzorzec Zarządca Zasobów:
przeznaczenie
Każda bez wyjątku gra skonstruowana jest z pewnej liczby zasobów (ang.
resources), zwanych też czasami aktywami (ang. assets) bądź mediami
(ang. media). Przykładami takich zasobów są:
»» tekstury,
»» atlasy tekstur,
»» materiały,
»» animacje,
»» programy cieniujące (ang. shaders),
»» klipy audio i video,
»» pliki z definicjami struktury poziomów.
Do tej listy można by dodać jeszcze wiele, wiele punktów. Tak czy inaczej,
w praktyce każda gra musi w taki czy inny sposób zarządzać swoimi zasoba-
mi, chociażby w najbardziej podstawowym zakresie, jakim jest:
»» wczytywanie zasobów do pamięci (ang. resource loading),
»» usuwanie zasobów z pamięci (ang. resource unloading),
»» zapewnienie łatwego dostępu do zasobów dla komponentów gry (ang.
resource accessibility).
Przeznaczeniem wzorca Zarządca Zawartości jest opakowanie funkcjonalno-
ści związanej z zarządzaniem zasobami i dostarczenie jednolitego, eleganc-
kiego interfejsu do obsługi tej funkcjonalności.
Warto w tym miejscu zaznaczyć również, iż zarządzanie zasobami gry od-
bywa się zazwyczaj w dwóch etapach:
»» przetwarzanie wstępne (ang. off-line processing), realizowane najczęściej
przez dedykowane narzędzia działające na etapie budowania gry; przy-
kładem takiego przetwarzania może być transformacja pojedynczych
tekstur w atlasy tekstur czy pakowanie całego drzewa zasobów w jeden
plik-archiwum,
»» przetwarzanie w czasie wykonania (ang. run-time processing), na co skła-
dają się takie działania jak wstępne wczytywanie zasobów (ang. pre-lo-
ading) czy usuwanie z pamięci zasobów, które nie są już potrzebne; zna-
mienne w tym przypadku jest to, iż wszystkie te działania odbywają się
w czasie działania gry.
Kompleksowy proces zarządzania zasobami (zarówno przed, jak i w czasie
wykonania gry) oraz wspomagające go narzędzia nazywamy potokiem prze-
twarzania zawartości (ang. content pipeline). Warto w tym miejscu podkreślić,
iż komponent implementujący prezentowany w niniejszym artykule wzorzec
Zarządca Zawartości związany jest z drugim z wymienionych wyżej etapów
i wspiera obsługę zasobów gry w czasie jej wykonania.
Inne nazwy
Wzorzec Zarządca Zawartości (ang. Content Manager) występuje pod wielo-
ma innymi nazwami; niektóre z nich to:
»» Zarządca Zasobów (ang. Resource Manager),
»» Słownik Zasobów (ang. Asset Dictionary),
»» Zarządca Mediów (ang. Media Manager).
Uzasadnienie stosowania
Zarządzanie zasobami gry wiąże się z zestawem konkretnych działań i odpo-
wiedzialności, które można podsumować następująco:
»» Zarządca Zawartości powinien zagwarantować, że w danym momencie
w pamięci istnieje tylko jedna kopia unikalnego zasobu.
»» Zarządca Zawartości powinien zarządzać czasem życia każdego zasobu:
wczytywać go do pamięci, kiedy zachodzi taka potrzeba, oraz usuwać go
z pamięci, kiedy staje się bezużyteczny.
»» Zarządca Zawartości powinien obsługiwać wczytywanie zasobów złożo-
nych (ang. composite assets); zasób złożony, w przeciwieństwie do zasobu
atomowego (ang. atomic asset), jest zasobem, który składa się z innych za-
sobów. Przykładem tego rodzaju zasobu może być animacja, która oprócz
informacji dotyczących struktury klatek i czasów ich wyświetlania odnosi
się również do konkretnych tekstur, będących zasobami atomowymi.
»» Zarządca Zawartości utrzymuje spójność referencji pomiędzy zasobami
(ang. referential integrity); jest to szczególnie istotne przy przetwarzaniu
zasobów złożonych, gdzie wiele aktywów może odnosić się do siebie (tak
jak w przypadku opisanego wyżej przykładu z animacją). W tym układzie
zarządca powinien dbać o to, aby wszystkie aktywne zasoby zawsze znaj-
dowały się w pamięci oraz, w miarę potrzeby, usuwać je, kiedy nie są już
potrzebne.
»» W wybranych przypadkach Zarządca Zawartości może odpowiadać za
zarządzanie pamięcią przydzieloną na obsługę zasobów; np. zapewniać,
iż zasoby w czasie wczytywania będą umieszczane we właściwych obsza-
rach pamięci.
»» Zarządca Zawartości może udostępniać mechanizm do wykonywania
dodatkowego przetwarzania zasobów tuż po ich wczytaniu do pamięci
(ang. load-initializing).
»» ZarządcaZawartościpowinienudostępniaćujednoliconyinterfejsdoobsługi
dowolnegorodzajuzasobów;powinienrównieżpozwalaćużytkownikombi-
Wzorce Programowania Gier:
Zarządca Zawartości
Witam w kolejnej, drugiej odsłonie cyklu Wzorce Programowania Gier. Napisanie
tego artykułu zajęło mi z różnych przyczyn o wiele więcej czasu niż się spodzie-
wałem, więc w tym miejscu chciałbym przeprosić wszystkich zainteresowanych
czytelników za zwłokę i podziękować im jednocześnie za cierpliwość. Dziś na
warsztat weźmiemy jedno z fundamentalnych zagadnień w dziedzinie programo-
wania gier, jakim jest zarządzanie zasobami (ang. resource management).
24 / 5 . 2014 . (24) /
programowanie gier
blioteki definiować własne typy zasobów i być w stanie je obsłużyć.
»» Jeśli zachodzi taka potrzeba, Zarządca Zawartości może obsługiwać asyn-
chroniczne wczytywanie zasobów, (ang. resource streaming).
Biorąc pod uwagę tak szeroki zakres odpowiedzialności, uzasadnioną wydaje
się potrzeba jej kapsułkowania w ramach wyspecjalizowanego komponen-
tu wielokrotnego użytku. W związku z tym Zarządca Zasobów jest idealnym
kandydatem na składnik biblioteki bądź silnika wspomagającego programo-
wanie gier.
Patrząc na te kilkadziesiąt projektów związanych z produkcją gier,
w których miałem okazję brać udział, te, w których zarządzanie zasobami od-
bywało się na zasadzie ad hoc, były zazwyczaj marszami ku klęsce. Solidny
komponent do zarządzania zawartością gry oraz uniwersalny mechanizm do
zarządzania stanami gry (o którym napiszę w jednym z kolejnych odcinków
niniejszej serii) to w mojej opinii baza, bez której ciężko budować jakiekol-
wiek gry, nawet proste, nie mówiąc już o tych bardziej złożonych…
Stosowalność
Zarządzanie zawartością jest tak fundamentalnym zagadnieniem w dzie-
dzinie wytwarzania gier, że odpowiedź na pytanie dotyczące stosowalności
wzorca Zarządca Zawartości wydaje się być oczywista: w zasadzie powinno
się stosować go zawsze.
Wyjątkiem mogą być tu naprawdę proste gry, wykorzystujące co najwyżej
kilka rodzajów zasobów atomowych (np. tekstury i klipy audio). Jeśli jednak
wTwojej grze pojawiają się zasoby złożone, to nie ma co się zastanawiać: trud
włożony w implementację Zarządcy Zawartości opłaci Ci się z nawiązką!
Struktura
Struktura wzorca Zarządca Zawartości przedstawiona jest na Rysunku 1.
Rysunek 1. Struktura wzorca Zarządca Zawartości
Sam Zarządca Zawartości jest scentralizowanym komponentem o dostępie
globalnym, zaimplementowany zazwyczaj jako singleton. Zarządca oferuje
dwie podstawowe operacje:
»» Acquire(), czyli pozyskanie zasobu; przy pierwszej próbie pozyskania
operacja ta jest tożsama z wczytaniem zasobu do pamięci.
»» Release(), czyli uwolnienie zasobu. Jeśli żaden inny komponent nie
przechowuje referencji do tego zasobu, operacja ta jest w większości
przypadków tożsama z usunięciem zasobu z pamięci.
Poza tym, w skład wzorca wchodzi często hierarchia klas reprezentujących za-
soby, z abstrakcyjną klasą Asset oraz szeregiem pod-klas, w ramach których
zaimplementowane są konkretne rodzaje aktywów.
Uważni czytelnicy zwrócili zapewne uwagę na powtarzający się termin:
referencja. Otóż kluczowym elementem procesu zarządzania zasobami (a co
za tym idzie: również istotnym składnikiem naszego wzorca) jest mechanizm
zliczania referencji. Problem w tym, iż implementacja tego mechanizmu jest
bardzo silnie zależna od języka, w którym jest on realizowany. Np. w przy-
padku języka C++ do zliczania referencji można wykorzystać mechanizm
inteligentnych wskaźników (jak to zrobić, przekonasz się, czytając podpunkt
Implementacja zawarty w dalszej części niniejszego artykułu).
Proszę zwrócić uwagę, że na diagramie klas obrazującym strukturę wzor-
ca metoda AssetManager::Acquire() zwraca obiekt typu AssetRef.
Typ ten reprezentuje rodzaj uchwytu do zasobu, który odpowiada za zliczanie
referencji jego użycia.
Postaram się ten niełatwy temat wyjaśnić bardziej jasno i szczegółowo
w sekcji opisującej implementację prezentowanego wzorca.
Uczestnicy
»» AssetManager: globalny obiekt reprezentujący scentralizowany mecha-
nizm zarządzania zasobami.
»» Asset: abstrakcyjny interfejs reprezentujący zasób.
»» AssetRef: uchwyt do zasobu, odpowiedzialny za zliczanie referencji jego
użycia.
»» Texture, AudioClip itd.: przykłady klas reprezentujących konkretne
typy zasobów.
Współpraca
»» AssetManager: udostępnia zasoby w postaci uchwytów.
»» AssetManager: w razie potrzeby wczytuje zasoby do pamięci.
»» AssetManager: odpowiada za przetworzenie zasobów tuż po ich wczy-
taniu do pamięci (ang. load-initializing).
»» AssetRef: odpowiada za zliczanie referencji do zasobów (ang. reference
counting)
»» AssetRef: usuwa zasób z pamięci, jeśli nikt go nie używa.
Konsekwencje
Stosowanie wzorca Zarządca Zawartości znacznie upraszcza proces zarządza-
nia zasobami oraz czyni go bardziej stabilnym. Elastyczność wzorca ułatwia
dodawanie nowych rodzajów zasobów do gry. Mechanizm zliczania referen-
cji pozwala łatwo obsługiwać zasoby złożone, co w przypadku podejścia ad
hoc jest wysoce uciążliwe.
Generalnie stosowanie spójnego mechanizmu zarządzania zawartością
jest w większości przypadków nieodzownym fundamentem, bez którego
bardzo ciężko zbudować stabilną funkcjonalność gry.
Intermezzo: co w Gem'ie piszczy?
Zanim przejdziemy do sekcji Implementacja, kilka słów na temat zmian w bi-
bliotece Gem. Silnik ten, pomimo tego, iż w porównaniu do produkcyjnych
rozwiązań nadal pozostaje bardzo skromnym rozwiązaniem, od czasu kiedy
pisałem poprzedni artykuł z serii Wzorce Programowania Gier przeszedł dość
istotną metamorfozę. Poniżej przedstawiam podsumowanie najważniejszych
zmian:
»» Gem posiada teraz swój własny, niezależny od biblioteki SDL interfejs do
renderowania sprzętowo wspomaganej grafiki 2D (Gem::Graphics).
Interfejs ten pozwala póki co na rysowanie dwuwymiarowych obrazów
z podstawowymi transformacjami (rotacja, skalowanie, podkolorowanie,
odbicia) w czterech podstawowych trybach mieszania kolorów. Na Listin-
gu 1 przedstawiona jest deklaracja jednej z metod klasy Gem::Graphics,
odpowiedzialna za rysowanie tekstury.
»» W związku z dodaniem do Gem interfejsu do renderowania tekstur,
w bibliotece pojawił się szereg pomocniczych komponentów takich jak
Rectangle, Vector2 czy Color.
»» W czasie kiedy pisałem pierwszą część artykułu, środowisko Visual Studio
C++ 2013 miało problem z kompilacją biblioteki Boost C++.W międzycza-
sie pojawiła się nowa odsłona tej biblioteki (Boost C++ 1.55.0), w której
25/ www.programistamag.pl /
Wzorce Programowania Gier: Zarządca Zawartości
problem ten został rozwiązany. W związku z powyższym zdecydowałem
się wykorzystać Boost'a przy implementacji biblioteki Gem. Najbardziej
widocznymi zmianami w tym zakresie jest użycie klasy boost::variant
do przekazywania obiektów reprezentujących zdarzenia generowanie
przez system oraz pochodzące z kontrolerów (patrz nagłówki: Event.hpp
oraz Input.hpp), co pozwoliło wyeliminować niebezpieczny mechanizm
przekazywania argumentów w metodach zwrotnych obsługujących te
zdarzenia jako typ void*.
»» Do Gem dodany został system do obsługi testów jednostkowych (oparty
na bibliotece Google Mock; https://code.google.com/p/googlemock/);
póki co testów nie ma zbyt wiele, ale liczę, że w niedalekiej przyszłości
to się zmieni.
»» I wreszcie: Gem wzbogacił się o mechanizm zarządzania zasobami. Mecha-
nizm ten opiszę szczegółowo w kolejnym podpunkcie niniejszego tekstu.
Listing 1. Deklaracja metody Gem::Graphics::DrawTexture()
virtual void DrawTexture(const Texture& texture,
const Rectangle& destinationRectangle,
const Rectangle* const sourceRectangle,
const Color& color,
float rotation,
const Vector2& origin,
DrawEffects drawEffects=DrawEffects::None,
BlendMode blendMode=BlendMode::AlphaBlend) = 0;
Przypominam również, że źródła biblioteki Gem udostępnione są na licencji
MIT w serwisie BitBucket pod adresem https://bitbucket.org/rkocisz/gem/.
Implementacja
Zarządca Zasobów to jeden z tych wzorców projektowych, które rozważa-
ne w ujęciu abstrakcyjnym wydają się być relatywnie proste, jednakże kie-
dy rozważamy ich implementację, okazuje się, że przysłowiowy diabeł tkwi
w szczegółach.
W tym miejscu pokuszę się o krótką dygresję na temat implementacji
mechanizmu zarządzania zasobami w języku C++. Implementacja ta nie jest
specjalnie rozległa (w sensie ilości kodu źródłowego), jednakże łączy w sobie
szereg niebanalnych właściwości tego języka, i prawdę mówiąc – do najprost-
szych nie należy… Dlatego też proszę czytelników z mniejszym doświadcze-
niem w C++ o cierpliwość i wyrozumiałość, a także o sporą dozę uwagi.
Na początek zbadajmy definicję abstrakcyjnej klasy Gem::Asset, która
zgodnie z założeniami przedstawionymi we wcześniejszej części artykułu bę-
dzie stanowić bazę dla wszystkich komponentów reprezentujących zasoby
obsługiwane przez naszego zarządcę. Listing 2 przedstawia plik nagłówkowy
z definicją klasy, zaś Listing 3 zawiera implementację jej metod.
Listing 2. Zawartość pliku Asset.hpp
#ifndef GEM_ASSET_HPP
#define GEM_ASSET_HPP
#include #include namespace Gem
{
class Asset : private boost::noncopyable
{
public:
virtual ~Asset() = 0;
public:
virtual void Initialize();
};
typedef std::shared_ptr AssetPtr;
typedef std::weak_ptr AssetWPtr;
}
#endif
Listing 3. Zawartość pliku Asset.cpp
#include "gem/Asset.hpp"
namespace Gem
{
Asset::~Asset()
{
}
void Asset::Initialize()
{
}
}
Warto zwrócić uwagę, że Gem::Asset dziedziczy prywatnie po
boost::noncopyable, co oznacza, iż obiekty tej klasy nie podlegają stan-
dardowej semantyce kopiowania. Obiekty te będą tworzone zawsze na ster-
cie, a czas ich życia będzie kontrolowany przez standardowy, inteligentny
wskaźnik typu std::shared_ptr. Dlatego też w pliku nagłówkowym znaj-
dują się następujące definicje typów:
typedef std::shared_ptr AssetPtr;
typedef std::weak_ptr AssetWPtr;
Wirtualna metoda Gem::Asset::Initialize() służy do realizacji do-
datkowego przetwarzania zasobów tuż po ich wczytaniu do pamięci (ang.
load-initializing).
Przeanalizujmy, jak w Gem zaimplementowany jest najbardziej podsta-
wowy, atomowy zasób: tekstura. Ponieważ tekstura jest bardzo niskopozio-
mowym typem zasobu, stanowiącym jeden z punktów styku silnika Gem oraz
biblioteki SDL, postanowiłem w pełni odseparować jej interfejs od implemen-
tacji (to na wypadek, gdybym w przyszłości zechciał podmienić SDL'a na ja-
kieś inne rozwiązanie). W tym układzie interfejs tekstury reprezentowany jest
przez abstrakcyjną klasę Gem::Texture. Plik nagłówkowy tej klasy przedsta-
wiony jest na Listingu 4.
Listing 4. Zawartość pliku Texture.hpp
#ifndef GEM_TEXTURE_HPP
#define GEM_TEXTURE_HPP
#include "gem/Asset.hpp"
#include #include namespace Gem
{
class Texture : public Asset
{
public:
static AssetPtr Load(const std::string& path,
bool cache=false);
public:
virtual int Width() const = 0;
virtual int Height() const = 0;
};
typedef std::shared_ptrTexturePtr;
typedef std::shared_ptrConstTexturePtr;
}
#endif
Interfejs ten póki co jest bardzo prosty i pozwala odpytać się jedynie o sze-
rokość i wysokość tekstury (ten stan rzeczy zmieni się prawdopodobnie
w niedalekiej przyszłości). Kluczowa jest w tym przypadku statyczna meto-
da Gem::Texture::Load(), która dla zadanej ścieżki do pliku z zasobem
zwraca obiekt typu Gem::AssetPtr reprezentujący teksturę. Implementa-
cja tej funkcji (patrz: Listing 5) deleguje zadanie wczytania tekstury do funkcji
Gem::SdlTexture::Load().
5/2014 (24) www•programistamag•pl Cena 22.90 zł (w tym VAT 8%) Index: 285358 Robot Framework • Obsługa wyświetlaczy na Raspberry Pi • Wzorce silników zdarzeń w C++ Python i profilowanie Agile a komunikacjaZarządca zawartości Jak profilować i optymalizować aplikacje w Pythonie Psychologiczny aspekt komunikacji z zespołem Fundamentalny wzorzec projektowy w silnikach gier Co programista grafiki 3D powinien wiedzieć o macierzach
DOMENY | E-MAIL | HOSTING | STRONY WWW | SERWERY *Ceny nie zawierają VAT (23%). Umowa zawierana na 12 miesięcy płatne z góry. Podana miesięczna cena promocyjna obowiązuje przez cały okres umowy. Niniejszy materiał promocyjny nie stanowi oferty w rozumieniu kodeksu cywilnego. Ogólne warunki handlowe i regulamin promocji na www.1and1.pl. 1and1.pl 22 116 27 77 MIESIĄC 30 DNI NA PRÓBĘ TELEFON PORADA SPECJALISTY PEWNOŚĆ DZIĘKI GEO- REDUNDANCJI Wszystko w komplecie ■ darmowa domena .pl ■ nielimitowana powierzchnia, transfer, konta e-mail i bazy danych MySQL ■ system Linux lub Windows Centrum aplikacji ■ ponad 140 popularnych aplikacji (Drupal™, WordPress , Joomla!™, TYPO3, Magento®...) Nowość! Teraz także w wersji próbnej ■ wsparcie eksperta od aplikacji Potężne narzędzia ■ NetObjects Fusion® 2013 1&1 Edition ■ 1&1 Kreator Stron Mobilnych ■ Linux: PHP 5.5, Perl, Python, Ruby ■ Windows: ASP.NET 4.5, ASP MVC 4, dedykowane pule aplikacji Skuteczny marketing ■ 1&1 SEO Ekspert ■ 1&1 SiteAnalytics Nowoczesna technologia ■ Maksymalna dostępność dzięki georedundancji ■ Ponad 300 Gbit/s przepustowości ■ Gwarantowana wydajność nawet 2 GB RAM ■ 1&1 CDN powered by CloudFlare® ■ Skaner bezpieczeństwa 1&1 SiteLock WIODĄCE APLIKACJE JESZCZE LEPSZE! MOCNE PAKIETY DLA ZAWODOWCÓW zł/mies.* 4,90już od NOWY HOSTING 4,już od
spis treści / edytorial PROGRAMOWANIE GRAFIKI Macierze w grafice 3D.................................................................................................................. Jacek Matulewski 4 PROGRAMOWANIE GIER Wzorce Programowania Gier: Zarządca Zawartości................................................................ Rafał Kocisz 22 PROGRAMOWANIE SYSTEMÓW OSADZONYCH Zwizualizuj to sam. Obsługa wyświetlaczy na Raspberry Pi................................................... Karol Poczęsny 32 PROGRAMOWANIE SYSTEMOWE Jak napisać własny debugger w systemie Windows – część 4.............................................. Mateusz "j00ru" Jurczyk 38 TESTOWANIE I ZARZĄDZANIE JAKOŚCIĄ Wprowadzenie do testowania w Robot Framework (Robot)................................................ WojciechTański 46 Profilowanie aplikacji w języku Python...................................................................................... AdamWołk 50 LABORATORIUM BOTTEGA Wzorce silników zdarzeń w C++. Część I: Wzorzec Reaktor i podstawowa implementacja Roman Ulan 60 Brakujący element Agile. Część 4: Emocje w komunikacji...................................................... Paweł Badeński 64 PLANETA IT Zakodowana pasja......................................................................................................................... Łukasz Sobótka 70 STREFA CTF Zdobyć flagę… ASIS CTF Quals 2014 – Random Image.......................................................... Gynvael Coldwind 72 KLUB LIDERA IT Jak całkowicie odmienić sposób programowania, używając refaktoryzacji (część 9)....... Mariusz Sieraczkiewicz 76 KLUB DOBREJ KSIĄŻKI TDD. Sztuka tworzenia dobrego kodu....................................................................................... Rafał Kocisz 78 Odrobina matematyki Zacznijmy od tezy: Programowanie to matematyka. To zdanie często wywołuje sprzeciw ze strony programistów nastawionych na aplikacje biznesowe – i słusznie. Wielu utalentowanych matematyków na przestrzeni wielu, wielu lat zrobiło wystarczająco dużo, aby statystyczny programista aplikacji biznesowych mógł zająć się tym, co jest dla niego istotne. Nie wolno jednak zapomnieć o tym, że termin „programista” ma również dziesiątki innych znaczeń. Bardzo gorący (bo majowy) numer Programisty, za sprawą Jacka Matulewskiego (specjalne podziękowania dla autora za wło- żoną pracę nad tym wydaniem!) nasycony został bardzo dużą ilością macierzy, rzutów i funkcji trygonometrycznych. Nie jest to jednak zwykły, czysto matematyczny wywód oderwany od rzeczywistości. „Macierze w grafice 3D” to bogaty wykład przemyślany pod kątem jak największej przydatności dla wszystkich, którzy interesują się aspektami zwią- zanymi z grafiką 3D. Czy programowanie to matematyka? Zapraszamy do ukształtowania własnego poglądu, zaraz po zapoznaniu się z najnowszym wydaniem magazynu, który możemy dostarczyć doTwoich rąk (lub naTwój monitor). Magazyn Programista istnieje również dzięki Tobie, drogi Czytelniku. Cały czas jeste- śmy ciekawiWaszych opinii: http://fb.me/ProgramistaMagazyn Z poważaniem, Redakcja Wydawca/ Redaktor naczelny: Anna Adamczyk annaadamczyk@programistamag.pl Redaktor prowadzący: Łukasz Łopuszański lukaszlopuszanski@programistamag.pl Korekta: Tomasz Łopuszański Kierownik produkcji: Krzysztof Kopciowski bok@keylight.com.pl DTP: Krzysztof Kopciowski Dział reklamy: reklama@programistamag.pl tel. +48 663 220 102 tel. +48 604 312 716 Prenumerata: prenumerata@programistamag.pl Współpraca: Michał Bartyzel Mariusz Sieraczkiewicz Michał Leszczyński Marek Sawerwain Łukasz Mazur Rafał Kułaga Sławomir Sobótka Michał Mac Gynvael Coldwind Bartosz Chrabski Adres wydawcy: Dereniowa 4/47 02-776 Warszawa Druk: Drukarnia Kontakt ul. Gospodarcza 5a 05-092 Łomianki Nakład: 5000 egz. Redakcja zastrzega sobie prawo do skrótów i opracowań tekstów oraz do zmiany planów wydawniczych, tj. zmian w zapowiadanych tematach artykułów i terminach publikacji, a także nakładzie i objętości czasopisma. O ile nie zaznaczono inaczej, wszelkie prawa do materiałów i znaków towarowych/firmowych zamieszczanych na łamach magazynu Programista są zastrzeżone. Kopiowanie i rozpowszechnianie ich bez zezwolenia jest Zabronione. Redakcja magazynu Programista nie ponosi odpowiedzialności za szkody bezpośrednie i pośrednie, jak również za inne straty i wydatki poniesione w związku z wykorzystaniem informacji prezentowanych na łamach magazynu Programista. Magazyn Programista wydawany jest przez Dom Wydawniczy Anna Adamczyk Zamów prenumeratę magazynu Programista przez formularz na stronie: http://programistamag.pl/typy-prenumeraty/ lub zrealizuj ją na podstawie faktury Pro- forma. W spawie faktur Pro-Forma prosimy kontktować się z nami drogą mailową: redakcja@programistamag.pl. Prenumerata realizowana jest także przez RUCH S.A. Zamówienia można składać bezpośrednio na stronie: www.prenumerata.ruch.com.pl Pytania prosimy kierować na adres e-mail: prenumerata@ruch.com.pl lub kontaktując się telefonicznie z numerem: 801 800 803 lub 22 717 59 59, godz.: 7:00 – 18:00 (koszt połączenia wg taryfy operatora).
4 / 5 . 2014 . (24) / programowanie grafiki Jacek Matulewski Poniższy artykuł jest rozdziałem książki pt. Podstawy grafiki 3D. Nowocze- sny OpenGL przygotowywanej dla Wydawnictwa Naukowego PWN. Książ- ka ukaże się jesienią tego roku. Werteksy przesyłane do karty graficznej wyrażone powinny być w czterowy- miarowym układzie współrzędnych modelu. Macierze świata i widoku, ewen- tualnie jedna macierz model-widok, transformują współrzędne werteksu z lokalnego układu obiektu do układu współrzędnych kamery – to jest ten układ, który zazwyczaj przedstawiany jest na rysunkach wprowadzających do OpenGL; z osią OZ skierowaną do widza, osią OX skierowaną w prawo i osią OY – do góry. Układ modelu może mieć względem niego dowolne położenie i orientację - macierze świata i widoku mogą być bowiem iloczynem macierzy opisujących obroty, translacje, skalowania, odbicia czy pochylenia. Macierze opisujące te wszystkie przekształcenia omówione zostaną niżej w tym arty- kule. Za dalsze przekształcenia, tj. transformację z układu odniesienia kamery do układu przycinania, odpowiada macierz rzutowania, od wyprowadzenia której zaczniemy. Wyprowadzenie nie jest matematycznie bardzo trudne, ale dość zawiłe. Warto zatem czytać artykuł z ołówkiem w ręku. Macierze rzutowania Macierze w grafice 3D Nowoczesny OpenGL uruchamiany w profilu rdzennym zmusza programistę do rzeczy, o których mógł wcześniej nie myśleć: pisania shaderów, tworzenia buforów werteksów i definiowania macierzy. Oczywiście, że nadal można korzystać z bibliotek czy gotowych rozwiązań pobranych z Internetu. Można to jednak wykorzystać jako impuls do nauki GLSL lub rachunku macierzy. Poniższy artykuł ma być pomocą w re- alizacji drugiego z tych impulsów. Nie rozpocznę jednak od wyjaśniania, czym są ma- cierze – skupię się na zagadnieniach, których często brak w typowych podręcznikach algebry, a charakterystycznych dla grafiki 3D. Artykuł wymaga od czytelnika pewnej wiedzy o grafice 3D, choćby obycia ze współrzędnymi jednorodnymi. Rysunek 1. Bryły widzenia w układzie kamery w przypadku perspektywy (frustum) i rzutu izometrycznego oraz po transformacji do układu NDC (sześcian w prawym górnym rogu) Macierzrzutowaniaodpowiedzialnajestzatransformacjęzukładuwspółrzęd- nych kamery (lub jak kto woli – oka) do układu przycinania (ang. clip coordi- nate system). To nie jest układ docelowy. Uzupełnieniem tego przekształcenia jest bowiem tzw. dzielenie perspektywiczne, tj. podzielenie współrzędnych x, y i z przez współrzędną skalowania w. W efekcie następuje transformacja do trójwymiarowego układu unormowanych współrzędnych urządzenia (NDC), w którym bryła widzenia przyjmuje kształt sześcianu (Rysunek 1). Kształt sceny wyznacza charakter rzutowania. Dokładniej rzecz ujmując, skoro sce- na w układzie NDC ma kształt sześcianu, to odwrócenie podziału perspek- tywicznego i użycie macierzy odwrotnej do macierzy rzutowania, powinno ten sześcian przekształcić w kształt oryginalnej sceny. W przypadku rzuto- wania izometrycznego, w którym przekształcenia wszystkich współrzędnych są liniowe, bryła widzenia w układzie kamery będzie prostopadłościanem. W przypadku rzutu perspektywicznego – będzie miała kształt frustum. Zgodnie z konwencją OpenGL przyjmijmy, że ekran ułożony jest w taki sposób, że w układzie odniesienia kamery znajduje się w płaszczyźnie pro- stopadłej do osi OZ (osi optycznej wirtualnej kamery), a jego lewa i prawa krawędź znajdują się w x = l i x = r, natomiast dolna i górna w y = b i y = t (zwykle l i b są ujemne, a r i t – dodatnie, ale to nie jest konieczne). Ekran usta- wiony jest o n od kamery, a tylna płaszczyzna ograniczająca scenę o f. Ponie- waż oś OZ skierowana jest w kierunku do widza z zerem w położeniu kamery (Rysunek 1), ekran znajduje się w z=–n, a tylna płaszczyzna odcinania w z=–f.
6 / 5 . 2014 . (24) / programowanie grafiki Wartości współrzędnej z przyjmują wartości ujemne (n i f są dodatnie).Te war- tości, a więc kolejno l, r, b, t, n i f są argumentami funkcji glFrustum i glOrtho tradycyjnego OpenGL (nieskuteczne w trybie rdzennym). Jak wspomniałem, w efekcie rzutowania i następującego po nim dzielenia przez współrzędną w uzyskujemy wartości współrzędnych w układzie NDC, w którym scena staje się sześcianem o boku 2. W tym układzie współrzędne x, y i z przyjmują war- tości od -1 do 1, przy czym poszczególne współrzędne są transformowane w taki sposób, aby zachodziły relacje: (1) Zwróćmy uwagę, że w przypadku współrzędnej z zmianie ulega kierunek osi OZ (pamiętajmy, że f > n). W układach przycinania i NDC wartość współrzęd- nej z oddaje bowiem głębię, czyli odległość werteksu od kamery, i rośnie, gdy przesuwamy się od ekranu do tylnej„ściany”frustum. Przekształcenia wszyst- kich współrzędnych z układu kamery do układu przycinania są liniowe, a więc zadane przez: (2) Uprzedzając fakty, zdradzę jednak, że o ile przekształcenie współrzęd- nych x i y do układu NDC także jest liniowe, to nie musi tak być w przypad- ku współrzędnej z, a mówiąc konkretniej: nie jest tak w przypadku rzutu perspektywicznego. We wzorze (2) stosuję oznaczenia, w których indeks c oznacza współ- rzędną w układzie przycinania (ang. clipping), a e – współrzędną w układzie kamery (od ang. eye, czyli oko). To typowa konwencja grafiki 3D, którą posta- nowiłem tutaj zachować, pomimo że od„oka”wolę używać terminu„kamera”. Macierz rzutowania izometrycznego Rysunek 2. Rzut z kierunku +y na płaszczyznę OXZ Wyprowadźmy najpierw wzór macierzy rzutowania izometrycznego. Po pierw- sze dlatego, że jest prostsza – wszystkie przekształcenia są w niej liniowe, a po wtóre – wyprowadzenie jej można traktować jako wstęp do wyprowadzenia macierzy rzutowania perspektywicznego. Skupmy się na początek na współrzędnej x. Wiemy, że dla wartości xe = l powinniśmy uzyskać xc = -1, natomiast dla xe = r otrzymamy xc = 1. Mamy za- tem dwa warunki, które pozwolą nam znaleźć wartości stałych ax i bx: (3) Z drugiego równania znajdujemy wartość stałej którą wsta- wiamy do pierwszego, co pozwoli nam ustalić, że: (4) Jeżeli r > l, to stała ax jest dodatnia. Jest ona odpowiedzialna za przeskalowanie układów odniesienia – równa się wobec tego stosunkowi szerokości płaszczy- zny odcinającej w bliży (ekranu) w układach przycinania (równa 2) i kamery (równa r – l). Druga stała opisuje różnicę położenia początku układu współrzęd- nych w obu układach – jeżeli r i l mają wartości symetryczne względem zera tj. l = –r, to bx równa jest zeru. W efekcie wzór transformacji przyjmuje postać: (5) Sprawdźmy, czy wszystko jest w porządku, podstawiając za xe wartości l, (l+r)/2 i r. Uzyskamy kolejno: –1, 0 i 1. Analogiczne rozumowanie prowadzi do znalezienia wzoru na transformację współrzędnej y: (6) i współrzędnej z: (7) W tym trzecim przypadku minus przy pierwszym wyrazie związany jest ze zmianą kierunku osi OZ. W przypadku transformacji rzutowania izometrycz- nego, dzielenie perspektywiczne nie powinno wprowadzać żadnych zmian. Dlatego współrzędna wc powinna mieć wartość równą jedności. Z tego wyni- ka, że transformacja do współrzędnych przycinania, a potem do współrzęd- nych NDC polega jedynie na ich przeskalowaniu. We wzorach (5) – (8) kryje się jednak pewien problem: jest nim wolny wy- raz niezależący od żadnej współrzędnej układu kamery. Taki wyraz uniemoż- liwia zapis przekształcenia za pomocą macierzy! Sprawę ratuje jednak „nad- miarowa”współrzędna w. Jeżeli założymy, że w układzie kamery współrzędna we musi mieć wartość równą 1, to wówczas przekształcenie do współrzęd- nych przycinania i NDC (w przypadku rzutu izometrycznego te dwa układy są identyczne) można zapisać: (8) co z kolei można wyrazić w zwarty sposób za pomocą macierzy: (9)
7/ www.programistamag.pl / Macierze w grafice 3D Jeszcze raz przypomnę, że formalnie rzecz biorąc macierz ta przeprowa- dza współrzędne w układzie kamery do współrzędnych przycinania. Jednak w tym przypadku dzielenie perspektywiczne nie zmienia ich wartości, bo wc = we = 1, zatem współrzędne NDC mają taką samą wartość, co współrzęd- ne przycinania. Jeżeli założymy, że scena jest symetryczna względem osi optycznej kame- ry, co jest częste w praktyce, czyli r = –l = w/2 (wielkość w = r – l to szerokość ekranu) oraz t = –b = h/2 (h = t – b to wysokość ekranu), to macierz rzutowania izometrycznego znacznie się uprości: (10) Zakładając dodatkowo, że w = 2 (l = –1, r = 1), h = 2 (t = 1, b = –1) i n = 1, uzy- skamy postać: (11) Macierz rzutowania perspektywicznego W rzutowaniu izometrycznym bryła widzenia ma kształt prostopadłościanu, zatem jej boczne krawędzie są do siebie równoległe i jednocześnie równoległe do osi OZ. W przypadku rzutu perspektywicznego jest inaczej – wszystkie te krawędzie leżą na prostych, które przecinają się w jednym punkcie nazywanym ogniskiemrzutowania.Topunkt,wktórymznajdujesiękamera–początekukła- du współrzędnych kamery. W rzutowaniu izometrycznym położenie kamery jest umowne. Z istnienia ogniska i ze zbiegania promieni widzenia do jednego punktu wynika, że obiekt mniejszy, ale znajdujący się bliżej, może przesłaniać obiekt większy, ale dalszy (por. Rysunek 3). Zastanówmy się wobec tego, jak w miarę oddalania od kamery zmienia się wielkość obiektów, które na ekranie mają ten sam rozmiar albo odwrotnie: jak rozmiar obrazu na ekranie w rzucie perspektywicznym zależy od jego odległości od ekranu i kamery.W rzutowaniu izometrycznym rozmiar obrazu zrzutowanego na ekran obiektu jest taki sam, jak samego werteksu – współrzędne dowolnego werteksu po zrzutowaniu, nazwijmy je xp i yp, mają te same wartości, co współrzędne xe i ye (Rysunek 2). Rysunek 3. Rzut frustum z kierunku +y. Zaznaczony werteks Pe nie musi znajdować się w płaszczyźnie OXZ zawierającej oś optyczną Spójrzmy na kamerę i frustum„z góry”, tj. z kierunku +y. Pokazuje to Rysu- nek 3 prezentujący płaszczyznę OXZ, który pozwoli nam sprawdzić, jak współ- rzędna xp obrazu werteksu na ekranie powinna zależeć od współrzędnej ze oryginalnego werteksu. Rozważmy w tym celu punkt Pe znajdujący się w ob- rębie sceny. Z podobieństwa trójkątów OPeRe i OPpRp wynika, że (12) Pamiętajmy, że ze i zp = –n są mniejsze od zera. Te trójkąty nie muszą leżeć w płaszczyźnie OXZ, zatem długości odcinków ORe i ORp wcale nie są równe ze i zp, ale ich stosunek równy jest stosunkowi tych współrzędnych. Identyczne proporcje można zapisać dla zmiennej y. Z tego wynika, że zrzutowany na ekran obiekt ma rozmiary: (13) Już teraz widać, że rozmiar ten maleje wraz ze zwiększaniem wartości –ze, czyli odległości od kamery. To właśnie jest perspektywiczne pomniejszenie dalszych obiektów. Transformacja do układu NDC powinna je uwzględniać. Właśnie dlatego w przypadku rzutowania perspektywicznego przekształce- niom skalowania (8) nie podlegają współrzędne xe i ye oryginalnego werteksu, a współrzędne xp i yp obrazu zrzutowanego na ekran. Połączmy zatem oba przekształcenia. W tym celu musimy złożyć wzory (5) – (7). Uzyskamy w ten sposób transformację współrzędnych werteksu z układu kamery bezpośrednio do współrzędnych NDC: (14) Zwróćmy uwagę, że o ile we wzorach (14) zawartość nawiasów może być zapi- sana za pomocą macierzy z elementami, które zależą jedynie od parametrów frustum (stałe l, r, b i t), to współczynnik przed nawiasem zależy od współrzędnej transformowanego werteksu. Takiego przekształcenia nie zapiszemy za pomocą macierzy! A to oznacza, wbrew temu, co się zwykle mówi, że sama macierz rzu- towania perspektywicznego wcale nie opisuje rzutowania perspektywicznego. Zasadniczą pracę w tym względzie musi wziąć na siebie dzielenie perspekty- wiczne, czyli redukcja współrzędnych jednorodnych w układzie przycinania do współrzędnych kartezjańskich NDC z jednoczesnym podzieleniem współrzęd- nych xc, yc i zc przez współrzędną wc. Jednak aby to zadziałało, musimy zadbać o to, aby we współrzędnej wc znalazła się wartość współrzędnej ze. To oznacza, że wzory na przekształcenie układu współrzędnych do układu przycinania, a więc bez dzielenia perspektywicznego, powinny wyglądać następująco: (15) Jak widać do współrzędnej wc przeniosłem także minus, który widoczny jest we wzorach (14). Nadal nie mamy jeszcze wzoru na transformację współrzęd- nej z. Wiemy, że wartość zc nie powinna zależeć od xe i ye, jest natomiast linio- wą funkcją ze (por. wzór 2):
8 / 5 . 2014 . (24) / programowanie grafiki (16) Musimy także pamiętać o tym, że przy przejściu do współrzędnych NDC wszystkie współrzędne, w tym także współrzędna zc, zostaną podzielone przez wc = –ze, co jest konieczne, aby wprowadzić perspektywę: (17) przy czym az musi być ujemne, bo oś OZ w układzie NDC zmienia kierunek. Przekształcenie współrzędnej ze do zNDC, w odróżnieniu od transformacji do układu przycinania (16), traci charakter liniowy (por. Rysunek 4). Rysunek 4.Współrzędna zNDC w funkcji współrzędnej ze z układu kamery do układu NDC dla n = 1 i f = 10 Ponadto wiemy, że dla ze =–n powinniśmy uzyskać zNDC =–1, a dla ze =–f powin- niśmy otrzymać zNDC = 1. Podstawiając te wartości do wzoru (17), otrzymujemy: (18) Proste przekształcenia pozwalają na znalezienie wartości az i bz: (19) Transformacja współrzędnej z z układu kamery do układu współrzędnych NDC przyjmuje wobec tego postać (20) Nie usuwam minusów z tego wzoru, ponieważ minus przed nawiasem jest już zafiksowany w dzieleniu perspektywicznym (ustaliliśmy wcześniej, że wc = –ze). Pojawia się tu ponownie problem związany z wolnym wyrazem wewnątrz nawiasu. I tym razem możemy go rozwiązać, zakładając, że współrzędna we w układzie kamery musi mieć wartość równą 1. Wówczas można napisać: (21) W ten sposób ustaliliśmy przekształcenia wszystkich współrzędnych i mo- żemy zapisać kompletny wzór na macierz rzutowania perspektywicznego: (22) Taki właśnie wzór implementuje funkcja glFrustum tradycyjnego OpenGL, czy funkcja frustum z biblioteki GLM. Sprawdźmy jeszcze, jak przekształcenie (21) zmieni wartość współrzęd- nych ze = –n, – (n+f)/2 i f z układu odniesienia kamery. Pamiętając o wykre- sie z Rysunku 3, nie należy spodziewać się „ładnych” wyników dla wartości pośrednich między –n, a –f, w szczególności punkt o składowej ze leżącej dokładnie pośrodku między płaszczyznami bliży i dali, w nowych współrzęd- nych nie będzie miał współrzędnej zNDC równej zeru. Podstawiając te wartości współrzędnej ze kolejno do wzoru (21), otrzymamy wyniki: –1, (f–n)/(f+n) i +1. Wartość zerową zNDC otrzymamy natomiast dla ze = –2nf/(f+n). Fakt, że przekształcenie współrzędnej z nie jest liniowe, nie jest tak dużym problemem, jak mogłoby się wydawać. Po przeprowadzeniu transformacji współrzędna ta służy bowiem jedynie do przeprowadzenia testu głębi. A do tego ważne jest zachowanie kolejności odległości punktów od kamery. Wa- runkiem jest więc jedynie, aby transformacja była zmienna monotonicznie, a nie aby zachowywała proporcje odległości punktów od kamery. Podobnie, jak zrobiliśmy w przypadku macierzy rzutowania izometrycznego, sprawdźmy, jak wygląda macierz rzutowania perspektywicznego dla symetrycz- nie ustawionej sceny (stosuję te same oznaczenia, co w przypadku wzoru (10)): (23) Ponadto dla w = 2, h = 2 i n = 1 otrzymamy (24) Różnice funkcji glFrustum i gluPerspective Rysunek 5.Widziany z boku (z kierunku +x) obszar przed ekranem
9/ www.programistamag.pl / Macierze w grafice 3D Biblioteka pomocnicza GLU (od ang. OpenGL Utility Library) zawiera często używaną funkcję gluPerspective. Efektem jej wywołania jest, podobnie jak w przypadku glFrustum, pomnożenie bieżącej macierzy przez macierz rzu- towania perspektywicznego. Różni się jednak przyjmowanymi argumentami: gluPerspective(ϕ, w/h, n, f); Oiledwaostatnieparametrynifsątakiesame,jakwprzypadkuglFrustum,tzn. są odległością bliży i dali od kamery, to dwa pierwsze się różnią. Argument drugi to stosunek szerokości w do wysokości h viewportu (ang. aspect ratio), a φ to kąt pionowego pola widzenia wyrażony w stopniach (ang. nazwa tego parametru to fovy,odfieldofviewy).ZRysunku5wynika,żekątówmożnazwiązaćzrozmiarem ekranu i odległością ekranu od kamery, korzystając z funkcji trygonometrycz- nych. Weźmy pod uwagę górny trójkąt z rysunku rozciągnięty między trzema punktami o zaznaczonych na rysunku współrzędnych (dla wygody narysowany także z prawej strony). Jego wysokość równa jest połowie wysokości ekranu h/2, a długość podstawy równa odległości kamery od ekranu n. Zatem: (25) Argumentów funkcji gluPerspective jest mniej i są bardziej intuicyjne. Proszę jednak zauważyć, że możliwe jest to dzięki założeniu, że scena jest ustawiona symetrycznie względem kamery (jak we wzorze (23)). Założenie symetrii sceny oznacza, że samodzielnie implementując funkcję z takimi parametrami, jak w gluPerspective,możemywykorzystaćfunkcjęodpowiadającąglFrustum,ale nie odwrotnie. Znajdźmy zatem wartości parametrów funkcji glFrustum, znając wartości parametrów funkcji gluPerspective. Z założenia symetrii wynika, że (26) Ponadto możemy obliczyć wysokość ekranu, korzystając ze wzoru (25), czyli: (27) Z kolei znając wysokość ekranu i jego proporcję, możemy łatwo znaleźć także jego szerokość, która równa jest iloczynowi tych dwóch wartości. Nie jestem jednak przekonany, czy wykorzystanie odpowiednika funkcji glFrustum do implementacji odpowiednika gluPerspective jest opłacalne, czy nie ła- twiej jest użyć po prostu wzoru (23). Współrzędne viewportu Układ współrzędnych NDC nie jest jeszcze ostatnim układem, jaki jest stoso- wany w grafice 3D. Na końcu potoku renderowania wykonywana jest jeszcze jedna liniowa transformacja: zmienne x i y są przeskalowywane w taki spo- sób, aby obejmowały zakres obszaru klienta, w którym znajduje się viewport, i zaokrąglane do liczb całkowitych (jednostką jest teraz piksel), a głębokość przekształcana jest do zakresu od 0 do 1: (28) To układ współrzędnych viewportu (ang. viewport coordinates). Wielkości X i Y wyznaczają lewy dolny róg viewportu, a W i H to odpowiednio jego sze- rokość i wysokość (we współrzędnych obszaru klienta okna). Zakres zmiennej z w układzie okna można kontrolować za pomocą funkcji glDepthRange, domyślnie jest to właśnie zakres od 0 do 1. Korzystając ze współrzędnych zv (skonwertowanych do liczb całkowitych), przeprowadzany jest test głębi, a współrzędne xv i yv używane są do zapełnienia bufora ramki, w którym przy- gotowywany jest obraz widoczny na ekranie monitora. Przykład Sprawdźmy, jak ten ciąg przekształceń wygląda w praktyce. Załóżmy, że na scenie znajduje się trójkąt o wierzchołkach w punktach A = (–1, –1, 0, 1), B = (1, –1, 0, 1) i C = (0, 1, 0, 1). To są współrzędne w lokalnym układzie od- niesienia. Załóżmy, że macierz świata jest jednostkowa, a macierz widoku taka, że obiekt odsunięty jest od kamery o 3. W układzie kamery punkty mają zatem współrzędne Ae = (–1, –1, –3, 1), Be = (1, –1, –3, 1) i Ce = (0, 1, –3, 1). Jeżeli do ustawienia macierzy w tradycyjnym OpenGL użylibyśmy nieobecnej w trybie rdzennym funkcji glFrustum z argumentami glFrustum(-1.0f, 1.0f, 0.71f*-1.0f, 0.71f*1.0f, 1.0f, 10.0f); to stałe określające bryłę widzenia równe są l = –1, r = 1, n = 1 i f = 10.Wartości b i t zależą od rozmiaru okna tak, aby proporcja wirtualnego ekranu na rzutni była taka sama, jak proporcja viewportu, a tym samym trójkąt widoczny na ekranie miał takie same kąty, co ten wirtualny. Dla okna o rozmiarze 800×600 w Windows 7, obszar klienta, którego całą powierzchnię zajmuje viewport, ma wielkość W×H = 782×555.To oznacza, że proporcja H/W będzie miała war- tość 0.709718645 (to jej odwrotność nazywana jest aspect ratio). Na potrzeby tego ćwiczenia zaokrąglimy ją do 0.71, co jest pewnie zbyt zgrubne, biorąc pod uwagę fakt, że piksel może mieć szerokość mniejszą niż jedną tysięczną szerokości viewportu. Wobec tego b = –0.71, a t = 0.71. Macierz rzutowania perspektywicznego będzie zatem miała postać: (29) Transformacja punktów A , B i C do współrzędnych przycinania wygląda następująco: (30)
10 / 5 . 2014 . (24) / programowanie grafiki Ponieważ wszystkie trzy punkty leżą na jednej płaszczyźnie prostopadłej do osi optycznej kamery ze = –3, to werteksy różnią się tylko współrzędnymi x i y. Tak pozostaje w układzie przycięcia. To się nie zmieni również po przejściu do układu NDC, czyli po podzieleniu przez wc = –ze = 3. Gdyby trójkąt znajdował się jeszcze dalej od płaszczyzny bliży, np. w ze = –5.5 (środek między bliżą a dalą), to współrzędna zc wzrosłaby do 4.49. Rośnie, bo w układzie przycięcia zmienia się kierunek osi z. Dla ze = –10, tj. gdyby trójkąt znajdował się w płasz- czyźnie dali, otrzymujemy zc = 10. Wróćmy jednak do ze = –3. W układzie NDC współrzędne punktów będą wówczas równe: (31) Wartości współrzędnych xc i yc punktów ANDC i BNDC wskazują, że trójkąt powi- nien zajmować mniej więcej jedną trzecią szerokości okna i prawie połowę wysokości. I tak jest w rzeczywistości (por. Rysunek 6). Rysunek 6. Ostateczne współrzędne pikseli odpowiadających punktom A, B i C Sprawdźmy jeszcze, jak zmieniałaby się współrzędna zNDC przy zmianie współ- rzędnej ze. Za współrzędną ze podstawmy wartości, dla których przed chwilą obliczaliśmy współrzędną przycinania zc. Dla ze = –5.5 otrzymaliśmy zc = 4.49, czyli zNDC = 4.49/5.5 = 0.81(63). Dla ze = 10, zc = 10, więc, jak należało się spo- dziewać zNCD = 1. Ostatnim przekształceniem, za które, podobnie jak za podział perspekty- wiczny także nie jesteśmy odpowiedzialni, przygotowując shadery dla karty graficznej, jest przejście do układu współrzędnych viewportu (kontrolują je funkcje glDepthRange i glViewport). A ponieważ X = 0 i Y = 0 oraz W = 782 i H = 555 (nasz viewport zajmuje cały obszar klienta), to przekształcenia te opi- sane są wzorami: (32) W efekcie otrzymujemy (por. Rysunek 6) (33) Sprawdźmy także, jak zmieni się obraz trójkąta, jeżeli rzut perspektywiczny zastąpimy rzutem izometrycznym, a więc jeżeli zamiast funkcji glFrustum, użyjemy funkcji glOrtho z tymi samymi argumentami, tj.: glOrtho(-1.0f, 1.0f, 0.71f*-1.0f, 0.71f*1.0f, 1.0f, 10.0f); dla odwrotności aspect ratio równej 0.709718645 ≈ 0.71. Wówczas macierz rzutowania przyjmie postać: (34) Korzystając z tej macierzy, możemy przetransformować punkty Ae, Be i Ce z układu kamery do układu przycinania: (35) W tym przypadku odległość werteksów od ekranu w żaden sposób nie wpływa na uzyskany obraz (współrzędne xc i yc), ma jedynie znaczenie przy teście głębi.
11/ www.programistamag.pl / Macierze w grafice 3D Konwersja do układu NDC jest trywialna, bo wc = 1: (36) Zwróćmy uwagę, że współrzędne y wszystkich punktów wykraczają poza zakres (–1, 1). To oznacza, że wierzchołki trójkąta znajdą się poza ekranem (Rysunek 7). I nic tu nie pomoże zwiększanie odsunięcia kamery od ekranu. Sprawdźmy jeszcze, jak zmieniałaby się współrzędna zc = zNDC, gdybyśmy prze- sunęli trójkąt w głąb do ze = –5.5 i ze = –10. Podstawiając te wartości do wzoru (37) otrzymamy odpowiednio 0 i 1. W odróżnieniu od przypadku rzutu perspekty- wicznego, w rzucie izometrycznym transformacja współrzędnej z jest liniowa. Rysunek 7. Obraz w przypadku rzutowania izometrycznego. Część trójkąta nie mieści się w obrębie ekranu I wreszcie przejście do współrzędnych viewportu (wzory 32) daje nam współ- rzędnepikseliodpowiadającychwierzchołkomtrójkątawidocznegonaekranie: (38) Czyli rzeczywiście współrzędne y dolnych wierzchołków są mniejsze od zera, a górnego większa od wysokości viewportu, tj. 555 (por. Rysunek 7). Macierz świata Macierz świata odpowiada za transformacje współrzędnych lokalnego układu współrzędnych modelu do układu sceny. Układ ten może być dowolnie zo- rientowany – zwykle związany jest z trwałym podłożem widocznym na scenie, jeżeli takie jest obecne. Z tego powodu nie ma żadnych specjalnych funkcji, które by pozwalały taką macierz zbudować. Będzie to natomiast dla nas okazją, aby przyjrzeć się macierzom podstawowych przekształceń: translacji, skalowa- nia, odbicia, pochylenia i obrotu. Poza pierwszym, wszystkie te przekształcenia mogą być zapisane za pomocą macierzy 3×3. Tylko translacja wymaga macie- rzy we współrzędnych jednorodnych. I od niej właśnie zaczniemy. Translacja Ciekawą własnością współrzędnych jednorodnych jest to, że umożliwiają opisanie przesunięcia o dowolny wektor za pomocą macierzy. W zwykłych współrzędnych kartezjańskich to nie jest możliwe – tam operacja przesunię- cia opisywana jest przez dodanie wektorów: (39) Analogiczny wynik, z tym, że dla czterowymiarowych wektorów, może- my uzyskać, jeżeli na współrzędne werteksu zadziałamy macierzą, w której czwarta kolumna zawierać będzie współrzędne wektora translacji: (40) Zakładając, że w = 1, w wyniku działania macierzy otrzymamy wektor prze- sunięty we współrzędnych x, y i z o wektor . Macierzą odwrotną do macierzy translacji o wektor jest macierz translacji o wektor . W tradycyjnym OpenGL funkcja glTranslate mnoży bieżącą macierz przez macierz (39). Jej argumentami są wielkości ∆x, ∆y i ∆z. Skalowanie i odbicia względem płaszczyzn układu współrzędnych Najprostszym przekształceniem jest skalowanie. Opisuje je macierz: (41) Działając nią na dowolny wektor, uzyskamy wektor, w którym poszczególne współrzędne będą przemnożone przez odpowiednie współczynniki sx, sy lub sz: (42)
12 / 5 . 2014 . (24) / programowanie grafiki W efekcie bryła, której macierz świata zawiera takie skalowanie, zostanie w kierunku x powiększona sx razy. Analogicznie w kierunkach y i z. Jeżeli war- tości współczynników są mniejsze od jedności (ale dodatnie), bryła zostanie zmniejszona. Dla ujemnych wartości uzyskamy złożenie skalowania i odbicia względem płaszczyzn wyznaczonych przez osie bieżącego układu współ- rzędnych. Dla sx < 0 będzie to odbicie względem płaszczyzny OYZ. Wielkości sx, sy i sz są argumentami funkcji glScale tradycyjnego OpenGL, która mnoży bieżącą macierz przez macierz (41). Macierz może również zawierać współczynnik sw (zamiast jedynki w ostat- nim wierszu). Wówczas, w efekcie dzielenia perspektywicznego wszystkie współrzędne wektora zostaną zmniejszone sw razy! Obroty Wróćmy na chwilę do zwykłej dwuwymiarowej płaszczyzny, na której obro- ty są jednoznaczne. W takim przypadku jest tylko jedna możliwa oś obrotu – prostopadła do tej płaszczyzny. Macierz obrotu równa jest wówczas (43) gdzie φ to kąt obrotu wokół środka układu współrzędnych skierowany w kie- runku przeciwnym do ruchu wskazówek zegara (Rysunek 8, lewy). Ten wzór podaję jako oczywisty, ale jego wyprowadzenie wcale nie jest takie trywialne. Macierz (43) to macierz obrotu punktu w nieruchomym układzie odniesienia. Warto to sprecyzować, bo czasem macierz obrotu dotyczy tzw. transformacji pasywnej, w której to nie punkt jest obracany, a układ odniesienia. Macierz taka jest wówczas transponowana względem macierzy (43) (por. Rysunek 8, prawy). Rysunek 8. Obrót obiektu w układzie odniesienia oraz obrót układu względem obiektu.Wzór (43) dotyczy sytuacji z lewego rysunku W trzech wymiarach sprawa się komplikuje, bo oś obrotu możemy wy- brać dowolnie. Pół biedy, jeżeli jest ona jedną z osi kartezjańskiego układu współrzędnych. Wówczas macierze obrotu przyjmują proste formy, które są prostym uogólnieniem wzoru (43). Najprościej to zobaczyć, jeżeli oś obrotu będzie osią OZ.Wówczas zmiany wartości współrzędnych dotyczą tylko płasz- czyzny OXY i macierz obrotu przyjmuje postać: (44) Sprawdźmy, jak macierz ta transformuje dowolny wektor położenia punktu we współrzędnych jednorodnych: (45) Zmieniają się zatem współrzędne punktu w taki sposób, że: (46) Sprawdźmy dla przykładu, jak obraca się punkt położony początkowo na osi OX: (1,0,0,1). W wyniku obrotu otrzymamy wektor o współrzędnych . To oznacza, że składowa maleje w miarę zwięk- szania kąta (jeżeli γ < π), a składowa y rośnie (dla γ < π/2). Zatem rzeczywiście punkt obraca się w stronę osi OY, tj. w kierunku przeciwnym do wskazówek zegara. Jeżeli założymy, że , a to możemy łatwo sprawdzić, to odle- głość obróconego punktu od punktu (0,0) nie zmienia się: (47) Analogicznie możemy zbudować macierz obrotu wokół osi OX. Wówczas zmianie ulegają tylko współrzędne y i z: (48) Nieco inaczej wygląda macierz obrotu wokół osi OY: (49) Zmienia się element, przy którym jest znak minus. Przyczyna jest prosta. Za- kładamy, że obrót punktu o dodatni kąt jest przeciwny do wskazówek zegara. W przypadku osi OZ oznacza to, że obrót punktu znajdującego się w pierw- szej ćwiartce następuje od osi OX do osi OY. W przypadku osi OX – od osi OY do osi OZ. Natomiast w przypadku obrotu wokół osi OY zmiana następuje od osi OZ do osi OX (Rysunek 9). Ta odwrócona kolejność osi powoduje, że ma- cierz obrotu wygląda jak transponowana.
13/ www.programistamag.pl / Macierze w grafice 3D Rysunek 9. Kierunki obrotów wokół osi OX, OY i OZ Trzy kąty obrotu wokół osi układu współrzędnych związanego z obraca- nym obiektem, nazywane kątami Cardana, to typowy sposób opisu orientacji samolotów (Rysunek 10). Kąt odpowiadający obrotom wokół osi pionowej nazywa się kurs, a jego zmiana to odchylenie (ang. odpowiednio heading i yaw). Obrót wokół osi skierowanej wzdłuż samolotu to przechylenie (roll), a wokół osi poziomej, prostopadłej do kierunku lotu to nachylenie (pitch). Te terminy stosowane są także w grafice 3D. Nachylenie (pitch) zmieniamy wówczas, gdy wolant ciągniemy do siebie lub pchamy od siebie. Wówczas samolot zadziera lub obniża nos. Jeżeli wolant przechylimy na bok, spowo- dujemy przechylenie samolotu (jedno skrzydło będzie wówczas wyżej od drugiego) – zmienia się kąt określany angielskim terminem roll, czyli właśnie Rysunek 10. Jak opisać zmianę orientacji modelu? Używając tego ostatniego wzoru do implementacji, oszczędzimy na kilku mnożeniach. W fizyce bryły sztywnej zamiast kątów Cardana używa się raczej kątów Eulera. Różnią się tym od kątów Cardana, że jedna z osi obrotów wybierana (50) czona jest mniej więcej przez ostatni kręg kręgosłupa). Możemy także, choć w niezbyt dużym zakresie, przechylać głowę na boki. Wróćmy jednak do samolotu. Ustalmy, że osie układu zorientowane są w taki sposób, że oś OZ wyznacza kierunek dziobu samolotu, a oś OY wska- zuje jego górę. W konsekwencji oś OX przebiega wzdłuż jego prawego skrzy- dła. Wówczas kąt γ to przechylenie (roll), kąt β– nachylenie (pitch), a kąt α – odchylenie (yaw). Wszystkie trzy kąty mogą służyć do ustalenia zmiany orien- tacji samolotu. Ważna jest przy tym, i należy to szczególnie mocno podkreślić, kolejność obrotów wokół poszczególnych osi. Najczęściej używana konwencja to rozpoczęcie od przechylenia, potem nachylenie i wreszcie odchylenie, a więc użycie macierzy: . Macierz, jaką wówczas uzyskamy, to przechylenie. Wreszcie naciśnięcie orczyka zmienia pozycję steru kierunkowego, a co za tym idzie, kurs samolotu, czyli heading lub yaw. Mylące w tym mode- lu jest to, że lekkie położenie na boku rzeczywistego samolotu (tj. roll) w obecności grawitacji i powietrza również powoduje zmianę jego kierunku – dzieje się tak ze względu na zmianę kierunku siły nośnej, któ- ra jest prostopadła do płaszczyzny skrzydeł. Zamiast samolotu lepiej byłoby zatem wyobrażać sobie waha- dłowiec w stanie nieważkości. Możemy też użyć jako przykładu własnej głowy. Możemy ją skręcać w lewo i prawo (oś obrotu wyznaczona jest przez kręgosłup), możemy spoglądać w dół i w górę (oś obrotu wyzna- spośród osi lokalnego układów współrzędnych zostanie powtórzona. Dzię- ki wcześniejszym obrotom, przy drugim obrocie nie jest ona już jednak tak samo zorientowana. Klasycznym zestawem jest kolejność osi: OZ, OX i obró- cona oś OZ. Oznacza to macierz: (51)
14 / 5 . 2014 . (24) / programowanie grafiki W przypadku użycia kątów Eulera grozi nam jednak tak zwana bloka- da przegubowa (ang. gimbal lock). Gdy obrót układu (lub bryły) składa się z trzech następujących po sobie obrotów wokół niezależnych prostopadłych osi, to problem ów pojawi się wtedy, gdy pierwszy obrót wykonamy o kąt 90 stopni. Wówczas nowa oś obrotu OX pokrywa się z pierwotną osią OY, co zmniejsza ilość stopni swobody i powoduje, że obroty wokół drugiej osi zmie- niają jedynie „polaryzację” układu. Doświadczymy tego, gdy podniesiemy głowę tak, żeby spoglądać pionowo w górę, a następnie obracamy się wokół własnej osi. Punkt, na który patrzymy wówczas, się nie zmieni. Inna odmiana tego samego problemu pojawi się, gdy obrót o kąt prosty wykonany zostanie jako drugi z trzech obrotów. Wówczas trzeci obrót jest wykonywany wokół tej samej osi co pierwszy i skutek jest podobny. Drugą stroną tego samego pro- blemu jest fakt, że ten sam ostateczny obrót można osiągnąć za pomocą wie- lu zestawów trzech kątów Eulera.To bardzo utrudnia porównywanie obrotów i ich interpolację (np. przy automatycznym ruchu kamery). Rozwiązaniem jest użycie macierzy obrotu lub kwaternionów. Obrót wokół dowolnej osi W praktycznych zastosowaniach bardzo ważna jest możliwość obracania mo- deli wokół wskazanej osi. To zadanie realizuje funkcja glRotate tradycyjnego OpenGL. Wyprowadzenie wzoru przeprowadzę we współrzędnych kartezjań- skich, bo i bez współczynnika skalowania wzory będą bardzo długie. Załóżmy oś obrotu wyznaczoną przez jednostkowy werktor , przechodzącą przez początek kartezjańskiego układu współrzędnych O = (0,0,0) i dowolny punkt P = (x, y, z), który chcemy obrócić o kąt θ przeciwnie do wskazówek zegara. Rysunek 11. Obrót punktu wokół osi Zastanówmy się, w jaki sposób zapisać, że punkt P wskazywany przez wektor wodzą- cy został obrócony o kąt θ (Rysunek 11). Jeżeli wektor rozłożymy na część równo- ległą i prostopadłą do osi obrotu: , to część równoległa nie ule- gnie żadnej zmianie, natomiast część prostopadła będzie podlegać prawom obrotu dwuwymiarowego. W efekcie jeżeli obrócony punkt wskazywany jest przez wektor to: (52) Dwuwymiarowa przestrzeń obrotu prostopadłej części wektora, w której zapisa- na została macierz w drugim wzorze, wyznaczona jest przez wektor i wektor doniegoprostopadły,ajednocześnieprostopadłydowektorów i .Możnago zatem wyznaczyć, korzystając z iloczynu wektorowego . Załóżmy, że oś obrotu skierowana jest w kierunku z. Wówczas dwuwymiarowa przestrzeń pro- stopadła może być zapisana za pomocą współrzędnych x i y: (53) To oznacza, że: (54) Ostatecznie obrócony wektor dany jest wzorem: (55) Jak, korzystając z tego wzoru, napisać macierz obrotu, której elementy będą zawierały współrzędne wektora i kąt θ? Przede wszystkim musimy przepisać wzór (55) w taki sposób, aby opisywał transformację całe- go wektora .Wykorzystajmy fakt, że iloczyn wektorowy wektorów i jest równy zeru. Wobec tego . Składową równoległą wektora rozłóżmy na dwie części (56) Wówczas możemy przepisać wzór (55) jako: (57) Przechodząc do zapisu macierzowego, wykorzystajmy operator gwiazdki do zapisu iloczynu wektorowego oraz iloczyn tensorowy (diadyczny) do oblicze- nia składowej równoległej wektora. W efekcie uzyskamy: (58) (59) Uzyskaliśmy w ten sposób przekształcenie wektora , które, po zsumowaniu trzech wyrazów w nawiasie klamrowym, równoważne jest macierzy:
15/ www.programistamag.pl / Macierze w grafice 3D (60) ku ujemnych wartości x (w obróconym układzie współrzędnych). Realizuje to macierz będąca iloczynem trzech macierzy przekształceń: translacji, obrotu i ponownie translacji: W efekcie uzyskujemy macierz, w której łatwo rozróżnić część dotyczącą ob- rotu oraz część dotyczącą przesunięcia. Przesunięcie to jest złożeniem prze- sunięcia do środka obrotu i do nowego położenia obiektu. Pochylenie Mniej intuicyjnym przekształceniem, i stosunkowo rzadko używanym, jest pochylenie (ang. shear lub skew). Nie ma odpowiadającej mu funkcji w tra- dycyjnym OpenGL. Mówimy o nim w zasadzie tylko dlatego, że często jest niechcianym efektem błędnego zdefiniowania macierzy świata lub pojawia się w wyniku kumulacji błędów numerycznych. Pochylenie jest jedynym z omawianych w tej części przekształceń, które nie zachowuje kątów. Zmodyfikujmy macierz jednostkową w taki sposób, aby poza jedynkami na diagonali, któryś z pozostałych elementów także był różny od zera. Oto przykład: (62) Jeżeli współrzędne wektora przemnożymy przez taką macierz, uzyskamy: (63) Sprawdźmy tę macierz, choćby dla jednego prostego przypadku. Załóżmy oś obrotu skierowaną wzdłuż osi OZ: . Wówczas macierz znacznie się upraszcza, bo wszystkie iloczyny różnych składowych wektora są równe zeru, i uzyskujemy macierz postaci: Uzyskaliśmy więc, pomijając brak współrzędnej w, macierz ze wzoru (44). Macierze obrotu a kwaterniony jednostkowe Poza kątami Cardana lub Eulera oraz macierzami, istnieje jeszcze trzeci uznany sposób opisu obrotów – kwaterniony jednostkowe. Z ich pomocą można łatwo zapisać obrót wokół osi wyznaczonej przez wektor o zadany kąt, tzn. można wykazać ich pełną równoważność z macierzą (59). Warto także zaznaczyć, że kwaterniony pozwalają na zapisanie równań ruchu bryły sztywnej.Więcej na ten temat w książce Grafika, fizyka, metody numeryczne. Symulacje fizyczne z symulacją 3D wydanej przez PWN w 2010 roku. Złożenie obrotów i translacji – obrót wokół dowolnego punktu Wszystkie macierze obrotów, które wyżej przedstawiłem, opisują obroty wo- kół środka bieżącego układu odniesienia. A co w przypadku, gdy chcemy wy- konać obrót wokół osi przesuniętej względem początku układu współrzęd- nych? W takim wypadku konieczne jest złożenie macierzy obrotu z macierzą translacji: (61) gdzie to macierz obrotu, a jest macierzą translacji o wektor . Najpierw przesuwamy się do punktu, wokół którego ma być wykonywany obrót, na- stępnie wykonywany jest ów obrót. Wreszcie następuje przesunięcie z po- wrotem, ale w nowym, obróconym układzie (Rysunek 12). Nie wracamy więc do tego samego punktu, a do punktu obróconego względem punktu, do któ- rego przesunęła nas macierz . Rysunek 12. Złożenie dwóch translacji i obrotu, czyli obrót względem wyznaczo- nego punktu Sprawdźmy na prostym przykładzie, jak wygląda macierz takiego przekształ- cenia złożonego z trzech przekształceń, a konkretnie, jak będzie wyglądała macierz, która realizuje obrót wokół osi równoległej do OZ, ale przesuniętej w kierunku +x o . Oczywiście opisuje ją nadal macierz 4×4. Aby ją otrzymać, musimy przesunąć lokalny układ współrzędnych o w kierunku dodatnich wartości x, czyli do położenia osi obrotu. Następnie obrócić ów układ o kąt wokół nowej osi OZ i wreszcie przesunąć go z powrotem o , tj. w kierun-
16 / 5 . 2014 . (24) / programowanie grafiki W efekcie współrzędne x zaczną zależeć od współrzędnej y sprzed trans- formacji i bryły zostaną pochylone o kąt, którego tangen- sem jest wartość pxy (Rysunek 13). Rysunek 13. Pochylenie Najłatwiej to zrozumieć, myśląc o wersorach, czyli wektorach jednostkowych skierowanych wzdłuż osi układu współrzędnych,„zaszytych”w macierzy prze- kształcenia (zob. ramka poniżej). Pomyślmy, że nowy układ współrzędnych nie ma już prostopadłych osi. Kąt między nimi zmalał bowiem o kąt równy W efekcie zniekształceniu ulega cała scena. Zacznijmy od zrozumienia, czym tak naprawdę jest macierz przekształ- cenia (dla uproszczenia o rozmiarach 3×3). Powinna przeprowadzić jeden układ współrzędnych O w inny układ współrzędnych O’ (ograniczenie roz- miaru powoduje, że początki obu układów pozostają w tym samym punk- cie). Aby to lepiej poczuć, sprawdźmy, co się stanie, gdy macierzą zadzia- łamy na wersory nieruchomego układu współrzędnych O. W wyniku uzyskaliśmy trzy kolumny macierzy przekształcenia. Kolumny te przechowują więc współrzędne wersorów układu O’, ale wyrażone wzglę- dem układu O. Dla przykładu . Współrzędną można oczywiście pochylić poza płaszczyznę OXY, uzależ- niając ją także od współrzędnej z. Pozwoli na to macierz: (64) Analogicznie można wprowadzić pochylenie w osi OY lub OZ.Wystarczy tylko ustawić odpowiednie wartości elementów macierzy: (65) Pierwszy wiersz odpowiada za pochylenie osi OX, drugi OY, a trzeci OZ. Rzut na płaszczyznę Jedną z macierzy, która może się przydać, a której odpowiednika także nie ma w tradycyjnym OpenGL, jest macierz opisująca rzutowanie punktu na płaszczyznę. Wykorzystuje się ją chociażby do prostego generowania cieni. Macierz taka może być zapisana tylko we współrzędnych jednorodnych. Rysunek 14. Schemat rzutowania punktu na płaszczyznę My jednak zaczniemy od prostych wzorów we współrzędnych kartezjańskich. Załóżmy, że płaszczyzna, na którą rzutujemy punkt (rzutnia, Rysunek 14), opi- sana jest wzorem: (66) gdzie d to odległość płaszczyzny od początku układu współrzędnych, a to trójwymiarowy wektor normalny do płaszczyzny. Rzutowany punkt i śro- dek rzutowania (w przykładzie generowania cienia środkiem rzutowania jest punktowe źródło światła) wyznaczają prostą (promień): (67) Natejprostejmusioczywiścieleżećrównieżrzutowegopunktu(Rysunek14).Szu- kanym punktem jest więc przecięcie prostej i płaszczyzny.Wobec tego podstaw- my wzór punktu na prostej do wzoru wyznaczającego punkty na płaszczyźnie: (68) i wyznaczmy konkretną wartość współczynnika k, dla którego punkt na pro- stej znajduje się także na płaszczyźnie: (69) Jeżeli wstawimy jawną postać tego współczynnika do wzoru na prostą, otrzy- mamy gotowy przepis na obliczenie rzutu punktu: (70)
17/ www.programistamag.pl / Macierze w grafice 3D Dwa pierwsze wyrazy licznika tworzą podwójny iloczyn wektorowy. Ta informacja na niewiele nam się jednak przyda. Ważniejsze jest, że skróciły się dwa wyrazy, w których pojawiałyby się iloczyny składowych wektora. Nie oznacza to jeszcze, że położenie rzutu zależy liniowo od położenia rzutowa- nego punktu (co pozwalałoby na zapisanie rzutowania za pomocą macierzy). Współrzędne rzutowanego punktu pojawiają się niestety w mianowniku po- wyższych wzorów: (71) gdzie , i to współrzędne wektora , a , i – wektora . Wszystkie współrzędne rzutu skalowane są identycznym współczynni- kiem o wartości . To sugeruje, że aby pozbyć się mianownika, można przejść do współrzędnych jednorodnych, w których można go będzie przenieść do licznika współrzędnej w. Po rozszerzeniu układu współrzędnych uzyskamy: (72) Wektory i uzupełniamy o czwartą współrzędną w: , . Zgodnie z własnością współrzędnych jednorodnych wolne wyrazy opisujące translację o wektor znajdą się w czwartej kolumnie macierzy – wiążę je ze składową w wektora określającego położenie rzutowanego punktu. Zauważ- my, że jeżeli wprowadzimy dodatkowo oznaczenie i uporząd- kujemy wzory, to całe przekształcenie ujawni interesującą symetrię: (73) Nowe oznaczenie jest zgodne z definicją płaszczyzny we współrzędnych jed- norodnych, która określona jest iloczynem skalarnym w tych współrzędnych: (74) Nic już nie stoi na przeszkodzie, aby transformację punktu : do jego rzutu zapisać za pomocą macierzy: (75) Użyta w elementach diagonalnych liczba równa jest iloczynowi skalarnemu we współrzędnych jednorodnych wektora określającego położenie źródła światła (lub ogólniej: środka rzutowania) i wektora normalnego do rzutni: (76) Rozszerzenie współrzędnych kartezjańskich do współrzędnych jednorod- nych, poza tym, że daje możliwość zapisu rzutowania za pomocą macierzy, ma także inną, sygnalizowaną już, zaletę. W tych współrzędnych możemy przesunąć źródło światła do nieskończoności. Wprawdzie powyższe rozwa- żania zakładają, że współrzędne w źródła światła i rzutowanego punktu są równe jedności ( , ), jednak wyprowadzona w ten sposób macierz rzutowania daje poprawne rezultaty również dla dowolnych war- tości tych elementów, także gdy . Uzyskujemy wówczas rzut rów- noległy, który z tego punktu widzenia jest szczególnym przypadkiem rzutu perspektywicznego. Macierz widoku Macierz widoku określa transformację z układu sceny do układu kamery. Postaramy się odtworzyć macierz tworzoną przez wygodną funkcję glu- LookAt z biblioteki GLU służącą do określania położenia i orientacji kamery. Argumentami tej funkcji są trzy wektory określone w układzie współrzędnych sceny: wektor wskazujący położenie kamery E, punkt, na który kamera jest skierowana C i, tzw. wektor polaryzacji (symbol od ang. up – góra). void gluLookAt(GLdouble eyeX, GLdouble eyeY, GLdouble eyeZ, GLdouble centerX, GLdouble centerY, GLdouble centerZ, GLdouble upX, GLdouble upY, GLdouble upZ); Punkty E i C wyznaczają wektor (od ang. forward – do przodu) i zarazem oś optyczną kamery. Wzdłuż tej osi (od punktu C do E) położona będzie oś OZ w układzie współrzędnych kamery, choć skierowana w przeciwną stronę. Kamera skierowana na punkt C może być dowolnie obrócona wokół osi optycznej i dla- tego konieczny jest wektor polaryzacji, który wyznacza jej orientację. Rysunek 15, który ilustruje tę sytuację, nie pokazuje jednak sytuacji ogólnej, choć bar- dzo często spotykanej. Zakłada bowiem, że kamera skierowana jest na środek układu współrzędnych sceny, zatem C = O = (0,0,0), a wektor jest skierowany wzdłuż osi OY. Z Rysunku 15 widać, że macierz tworzoną przez funkcję gluLookAt można wyprowadzić przez złożenie dwóch obrotów: pierwszego wokół osi OY o kąt β, i drugiego wokół nowej obróconej osi OX' o kąt α, oraz translacji do punktu E, czyli w naszym szczególnym przypadku o wektor . Łatwiej jednak znaleźć ją, traktując ją jako transformację ukła- du współrzędnych i pamiętając, że kolumny macierzy 3×3 stanowią wersory nowego układu współrzędnych wyrażone w starym układzie współrzędnych,
18 / 5 . 2014 . (24) / programowanie grafiki a zatem wersory osi OX, OY i OZ układu współrzędnych kamery widziane z ukła- du współrzędnych związanego ze sceną. W czwartej kolumnie wstawimy poło- żenie punktu E, dzięki czemu macierz będzie także uwzględniała przesunięcie początku układu współrzędnych do położenia kamery. Rysunek 15. Położenie i orientacja kamery w układzie współrzędnych sceny Po transformacji do układu współrzędnych kamery, oś OZ będzie wyznaczona przez wektor (skierowany od centrum do kamery). Oś OX będzie jedno- cześnie prostopadła do osi OZ i wektora (wektory i nie mogą być równoległe!).Wskazywać będzie ją wektor (od ang. right, czyli prawo).Wek- tor jednoznacznie wyznacza polaryzację kamery, ale niekoniecznie jest prostopadły do wektorów i , dlatego należy znaleźć wektor , który ten warunek będzie spełniał, pozostając w płaszczyźnie napiętej na wekto- rach i (płaszczyzna zaznaczona na Rysunku 15, lewym). To oznacza, że należy wykonać następującą ogólną konstrukcję: 1. oblicz wektor 2. zapisz znormalizowaną wartość tego wektora , 3. oblicz wektor prostopadły do wektorów i , korzystając z iloczy- nu wektorowego (zwrot wyniku wyznacza reguła śruby prawoskrętnej), 4. znormalizuj wektor , 5. oblicz wektor prostopadły do wektorów i (wektor będzie jednostkowy, bo oba czynniki są jednostkowe, a jednocześnie pro- stopadłe do siebie). Wersorami nowego układu współrzędnych kamery (wyrażonymi we współ- rzędnych sceny) są zatem: , i , co oznacza, że macierz widoku obracająca współrzędne z układu sceny do układu kamery (jeszcze bez przesunięcia) powinna wyglądać następująco: (77) A co z przesunięciem? W przypadku, w którym C = O = (0,0,0), mamy dwie równoważne opcje: możemy zacząć serię przekształceń od przesunięcia ka- mery do punktu E, tj. o wektor , a potem wykonać obroty, lub najpierw wykonać obroty, a dopiero po nich przesunąć scenę w kierunku przeciwnym do osi OZ w nowym układzie kamery o odległość równą długości wektora , tj. o wektor : (78) W tym drugim przypadku macierz widoku wygląda znacznie lepiej, ale to pierw- szy przypadek jest ogólniejszy i działa dla dowolnego położenia punktu C: (79) W przypadku, w którym C = O = (0,0,0), wektor , a ponadto jest prostopadły do wektorów i . W efekcie pierwsze dwa wyrazy czwartej ko- lumny równe są zeru, a trzeci staje się długością wektora ze zmienionym znakiem i tym samym wzór (79) przechodzi w (78). Sprawdźmy,jakkonstrukcjamacierzywidokuwyglądawpraktyce.Najpierw rozważmy prosty przykład, w którym kamera znajduje się w E= (0,0,3) i jest skie- rowana jest na punkt C = (0,0,0) (pozycje w układzie sceny). Wektor polaryzacji równy . Zatem scena i kamera są rozsunięte o 3 jednostki wzdłuż osi OZ. Wówczas powyższa konstrukcja będzie przebiegać następująco: 1. 2. 3. , 4. Wektor jest jednostkowy ( ) 5. . Wektory , i są skierowane od- powiedniowzdłużosiOX,OYiprzeciwniedoosiOZwukładziekamery.Sąwięcna pewnodosiebieprostopadłe.Wefekciemacierzwidokuniebędzieobracałaukła- du sceny, a będzie go jedynie przesuwać. Będzie to więc zwykła macierz translacji:
19/ www.programistamag.pl / Macierze w grafice 3D (80) To oczywiście bardzo prosty przykład. Sprawdźmy więc, co się stanie, jeżeli przesuniemy kamerę do punktu E = (–1,1,1), nie zmieniając punktu C = (0,0,0), na który kamera jest skierowana.To bardziej odpowiada sytuacji z Rysunku 15. Tym razem konstrukcja przebiega następująco: 1. 2. , 3. 4. , a zatem po unormowaniu 5. . Wektor , choć nietrywialny, jest jednak jednostkowy, co łatwo sprawdzić, obliczając jego normę: (81) Sprawdźmy także, czy wektory: , i są do siebie prostopadłe. Najłatwiej zrobić to, obliczając ich iloczyny skalarne: (82) Łatwo się przekonać, że macierz, którą możemy dzięki tym wektorom oraz wektorowi skonstruować, a więc (83) jest ortonormalna. Złożenie macierzy widoku, świata i rzutowania Sprawdźmy teraz, jak trójkąt o wierzchołkach A = (–1, –1, 0, 1), B = (1, –1, 0, 1) i C = (0, 1, 0, 1) z wcześniejszego przykładu będzie widoczny na ekranie, jeżeli macierzą widoku będzie macierz odpowiadająca kamerze ustawionej w punkcie E = (–1,1,1) skierowana na punkt O = (0,0,0) z wektorem polary- zacji równym i zastosujemy rzutowanie perspektywiczne z parametrami ekranu takimi samymi, jak we wcześniejszym przykładzie. Ma- cierz świata pozostanie macierzą jednostkową. Wiemy już, że macierz widoku w tym przypadku równa jest:
20 / 5 . 2014 . (24) / programowanie grafiki (84) a macierz rzutowania to: (85) Zatem iloczyn macierzy świata, widoku i rzutowania równy jest: (86) Zastosujmy tę macierz na współrzędnych punktów A, B i C (w układzie własnym modelu), aby uzyskać współrzędne tych punktów w układzie przycinania: (87) Następnie wykonajmy dzielenie perspektywiczne: (88) I wreszcie przechodzimy do współrzędnych viewportu (wzór (32)), zaokrągla- jąc współrzędne x i y tak, żeby wskazywały konkretny piksel na ekranie (por. Rysunek 16): (89) Rysunek 16. Na rysunku widoczne są współrzędne wierzchołków trójkąta w układzie współrzędnych viewportu Bardzo dziękuje Karolinie Matulewskiej za pomoc w redagowaniu tekstu i upo- rządkowaniu wzorów. Jacek Matulewski Fizyk zajmujący się na co dzień optyką kwantową i układami nieuporządkowanymi na Wydziale Fizyki, Astronomii i Informatyki Stosowanej UMK w Toruniu. Od 1998 r. interesuje się programo- waniem dla systemu Windows, w szczególności platformą .NET i językiem C#. Autor serii książek poświęconych programowaniu. Większość ukazała się nakładem wydawnictwa Helion. Wierny użytkownik kupionego w połowie lat osiemdziesiątych "komputera osobistego" ZX Spectrum 48k.
22 / 5 . 2014 . (24) / programowanie gier Rafał Kocisz Tych czytelników, którzy po raz pierwszy mają do czynienia z cyklem Wzor- ce Programowania Gier, odsyłam do numeru 11/2013, w którym pojawił się pierwszy artykuł z niniejszej serii (opisujący wzorzec: Szkielet Aplikacji); czytając wstęp do tego artykułu, poznacie ideę cyklu oraz strukturę po- szczególnych jego części. Pozostałych czytelników zapraszam na wspólną podróż do pełnej przy- gód krainy programowania gier. Wzorzec Zarządca Zasobów: przeznaczenie Każda bez wyjątku gra skonstruowana jest z pewnej liczby zasobów (ang. resources), zwanych też czasami aktywami (ang. assets) bądź mediami (ang. media). Przykładami takich zasobów są: »» tekstury, »» atlasy tekstur, »» materiały, »» animacje, »» programy cieniujące (ang. shaders), »» klipy audio i video, »» pliki z definicjami struktury poziomów. Do tej listy można by dodać jeszcze wiele, wiele punktów. Tak czy inaczej, w praktyce każda gra musi w taki czy inny sposób zarządzać swoimi zasoba- mi, chociażby w najbardziej podstawowym zakresie, jakim jest: »» wczytywanie zasobów do pamięci (ang. resource loading), »» usuwanie zasobów z pamięci (ang. resource unloading), »» zapewnienie łatwego dostępu do zasobów dla komponentów gry (ang. resource accessibility). Przeznaczeniem wzorca Zarządca Zawartości jest opakowanie funkcjonalno- ści związanej z zarządzaniem zasobami i dostarczenie jednolitego, eleganc- kiego interfejsu do obsługi tej funkcjonalności. Warto w tym miejscu zaznaczyć również, iż zarządzanie zasobami gry od- bywa się zazwyczaj w dwóch etapach: »» przetwarzanie wstępne (ang. off-line processing), realizowane najczęściej przez dedykowane narzędzia działające na etapie budowania gry; przy- kładem takiego przetwarzania może być transformacja pojedynczych tekstur w atlasy tekstur czy pakowanie całego drzewa zasobów w jeden plik-archiwum, »» przetwarzanie w czasie wykonania (ang. run-time processing), na co skła- dają się takie działania jak wstępne wczytywanie zasobów (ang. pre-lo- ading) czy usuwanie z pamięci zasobów, które nie są już potrzebne; zna- mienne w tym przypadku jest to, iż wszystkie te działania odbywają się w czasie działania gry. Kompleksowy proces zarządzania zasobami (zarówno przed, jak i w czasie wykonania gry) oraz wspomagające go narzędzia nazywamy potokiem prze- twarzania zawartości (ang. content pipeline). Warto w tym miejscu podkreślić, iż komponent implementujący prezentowany w niniejszym artykule wzorzec Zarządca Zawartości związany jest z drugim z wymienionych wyżej etapów i wspiera obsługę zasobów gry w czasie jej wykonania. Inne nazwy Wzorzec Zarządca Zawartości (ang. Content Manager) występuje pod wielo- ma innymi nazwami; niektóre z nich to: »» Zarządca Zasobów (ang. Resource Manager), »» Słownik Zasobów (ang. Asset Dictionary), »» Zarządca Mediów (ang. Media Manager). Uzasadnienie stosowania Zarządzanie zasobami gry wiąże się z zestawem konkretnych działań i odpo- wiedzialności, które można podsumować następująco: »» Zarządca Zawartości powinien zagwarantować, że w danym momencie w pamięci istnieje tylko jedna kopia unikalnego zasobu. »» Zarządca Zawartości powinien zarządzać czasem życia każdego zasobu: wczytywać go do pamięci, kiedy zachodzi taka potrzeba, oraz usuwać go z pamięci, kiedy staje się bezużyteczny. »» Zarządca Zawartości powinien obsługiwać wczytywanie zasobów złożo- nych (ang. composite assets); zasób złożony, w przeciwieństwie do zasobu atomowego (ang. atomic asset), jest zasobem, który składa się z innych za- sobów. Przykładem tego rodzaju zasobu może być animacja, która oprócz informacji dotyczących struktury klatek i czasów ich wyświetlania odnosi się również do konkretnych tekstur, będących zasobami atomowymi. »» Zarządca Zawartości utrzymuje spójność referencji pomiędzy zasobami (ang. referential integrity); jest to szczególnie istotne przy przetwarzaniu zasobów złożonych, gdzie wiele aktywów może odnosić się do siebie (tak jak w przypadku opisanego wyżej przykładu z animacją). W tym układzie zarządca powinien dbać o to, aby wszystkie aktywne zasoby zawsze znaj- dowały się w pamięci oraz, w miarę potrzeby, usuwać je, kiedy nie są już potrzebne. »» W wybranych przypadkach Zarządca Zawartości może odpowiadać za zarządzanie pamięcią przydzieloną na obsługę zasobów; np. zapewniać, iż zasoby w czasie wczytywania będą umieszczane we właściwych obsza- rach pamięci. »» Zarządca Zawartości może udostępniać mechanizm do wykonywania dodatkowego przetwarzania zasobów tuż po ich wczytaniu do pamięci (ang. load-initializing). »» ZarządcaZawartościpowinienudostępniaćujednoliconyinterfejsdoobsługi dowolnegorodzajuzasobów;powinienrównieżpozwalaćużytkownikombi- Wzorce Programowania Gier: Zarządca Zawartości Witam w kolejnej, drugiej odsłonie cyklu Wzorce Programowania Gier. Napisanie tego artykułu zajęło mi z różnych przyczyn o wiele więcej czasu niż się spodzie- wałem, więc w tym miejscu chciałbym przeprosić wszystkich zainteresowanych czytelników za zwłokę i podziękować im jednocześnie za cierpliwość. Dziś na warsztat weźmiemy jedno z fundamentalnych zagadnień w dziedzinie programo- wania gier, jakim jest zarządzanie zasobami (ang. resource management).
24 / 5 . 2014 . (24) / programowanie gier blioteki definiować własne typy zasobów i być w stanie je obsłużyć. »» Jeśli zachodzi taka potrzeba, Zarządca Zawartości może obsługiwać asyn- chroniczne wczytywanie zasobów, (ang. resource streaming). Biorąc pod uwagę tak szeroki zakres odpowiedzialności, uzasadnioną wydaje się potrzeba jej kapsułkowania w ramach wyspecjalizowanego komponen- tu wielokrotnego użytku. W związku z tym Zarządca Zasobów jest idealnym kandydatem na składnik biblioteki bądź silnika wspomagającego programo- wanie gier. Patrząc na te kilkadziesiąt projektów związanych z produkcją gier, w których miałem okazję brać udział, te, w których zarządzanie zasobami od- bywało się na zasadzie ad hoc, były zazwyczaj marszami ku klęsce. Solidny komponent do zarządzania zawartością gry oraz uniwersalny mechanizm do zarządzania stanami gry (o którym napiszę w jednym z kolejnych odcinków niniejszej serii) to w mojej opinii baza, bez której ciężko budować jakiekol- wiek gry, nawet proste, nie mówiąc już o tych bardziej złożonych… Stosowalność Zarządzanie zawartością jest tak fundamentalnym zagadnieniem w dzie- dzinie wytwarzania gier, że odpowiedź na pytanie dotyczące stosowalności wzorca Zarządca Zawartości wydaje się być oczywista: w zasadzie powinno się stosować go zawsze. Wyjątkiem mogą być tu naprawdę proste gry, wykorzystujące co najwyżej kilka rodzajów zasobów atomowych (np. tekstury i klipy audio). Jeśli jednak wTwojej grze pojawiają się zasoby złożone, to nie ma co się zastanawiać: trud włożony w implementację Zarządcy Zawartości opłaci Ci się z nawiązką! Struktura Struktura wzorca Zarządca Zawartości przedstawiona jest na Rysunku 1. Rysunek 1. Struktura wzorca Zarządca Zawartości Sam Zarządca Zawartości jest scentralizowanym komponentem o dostępie globalnym, zaimplementowany zazwyczaj jako singleton. Zarządca oferuje dwie podstawowe operacje: »» Acquire(), czyli pozyskanie zasobu; przy pierwszej próbie pozyskania operacja ta jest tożsama z wczytaniem zasobu do pamięci. »» Release(), czyli uwolnienie zasobu. Jeśli żaden inny komponent nie przechowuje referencji do tego zasobu, operacja ta jest w większości przypadków tożsama z usunięciem zasobu z pamięci. Poza tym, w skład wzorca wchodzi często hierarchia klas reprezentujących za- soby, z abstrakcyjną klasą Asset oraz szeregiem pod-klas, w ramach których zaimplementowane są konkretne rodzaje aktywów. Uważni czytelnicy zwrócili zapewne uwagę na powtarzający się termin: referencja. Otóż kluczowym elementem procesu zarządzania zasobami (a co za tym idzie: również istotnym składnikiem naszego wzorca) jest mechanizm zliczania referencji. Problem w tym, iż implementacja tego mechanizmu jest bardzo silnie zależna od języka, w którym jest on realizowany. Np. w przy- padku języka C++ do zliczania referencji można wykorzystać mechanizm inteligentnych wskaźników (jak to zrobić, przekonasz się, czytając podpunkt Implementacja zawarty w dalszej części niniejszego artykułu). Proszę zwrócić uwagę, że na diagramie klas obrazującym strukturę wzor- ca metoda AssetManager::Acquire() zwraca obiekt typu AssetRef. Typ ten reprezentuje rodzaj uchwytu do zasobu, który odpowiada za zliczanie referencji jego użycia. Postaram się ten niełatwy temat wyjaśnić bardziej jasno i szczegółowo w sekcji opisującej implementację prezentowanego wzorca. Uczestnicy »» AssetManager: globalny obiekt reprezentujący scentralizowany mecha- nizm zarządzania zasobami. »» Asset: abstrakcyjny interfejs reprezentujący zasób. »» AssetRef: uchwyt do zasobu, odpowiedzialny za zliczanie referencji jego użycia. »» Texture, AudioClip itd.: przykłady klas reprezentujących konkretne typy zasobów. Współpraca »» AssetManager: udostępnia zasoby w postaci uchwytów. »» AssetManager: w razie potrzeby wczytuje zasoby do pamięci. »» AssetManager: odpowiada za przetworzenie zasobów tuż po ich wczy- taniu do pamięci (ang. load-initializing). »» AssetRef: odpowiada za zliczanie referencji do zasobów (ang. reference counting) »» AssetRef: usuwa zasób z pamięci, jeśli nikt go nie używa. Konsekwencje Stosowanie wzorca Zarządca Zawartości znacznie upraszcza proces zarządza- nia zasobami oraz czyni go bardziej stabilnym. Elastyczność wzorca ułatwia dodawanie nowych rodzajów zasobów do gry. Mechanizm zliczania referen- cji pozwala łatwo obsługiwać zasoby złożone, co w przypadku podejścia ad hoc jest wysoce uciążliwe. Generalnie stosowanie spójnego mechanizmu zarządzania zawartością jest w większości przypadków nieodzownym fundamentem, bez którego bardzo ciężko zbudować stabilną funkcjonalność gry. Intermezzo: co w Gem'ie piszczy? Zanim przejdziemy do sekcji Implementacja, kilka słów na temat zmian w bi- bliotece Gem. Silnik ten, pomimo tego, iż w porównaniu do produkcyjnych rozwiązań nadal pozostaje bardzo skromnym rozwiązaniem, od czasu kiedy pisałem poprzedni artykuł z serii Wzorce Programowania Gier przeszedł dość istotną metamorfozę. Poniżej przedstawiam podsumowanie najważniejszych zmian: »» Gem posiada teraz swój własny, niezależny od biblioteki SDL interfejs do renderowania sprzętowo wspomaganej grafiki 2D (Gem::Graphics). Interfejs ten pozwala póki co na rysowanie dwuwymiarowych obrazów z podstawowymi transformacjami (rotacja, skalowanie, podkolorowanie, odbicia) w czterech podstawowych trybach mieszania kolorów. Na Listin- gu 1 przedstawiona jest deklaracja jednej z metod klasy Gem::Graphics, odpowiedzialna za rysowanie tekstury. »» W związku z dodaniem do Gem interfejsu do renderowania tekstur, w bibliotece pojawił się szereg pomocniczych komponentów takich jak Rectangle, Vector2 czy Color. »» W czasie kiedy pisałem pierwszą część artykułu, środowisko Visual Studio C++ 2013 miało problem z kompilacją biblioteki Boost C++.W międzycza- sie pojawiła się nowa odsłona tej biblioteki (Boost C++ 1.55.0), w której
25/ www.programistamag.pl / Wzorce Programowania Gier: Zarządca Zawartości problem ten został rozwiązany. W związku z powyższym zdecydowałem się wykorzystać Boost'a przy implementacji biblioteki Gem. Najbardziej widocznymi zmianami w tym zakresie jest użycie klasy boost::variant do przekazywania obiektów reprezentujących zdarzenia generowanie przez system oraz pochodzące z kontrolerów (patrz nagłówki: Event.hpp oraz Input.hpp), co pozwoliło wyeliminować niebezpieczny mechanizm przekazywania argumentów w metodach zwrotnych obsługujących te zdarzenia jako typ void*. »» Do Gem dodany został system do obsługi testów jednostkowych (oparty na bibliotece Google Mock; https://code.google.com/p/googlemock/); póki co testów nie ma zbyt wiele, ale liczę, że w niedalekiej przyszłości to się zmieni. »» I wreszcie: Gem wzbogacił się o mechanizm zarządzania zasobami. Mecha- nizm ten opiszę szczegółowo w kolejnym podpunkcie niniejszego tekstu. Listing 1. Deklaracja metody Gem::Graphics::DrawTexture() virtual void DrawTexture(const Texture& texture, const Rectangle& destinationRectangle, const Rectangle* const sourceRectangle, const Color& color, float rotation, const Vector2& origin, DrawEffects drawEffects=DrawEffects::None, BlendMode blendMode=BlendMode::AlphaBlend) = 0; Przypominam również, że źródła biblioteki Gem udostępnione są na licencji MIT w serwisie BitBucket pod adresem https://bitbucket.org/rkocisz/gem/. Implementacja Zarządca Zasobów to jeden z tych wzorców projektowych, które rozważa- ne w ujęciu abstrakcyjnym wydają się być relatywnie proste, jednakże kie- dy rozważamy ich implementację, okazuje się, że przysłowiowy diabeł tkwi w szczegółach. W tym miejscu pokuszę się o krótką dygresję na temat implementacji mechanizmu zarządzania zasobami w języku C++. Implementacja ta nie jest specjalnie rozległa (w sensie ilości kodu źródłowego), jednakże łączy w sobie szereg niebanalnych właściwości tego języka, i prawdę mówiąc – do najprost- szych nie należy… Dlatego też proszę czytelników z mniejszym doświadcze- niem w C++ o cierpliwość i wyrozumiałość, a także o sporą dozę uwagi. Na początek zbadajmy definicję abstrakcyjnej klasy Gem::Asset, która zgodnie z założeniami przedstawionymi we wcześniejszej części artykułu bę- dzie stanowić bazę dla wszystkich komponentów reprezentujących zasoby obsługiwane przez naszego zarządcę. Listing 2 przedstawia plik nagłówkowy z definicją klasy, zaś Listing 3 zawiera implementację jej metod. Listing 2. Zawartość pliku Asset.hpp #ifndef GEM_ASSET_HPP #define GEM_ASSET_HPP #include#include namespace Gem
{
class Asset : private boost::noncopyable
{
public:
virtual ~Asset() = 0;
public:
virtual void Initialize();
};
typedef std::shared_ptr AssetPtr;
typedef std::weak_ptr AssetWPtr;
}
#endif
Listing 3. Zawartość pliku Asset.cpp
#include "gem/Asset.hpp"
namespace Gem
{
Asset::~Asset()
{
}
void Asset::Initialize()
{
}
}
Warto zwrócić uwagę, że Gem::Asset dziedziczy prywatnie po
boost::noncopyable, co oznacza, iż obiekty tej klasy nie podlegają stan-
dardowej semantyce kopiowania. Obiekty te będą tworzone zawsze na ster-
cie, a czas ich życia będzie kontrolowany przez standardowy, inteligentny
wskaźnik typu std::shared_ptr. Dlatego też w pliku nagłówkowym znaj-
dują się następujące definicje typów:
typedef std::shared_ptr AssetPtr;
typedef std::weak_ptr AssetWPtr;
Wirtualna metoda Gem::Asset::Initialize() służy do realizacji do-
datkowego przetwarzania zasobów tuż po ich wczytaniu do pamięci (ang.
load-initializing).
Przeanalizujmy, jak w Gem zaimplementowany jest najbardziej podsta-
wowy, atomowy zasób: tekstura. Ponieważ tekstura jest bardzo niskopozio-
mowym typem zasobu, stanowiącym jeden z punktów styku silnika Gem oraz
biblioteki SDL, postanowiłem w pełni odseparować jej interfejs od implemen-
tacji (to na wypadek, gdybym w przyszłości zechciał podmienić SDL'a na ja-
kieś inne rozwiązanie). W tym układzie interfejs tekstury reprezentowany jest
przez abstrakcyjną klasę Gem::Texture. Plik nagłówkowy tej klasy przedsta-
wiony jest na Listingu 4.
Listing 4. Zawartość pliku Texture.hpp
#ifndef GEM_TEXTURE_HPP
#define GEM_TEXTURE_HPP
#include "gem/Asset.hpp"
#include #include namespace Gem
{
class Texture : public Asset
{
public:
static AssetPtr Load(const std::string& path,
bool cache=false);
public:
virtual int Width() const = 0;
virtual int Height() const = 0;
};
typedef std::shared_ptrTexturePtr;
typedef std::shared_ptrConstTexturePtr;
}
#endif
Interfejs ten póki co jest bardzo prosty i pozwala odpytać się jedynie o sze-
rokość i wysokość tekstury (ten stan rzeczy zmieni się prawdopodobnie
w niedalekiej przyszłości). Kluczowa jest w tym przypadku statyczna meto-
da Gem::Texture::Load(), która dla zadanej ścieżki do pliku z zasobem
zwraca obiekt typu Gem::AssetPtr reprezentujący teksturę. Implementa-
cja tej funkcji (patrz: Listing 5) deleguje zadanie wczytania tekstury do funkcji
Gem::SdlTexture::Load().