Kiedy programiści się nudzą, to często spierają się o terminologię. Przedmiotem takich sporów są zwykle nieco mgliste określenia, które czasami – zwykle niesłusznie – używane są jako atrybuty oceniające. Przykład? Choćby “obiektowość” – w zależności od punktu widzenia stwierdzenie, że dany kod/biblioteka/moduł/itp. są bardziej lub mniej obiektowe może stanowić albo zaletę, albo wadę. Zależy to głównie od ‘poglądów’ danej osoby na temat użyteczności podejścia OO w programowaniu.
Co to jednak znaczy, że dany kod jest obiektowy?… Nie uważam wcale, że należy go w tym celu napisać go w języku obiektowym. Twierdzę na przykład, że Windows API jest całkiem dobrym przykładem biblioteki realizującej obiektowy paradygmat, mimo tego że została ona stworzona w jak najbardziej strukturalnym C. Praktyczna różnica między poniższymi wywołaniami:
jest bowiem właściwie żadna. Dodajmy do tego fakt, że z punktu widzenia programisty-użytkownika w WinAPI występuje zarówno dziedziczenie (rodzajów uchwytów), jak i polimorfizm (funkcje niezależne od typu uchwytów, np. CloseHandle
), a więc bardzo obiektowe feature‘y.
Jeśli komuś wydaje się to naciągane i twierdzi, że w ten sposób pod obiektowość można podciągnąć właściwie każdy kod, to już spieszę z przykładem ukazującym, że tak nie jest. Otóż większa część biblioteki OpenGL obiektowa na pewno nie jest, zaś w tych nielicznych miejscach gdzie OOP jest niemal konieczny (jak zarządzanie teksturami czy buforami wierzchołków) pojawia się nieco dziwna koncepcja ‘indeksów’ używanych do identyfikowania poszczególnych obiektów.
Dla niektórych (łącznie ze mną) taki interfejs nie jest szczytem marzeń, a preferowane jest wyraźnie obiektowe podejście DirectX. Absolutnie jednak nie zgadzam się z twierdzeniem, że DirectX jest lepszy, bo bardziej obiektowy – to trochę tak jak powiedzenie, że ten obrazek jest ładniejszy, bo bardziej zielony. W obu przypadkach jest to kwestia gustu i nie powinniśmy zakładać, że cechy pozytywne dla nas są tak samo dobrze odbierane przez innych.
A wyższość DirectX nad OpenGL da się przecież uargumentować na wiele innych, lepszych sposobów :)
Internet pełen jest opisów, tutoriali i przykładowych kodów pokazujących, jak implementować różne efekty graficzne. Zakodowanie ich pojedynczo zazwyczaj nie jest więc problemem, o ile mamy jako takie pojęcie o grafice czasu rzeczywistego, bibliotece DirectX/OpenGL i programowaniu w ogóle.
Znacznie większym problemem jest połączenie kilku(nastu/dziesięciu) efektów tak, by było one zaaplikowane w jednym momencie do tej samej sceny. Ze względu na to, że każdy pojedynczy efekt może wymagać kodu w bardzo różnych miejscach potoku graficznego (chociażby w samej aplikacji oraz w kodzie shaderów), zintegrowanie wszystkich tych fragmentów nie wydaje się sprawą prostą.
Ostatnio aczkolwiek zajmowałem się praktycznym rozwiązywaniem tych kwestii; było to łączenie różnych rodzajów oświetlenia z cieniami generowanymi techniką shadow depth mapping i efektami postprocessingu w rodzaju depth of field. Pozwolę więc sobie podzielić kilkoma uwagami na ten temat. To może jeszcze nie są rady, jak dobrze zaprojektować architekturę silnika 3D, ale mały framework pewnie można o nie oprzeć ;] A zatem:
DrawPrimitive
czy DrawSubset
są w tej samej funkcji co Begin/EndScene
? W rzeczywistym kodzie zapewne tak nie będzie, bo dana scena będzie na pewno renderowana wielokrotnie.WORLD
(lub MODELVIEW
w OpenGL), bo nasza scena będzie renderowana kilka razy w potencjalnie różnych widokach (kamery, światła, obiektu odbijającego otoczenie, itp.). Dodatkowo mogą być nam potrzebne punkty w różnych przestrzeniach, np. w układzie widoku obserwatora i widoku od konkretnego światła naraz. Wreszcie, nie należy zapominać o prawidłowym przekształcaniu wektorów normalnych. W sumie więc sekcja deklaracji pliku z shaderami może wyglądać np. tak:
Są tutaj jeszcze dwie sprawy warte zaznaczania. Po pierwsze, obiekty rysujące się na scenie muszą wiedzieć, gdzie ustawiać swoją macierz lokalnego przekształcenia. We wszystkich używanych shaderach nazwa odpowiedniej stałej (tutaj ObjectTransform
) musi być taka sama; najlepiej też żeby mapowała się na te same rejestry stałych cn
. Naturalnie kod renderujący obiekty musi też “wiedzieć”, żeby korzystać właśnie z niej zamiast z macierzy przekształceń z fixed pipeline – czyli np. wywoływać effect->SetMatrix("ObjectTransform", &mat);
zamiast device->SetTransform (D3DTS_WORLD, &(currWorld * mat));
w przypadku DirectX).
Po drugie, nie trzeba “dla efektywności” przekazywać do shadera iloczynów macierzy, jeśli używamy także ich poszczególnych czynników. Można bowiem zupełnie bezkarnie mnożyć je na początku kodu shadera:
Kompilator wydzieli ten kod w postaci tzw. preshadera i zapewni, że będzie on wykonywany tylko raz (a nie dla każdego wierzchołka/piksela).
"ShadowMap"
, "DepthMap
, "Scene"
itp.Ogólnie trzeba przyznać, że implementowanie wielu efektów działających naraz w tej samej scenie to zagadnienie złożone i dość trudne. Chociaż więc starałem się podać kilka porad na ten temat, to w rzeczywistości niezbędne jest tutaj spore doświadczenie z różnymi rodzajami efektów, zarówno w teorii jak i praktyce.
Obecnie biblioteki graficzne umieją już bardzo, bardzo dużo. W narzędziach takich jak DirectX czy OpenGL mamy ogromne możliwości efektywnego wyświetlania grafiki trójwymiarowej, a jednocześnie ich obsługa staje się z kolejnymi wersjami coraz łatwiejsza. Należy oczywiście poznać nieco matematycznych podstaw stojących za całą grafiką 3D: przekształceniami, oświetleniem, buforem głębi, i tak dalej. Zasadnicza część – czyli cały potok renderowania oraz rasteryzacja – jest już jednak wykonana za nas.
Powiedzmy jednak, że nie zadowala nas ten stan rzeczy i ze względów edukacyjnych – lub możliwych czynników zewnętrznych :) – chcemy sami dowiedzieć (w sposób najbardziej praktyczny, czyli poprzez implementację), jak to wszystko działa. Wówczas do szczęścia potrzebujemy właściwie tylko jednej operacji: wyświetlenia pojedynczego piksela w określonym kolorze. I jest tylko jedno ale: aby to wszystko działało w jakikolwiek sensowny sposób, operacja ta musi być diabelnie szybka.
Niestety (a może właśnie ‘stety’) czasy, w których łatwo możemy pisać bezpośrednio po pamięci ekranu, najwyraźniej już dawno się skończyły. Nowoczesny system operacyjny nie pozwala po prostu na takie niskopoziomowe operacje żadnym programom użytkownika. Z drugiej strony korzystanie z tego, co w zakresie wyświetlania grafiki ów system oferuje, może być niewystarczająco – pod względem wydajnościowym, rzecz jasna.
Dlatego wydaje się, że do eksperymentów dobrze nadają istniejące biblioteki graficzne – tyle że ograniczone do jednej funkcjonalności: renderowania sprite’ów punktowych (point sprites). Polega ona na rysowaniu quadów (kwadratów złożonych z dwóch trójkątów) w taki sposób, że zawsze są one zwrócone przodem do kamery. Biorąc pod uwagę to, że wielkość takich punktów możemy określić, ustawienie jej na równą jednemu pikselowi sprawi, że wyświetlanie sprite’ów punktowych będzie dobrym substytutem “stawiania” pojedyncznych pikseli.
Gdzie w zakamarkach bibliotek graficznych ukryta jest tego rodzaju funkcjonalność? Otóż:
D3DRS_POINTSCALEENABLE
na FALSE
(co zresztą jest domyślną wartością) oraz D3DRS_POINTSIZE
na 1.0f
, czyli wielkość odpowiadającą jednemu pikselowi. Co ciekawe, trzeci o nazwie D3RS_POINTSPRITEENABLE
wbrew pozorom lepiej jest zostawić ustawiony domyślnie na FALSE
. Nie dotyczy on bowiem (co jest dość mylące) samej możliwości rysowania sprite’ów punktowych, a jedynie sposobu mapowania współrzędnych tekstur – co w przypadku symulowania pikseli nie jest nam potrzebne.D3DFVF_XYZRHW
oraz D3DFVF_DIFFUSE
przy pomocy dowolnych wywołań DrawPrimitive[UP]
z typem prymitywów D3DPT_POINTLIST
.1.0f
). Jeśli zaś chodzi o samo rysowanie punktów to jednym ze sposobów (i pewnie nie najlepszym :)) jest ustawienie wszystkich macierzy na jednostkowe i podawanie współrzędnych punktów jako wektorów 4D z ustaloną współrzędną Z (np. 0) oraz W równą 1, do funkcji glVertex4*
. Musi być ona umieszczona oczywiście między glBegin
(z parametrem GL_POINTS
) a glEnd
, zaś kolor punktu-piksela określić możemy przez glColor*
.Jak widać, mechanizmy wspomagające nas w ręcznej, pikselowej robocie są nieco ukryte. Co więcej, w DirectX 10 na przykład sprite’y punktowe zostaną wyeliminowane z biblioteki i konieczne będzie samodzielne tworzenie odpowiednich quadów, jeśli zechcemy uzyskać podobną funkcjonalność. To prawdopodobnie słuszna decyzja, bowiem nawet w swoim głównym zastosowaniu – czyli systemach cząsteczkowych – są one zbyt ograniczone (choćby dlatego, że są cały czas zwrócone do kamery, co wyklucza możliwość obrotu cząstek). A przyznajmy, że samodzielne stawianie “pikseli” jest dość egzotyczne – co nie znaczy, że nie warto wiedzieć, jak to się robi, jeśli kiedykolwiek będzie to nam potrzebne…
Eksperymentując z grafiką 3D bardzo często zachodzi potrzeba napisania i przetestowania programu testującego jakąś konkretną technikę zrobienia czegoś – na przykład powierzchni odbijającej światło czy rzucania cieni. Można w tym celu stworzyć jakiś szablon przykładowego programu, który będziemy odpowiednio modyfikować. Nie będzie to zwykle wygodne, chyba że naprawdę się postaramy (czytaj: poświęcimy dużo czasu na rzecz nie do końca potrzebną).
Istnieje aczkolwiek inne rozwiązanie i jest nim skorzystanie z wyspecjalizowanej aplikacji, jak na przykład RenderMonkey stworzonej przez ATI i dostępnej za darmo. Jest to coś w rodzaju wirtualnego środowiska dla programowania grafiki (czyli shaderów) działającego jednak na zupełnie realnym urządzeniu – przy użyciu DirectX lub OpenGL.
Ma ono całkiem spore możliwości oraz udostępnia sporą wygodę w testowaniu różnych efektów opierających się na ustalonych parametrach. O ile w kodzie zmiana jakiejś wartości wymagałaby zwykle rekompilacji, o tyle tutaj można taką wartość (zmienną) odpowiednio oznaczyć i modyfikować jej wartość przy pomocy graficznego interfejsu, natychmiast podglądając rezultat.
Rzeczone zmienne ostatecznie mają po prostu odpowiedniki w rejestrach shaderów (pisanych dla DX w HLSL). Oprócz edytowania ich RenderMonkey umożliwia też dodawanie do projektów różnego rodzaju zasobów (modeli, tekstur, itp.) oraz ustawianie ich różnych stanów. Na dodatek w pakiecie z programem jest całkiem sporo takich właśnie przykładowych zasobów.
A wady? Cóż… Mogę wspomnieć o tym, że obecnie program wykorzystuje tylko DX9, czyli niestety nie pobawimy się tymi jakże użytecznymi geometry shaderami (oczywiście przy założeniu, że nasza karta potrafi się nimi bawić :)) Ale jest i dobra strona – nie trzeba instalować Visty ;P