Dobrą zasadą programowania obiektowego jest to, że każdy obiekt powinien w miarę możliwości dbać wyłącznie o siebie. Nazywamy to enkapsulacją. W przypadku obiektów na scenie chcielibyśmy więc, aby potrafiły one odrysować się same bez konieczności zakładania czegokolwiek choćby o stanie urządzenia przed wywołaniem metody Render
danego węzła sceny.
Obiekt do samodzielnego narysowania się może jednak potrzebować ustawienia wielu parametrów: stanów renderowania, tekstur czy nawet shaderów. Z jednej strony część z nich może się powtarzać, a z drugiej pobieranie ustawień bezpośrednio od urządzenia przed każdą potencjalną zmianą kosztowałoby o wiele za dużo.
Prostym rozwiązaniem jest tutaj tzw. menedżer stanów, czyli warstwa pośrednicząca. Obiekt ten zachowuje wszystkie już ustawione stany urządzenia i kontroluje każde żądanie zmiany z zapisanymi wcześniej wartościami. Faktyczny stan urządzenia jest modyfikowany tylko wówczas, gdy nowa wartość jest rzeczywiście różna od starej.
Stworzenie takiego menedżera pozwala więc odciążyć nieco urządzenie zajęte ciągłym przełączaniem stanów w przypadku, gdy zgodnie ze słuszną filozofią OOPu każdy węzeł sceny renderuje się, dbając wyłącznie o siebie. Jedyną niedogodnością jest to, że w naszym menedżerze należy skopiować większość interfejsu urządzenia DirectX – a przynajmniej tę jego część, którą używamy. Jest to jednak potrzebne, aby w połączeniu z sortowaniem fragmentów geometrii sceny względem materiałów uzyskać jako taką wydajność całości.
Tworzenie oprogramowania nie jest łatwe. Zwłaszcza, jeżeli mówimy tu o komercyjnym jego wytwarzaniu, gdzie mamy przecież “nieprzekraczalne” (przynajmniej w teorii) terminy i takież koszty. Dlatego właśnie powstała inżynieria oprogramowania, która ma na celu uczynienie tego procesu bardziej systematycznym i przewidywalnym.
Większość modeli zakłada jednak całe mnóstwo dodatkowej pracy, którą należy wykonać przed, po i w trakcie zasadniczej części tworzenia – czyli implementacji. Jest to głównie produkowanie różnej maści dokumentów, wypełnianie dziwnych tabelek, rysowanie wykresów rzekomo pomocnych w szacowaniu kosztów przyszłych przedsięwzięć, i tym podobne. Jakie daje to korzyści?… Chyba najlepiej odpowiada pewna obserwacja dotycząca jednej z metod szacowania kosztów właśnie. Otóż przy jej użyciu mamy “aż” 25% szansy na to, że nasze przewidywania będą się różniły od rzeczywistości “tylko” o co najwyżej 25%. Innymi słowy, w trzech przypadkach na cztery moglibyśmy równie dobrze wziąć wszystkie liczby z sufitu :)
Zatem komercyjne tworzenie oprogramowania to rzeczywiście sport dla odważnych, ale parę lat temu było dość głośno o pewnej technice, która paradoksalnie jest jak najdalsza od wszelkiego formalizmu na siłę wrzucanego do inżynierii oprogramowania. Nazywa się ona – nomen omen – programowaniem ekstremalnym (XP) i faktycznie jest dość drastyczna :) Prawdopodobnie jest ona najbardziej znana z tego, że programuje się w parach: jedna osoba pisze kod, a druga ją kontroluje, wnosi poprawki, zadaje pytania i odpowiada na nie, itd. Ważniejsze jest jednak to, że cała ta metodologia (tak, bardzo mądre słowo) jest chyba najbardziej “programistyczna” ze wszystkich – no może amatorskim kodzeniem ‘na hurra’. Tutaj przede wszystkim pisze się kod: kod testów – bardzo ważny, kod właściwy, kod służący wypróbowywaniu różnych rozwiązań, kod służący do komunikacji między członkami zespołu… Kod zresztą chyba zastępuje nawet projekt, który często nie jest zbyt szczegółowy albo nawet kompletny, lecz rodzi się z czasem. Najważniejsze jest tu bowiem pisanie kodu, realizowanie poszczególnych wytycznych tak, aby jak najszybciej cokolwiek działało i żeby potem okres “rozgrzebania” kodu między kolejnymi zmianami był jak najkrótszy. A żeby jeszcze mieć jako taką pewność, że wynikowy produkt działa, trzeba po prostu testować, testować i jeszcze raz testować – na każdym etapie.
Wygląda interesująco, prawda? Być może wydaje się, że to lekka przesada, że bez porządnego projektu dużo się nie zrobi – i pewnie rzeczywiście prawda jak zwykle leży pośrodku. Ale metody podobne do XP stosowano chyba powszechnie podczas… konkursów Compo organizowanych w czasie konferencji IGK. I niekiedy nawet osiągano zadowalające efekty :)
Może po prostu gdy się zbierze kilku programistów nieobciążonych wiedzą o ‘bardziej profesjonalnych’ metodach zarządzania projektami, to w naturalny sposób wybiorą taką właśnie metodę pracy. A to, że usiłuje się ją systematyzować i wykorzystywać w inżynierii, jest chyba korzyścią dla jednej i drugiej.
W programowaniu też trzeba często podejmować różne trudne decyzje; zwykle dotyczą one kwestii projektowych. Szczególnie złożone robią się one tam, gdzie idee OOPu muszą się spotkać ze sprowadzeniem na ziemię przez wymagania wydajnościowe obecne w programowaniu grafiki i interfejs graficznych API takich jak DirectX.
Na co więc tak narzekam? Otóż chodzi o odpowiednie opakowanie systemu materiałów. Jak wiadomo, materiał to jest taka cecha geometrii, która nie jest zapisana w danych o wierzchołkach – czyli właściwie wszystko, co może być w geometrii interesujące :) Materiał określa więc cechy powierzchni (gładka, bump-mapowana), właściwości świetlne albo nawet tak fundamentalne cechy jak półprzezroczystość lub renderowanie jako siatki (wireframe).
Zakodowanie takiego systemu materiałów zgodnie z regułami programowania obiektowego oznaczałoby przede wszystkim to, że część potoku odpowiedzialna za rysowanie nie musiałaby nawet wiedzieć, z jakiego dokładnie materiału korzysta dany fragment geometrii. Za pomocą metod wirtualnych można by bowiem albo pobrać odpowiednie ustawienia stanów renderowania, albo wręcz kazać materiałowi, aby sam je ustawił.
To da się zrobić. Sęk w tym, że wszystkie te dane trzeba przekazać do shadera, który już taki sprytny nie jest. W rzeczy samej, dopiero od niezbyt w sumie długiego czasu shadery można pisać w czymś, co przypomina język wysokiego poziomu, a o OOPie czy tym bardziej polimorfizmie nawet nie ma co marzyć. Chociaż… w HLSL już teraz do ewentualnego przyszłego użycia zarezerwowano słowo kluczowe class
:)
Na teraz trzeba jednak zdecydować się na rozwiązanie pośrednie, które mniej więcej zadowoli obie strony – elastyczną obiektowość i “sztywny” shader. Osobiście widzę trzy rozwiązania dla organizacji klas(y) materiałów w silniku:
Zapewne to ostatnie rozwiązanie jest najbardziej elastyczne, rzeźnickie i w ogóle “naj” – tyle że jest też najbardziej skomplikowane i zdecydowanie nie chciałbym się za nie zabierać już za pierwszym razem. Dlatego, jak sądzę, powinienem wybrać coś między opcją pierwsza a drugą. Obie są właściwie pewnymi odcieniami tego samego wariantu, w którym możliwe cechy materiału są po prostu wpisane na sztywno w kodzie – zarówno samej aplikacji, jak i shaderów.
W przypadku drugiej opcji istnieje naturalnie tradycyjny dylemat: czy dana cecha zasługuje tylko na osobne pole, czy może wymaga już nowej klasy pochodnej. Myślę jednak, że tutaj można zastosować dość prostą zasadę związania klasy z shaderem. Jeśli więc dwa materiały należą do tej samej klasy, to różnią się tylko wartościami stanów renderowania. Natomiast materiały z różnych klas różnią się samym zestawem stanów, jakie można dla nich ustawiać; potrzebują zatem innych shaderów.
Na dzisiaj mam drobną radę, jak poprawić wygląd kodu i wygodę pracy z nim, jeżeli akurat używamy któregoś z IDE (dla jakiegokolwiek języka) pod systemem Windows. Otóż w większości wypadków domyślną czcionką dla kodu, jaka jest w takich programach używana, jest Courier New. Ma ona oczywiście stałą szerokość znaków, ale właściwie na tym jej zalety się kończą… Zwłaszcza programiści pracujący czasem w Linuxie stwierdzić mogą, że fonty tam obecne są zdecydowane ładniejsze.
Dlatego do kodu polecam stosunkowo niedawny wynalazek Microsoftu, wprowadzony wraz z nowym Office’em – czcionkę Consolas. Jest ona wbudowana w Vistę, a jeśli używamy Windows XP, możemy się w nią zaopatrzyć na stronie MS. Wówczas do idealnego wyglądu powinniśmy jeszcze włączyć opcję wygładzania czcionek ClearType.
Przyznam, że kiedy kilka miesięcy temu zacząłem używać tej czcionki, dość trudno było mi się do niej przyzwyczaić. Teraz jednak na poczciwego Couriera patrzę już z dużym niesmakiem :) Consolas oprócz niewątpliwie większej wartości estetycznej ma też tę zaletę, że jej znaki są dość wąskie – a mimo to nadal doskonale czytelne. Sprawia to, że na ekranie mieszczą się linijki o znacznie większej długości, co dla programisty jest rzecz jasna bardzo wygodne.
Naturalnie nie twierdzę, że wszystkim akurat ta właśnie czcionka przypadnie do gustu. Zachęcam jednak, aby poeksperymentować z ustawieniami środowiska programistycznego, bo prawie zawsze można sobie choć trochę poprawić komfort kodowania.
W języku angielskim istnieje bardzo ciekawe słowo ‘serendipity‘. W skrócie, oznacza ono “umiejętność” dokonywania szczęśliwych i ważnych odkryć całkowicie przez przypadek – w szczególności wtedy, gdy tak naprawdę szukaliśmy czegoś zupełnie innego. Najciekawszą cechą tego słowa jest fakt, że bardzo trudno przełożyć je na język inny niż angielski przy pomocy czegoś krótszego niż podana wcześniej definicja (a już na pewno nie poprzez jeden wyraz). Dlatego też w 2004 roku znalazło się ono w czołówce najtrudniejszych do przetłumaczenia słów.
Czy w językach programowania możemy spotkać się z czymś podobny? Istnieją oczywiście tzw. idiomy, nakazujące by określone czynności robić tak, a nie inaczej – jak choćby słynny idom erase
–remove
z STL. Jednak tutaj chodzi mi raczej o taki element języka, który ułatwia życie albo po prostu jest w jakiś sposób nietypowy i – co najważniejsze – nie jest po prostu cukierkiem składniowym: jego przełożenie na inny język wymagałoby większej ilości kodu lub byłoby po prostu niemożliwe.
Jeżeli odpowiedź to pytanie jest twierdząca, to moimi osobistymi typami są:
for
w C++. Można ją rzecz jasna z powodzeniem symulować we wszystkich językach proceduralnych przy pomocy pętli typu while
, lecz jej elastyczność jest naprawdę zadziwiająca. Prawie komicznie wygląda pętla, w której wszystkie operacje wykonywane są w nagłówku, a jej zasadnicza treść jest pustym blokiemlist
, pozwalająca rozdzielić elementy tablicy pomiędzy ustalone zmienne za pomocą jednej instrukcji. Przydatna choćby przy przetwarzaniu rekordów baz danych czy parametrów GET i POST. Jej odpowiednikiem w innych językach jest po prostu odpowiednia seria przypisań.
Mid
z Visual Basica. Zasadniczo jej celem jest wyodrębnienie fragmentu łańcucha znaków i zwrócenie go, lecz może ona występować też po prawej stronie operatora przypisania. Wówczas służy ona do zastąpienia tegoż fragmentu innym tekstem, np.:
initalization
i finalization
w modułach Delphi. Zawarty w nich kod jest wykonywany jednokrotnie, odpowiednio: przy załadowaniu modułu oraz przy “sprzątaniu” w trakcie kończenia aplikacji. Nie jest to za bardzo przydatne w kodzie obiektowym – tym bardziej używającym odśmiecacza pamięci – ale podobny mechanizm występuje też w języku Python.x
, y
, z
lub w
) podajemy kilka:
Na pierwszy rzut oka może to wyglądać nieco zagadkowo, ale warto tej konstrukcji używać, gdyż karty graficzne dokonują swizzlingu albo za darmo, albo bardzo małym kosztem Zatem mamy tutaj elegancję, tajemniczość i efektywność w jednym, czyli to co programiści lubią najbardziej ;)
Ta lista na pewno jest o wiele za krótka. Jestem pewien, że programiści innych języków – tych co bardziej egzotycznych – mogliby dodać mnóstwo własnych typów.
Szablony to bardzo potężna część języka C++. Można przynajmniej powiedzieć, że spośród podobnych mechanizmów obecnych w wielu innych językach (jak np. typy generyczne z C# czy parametryzowane z Javy), daje on zdecydowanie największe możliwości. Ale, jak wiadomo, wszędzie można zawsze coś poprawić :)
Jednym z takich mankamentów jest choćby to, że póki co szablonowe – czyli opatrzone frazą template <...>
– mogą być tylko funkcje i klasy (pomijając dość specyficzny przypadek szablonowych deklaracji przyjaźni). To może się wydawać absolutnie wystarczające, ale istnieje jeszcze jeden potencjalny kandydat na “uszablonowienie”. Chodzi tu o deklarację typedef
.
Przydatność takiego ‘szablonowego typedef
‘ ukazuje się choćby w tym – przyznam że generalnie niezbyt mądrym – przykładzie tablicy dwuwymiarowej:
Nieco bardziej życiowy przypadek to zdefiniowanie własnego alokatora STL i chęć użycia go w standardowych kontenerach:
Widać, że w obu sytuacjach pewne fragmenty nazw typów powtarzają się, co zresztą sprawia, że nazwy stają się dość długie. Są to więc doskonali kandydaci do aliasowania poprzez typedef
, lecz jest tu jeden feler: w takim aliasie musimy dokładnie wyspecjalizować typ, któremu nadajemy nową nazwę. Nie można zatem zostawić sobie pewnej swobody w sposób, jaki się od razu narzuca:
Po prostu w aktualnej wersji C++ nie ma szablonowego typedef
. Obecnie można tylko zastosować pewien substytut, posługując się zwykłą definicją szablonu klasy:
która sprawi, że np. my_list
będzie naszą listą int
ów z niestandardowym alokatorem.
Niezbyt to zgrabne, ale na razie musi wystarczyć. Poprawy życia pod tym względem należy spodziewać wraz z C++0x, acz proponowana składnia “szablonowego typedef
” jest nieco inna od tej, którą zaprezentowałem wyżej.
W naturze gdy jakiś obiekt jest oświetlony, automatycznie powstaje za nim obszar cienia (a czasem i półcienia) tylko z tego powodu, że promienie świetlne są zatrzymywane przez ów przedmiot. Jest to w gruncie rzeczy bardzo proste zjawisko, lecz opiera się ono na śledzeniu torów promieni (raytracing) i dlatego wszelkie jego symulacje są kosztowne obliczeniowo. Oczywiście nie dotyczy to prawdziwych fotonów, bo one “same się śledzą” :)
Jeżeli więc chcemy mieć na scenie 3D coś, co przynajmniej stara się wyglądać jak prawdziwe cienie, musimy sami o nie zadbać. Sposobów na osiągnięcie tego celu jest oczywiście mnóstwo; różnią się one zarówno stopniem skomplikowania, jak i kosztem obliczeniowym czy “wspieralnością” przez różne karty graficzne.
Prawdopodobnie dwie najprostsze (i najszerzej obsługiwane) to:
Decyzja, którą z tych technik wybrać (czy może posłużyć się jeszcze jakąś inną), ma wbrew pozorom znacznie dalej idące konsekwencje niż tylko sam wygląd cieni czy szybkość ich generowania. W każdym przypadku inaczej wygląda bowiem ‘współpraca’ cieni z różnymi cechami materiałów, jak choćby półprzezroczystością czy mapowaniem nierówności.
Chwilowo bardziej skłaniam się ku shadow mapom z tegoż to ambitnego powodu, iż są prostsze w implementacji :) Wypadałoby jednak wpierw zrobić krótkie rozpoznanie ewentualnych problemów, jakie mogą z tego później wyniknąć…