dareks_

  • Dokumenty2 821
  • Odsłony753 730
  • Obserwuję431
  • Rozmiar dokumentów32.8 GB
  • Ilość pobrań361 988

Programista 2014 05

Dodano: 6 lata temu

Informacje o dokumencie

Dodano: 6 lata temu
Rozmiar :5.9 MB
Rozszerzenie:pdf

Programista 2014 05.pdf

dareks_ CZASOPISMA
Użytkownik dareks_ wgrał ten materiał 6 lata temu.

Komentarze i opinie (0)

Transkrypt ( 25 z dostępnych 80 stron)

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 magazy­nu 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().