Notki oznaczone tagiem ‘shadery’

Łączenie efektów graficznych

2010-01-27 9:29

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:

  • Należy wydzielić kod zajmujący się rysowaniem samych obiektów na scenie, gdyż będzie on wywoływany wielokrotnie. Niektórym może wydawać się to oczywiste, ale w ilu przykładowych kodach wywołania 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.
  • Trzeba odpowiednio zająć się macierzami przekształceń. Ważne jest na przykład wydzielenie w shaderze macierzy lokalnego przekształcenia każdego obiektu. Nie można jej po prostu złączyć z macierzą 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:
    float4x4 ObjectTransform; // przekszt. lokalne obiektu
    float4x4 CameraWorld; // przekszt. globalne sceny
    float4x4 CameraWorldRotation; // jw. ale z samą rotacją
    float4x4 CameraView; // przekszt. do przestrzeni widoku
    float4x4 CameraProjection; // przekszt. do przestrzeni rzutowania
    float4x4 LightWorldViewProjection; // przekst. do przestrzeni światła
    // itd.

    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:

    float4x4 CameraObjectWorld = mul(ObjectTransform, CameraWorld);
    float4x4 CameraWVP = mul(CameraObjectWorld, mul(CameraView, CameraProjection));
    // dalej reszta shadera
    Out.Position = mul(float4(In.Position, 1), CameraWVP);

    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).

  • Konieczne jest zadbanie o dobrą obsługę render targetów. Powinna być ona przezroczysta dla poszczególnych efektów - nie muszą one wiedzieć, czy renderują bezpośrednio na ekran, czy do tekstury. Jednocześnie każdy efekt powinien móc określić, do którego RT chce aktualnie renderować i mieć potem możliwość wykorzystania wyników jako tekstur w kolejnych przebiegach. Generalnie do tych celów wystarcza prosty menedżer oparty np. na słowniku identyfikującym poszczególne RT za pomocą nazw: "ShadowMap", "DepthMap, "Scene" itp.
  • W bardziej skomplikowanych przypadkach trzeba pewnie będzie złączyć shadery. W chwili obecnej jest to pewnie jeden z najbardziej złożonych problemów przy tworzeniu silnika graficznego, ale istnieje szansa, że wprowadzane w DirectX 11 dynamiczne linkowanie shaderów będzie to w istotny sposób ułatwiało.
    Jeśli na razie nie chcemy się mierzyć z tym problemem, to można niekiedy go ominąć kosztem dodatkowych przebiegów renderowania. Przykładowo, cienie można nakładać na gotową scenę z już policzonym oświetleniem zamiast oświetlać i cieniować piksele w jednym passie.

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.

  • Facebook
  • Twitter
  • Wykop
  • Reddit
  • del.icio.us
  • Google Bookmarks

Shadery i pliki efektów – sprostowanie

2009-12-11 16:58

Płynne przejście od programowania grafiki 3D przy pomocy fixed pipeline do wykorzystania shaderów nie jest z początku takie proste. Jest oczywiście w sieci mnóstwo tutoriali, które to ułatwiają. Zauważyłem jednak, że mają one tendencję do przekazywania naciąganych - mówiąc delikatnie - faktów na temat tego, w jaki sposób shadery oraz pliki efektów .fx (często omawiane łącznie) muszą być wykorzystywane w aplikacjach DirectX.
Dlatego pomyślałem sobie, że dobrze byłoby sprostować kilka mitów, jakie się tutaj pojawiają i wprowadzają zamieszanie do i tak niełatwej dziedziny programowania. Warto bowiem wiedzieć, iż to nieprawda, że:

  • ...korzystając z shaderów, musimy używać też plików .fx. Shader wierzchołków czy pikseli możemy samodzielnie wczytać z pliku (w wersji skomplikowanej lub przeprowadzić kompilację przy pomocy funkcji D3DX) czy nawet wygenerować dynamicznie, a potem zwyczajnie ustawić go jako aktualny VS/PS przy pomocy metod SetVertex/PixelShader urządzenia DirectX. Plik efektów nie jest wtedy do niczego potrzebny.
  • ...pliki .fx są tylko po to, by łatwiej wczytywać/aplikować shadery do renderowania. W rzeczywistości celem istnienia plików efektów jest uproszczenie kodowania różnych efektów graficznych w wersjach zależnych od możliwości poszczególnych kart. Zamiast samodzielnie sprawdzać capsy (możliwości) sprzętu, na którym uruchamia się nasza aplikacja, możemy po prostu pozwolić DirectX-owi wyszukać w pliku .fx tą wersję efektu (tzw. technikę), która na danej konfiguracji sprzętowej będzie działać. W ten sposób możemy na przykład napisać dwie wersje oświetlenia: jedną per pixel z użyciem shaderów i drugą, używającą fixed pipeline i oświetlającą wierzchołkowo. Nie musi ona wtedy korzystać z żadnych shaderów.
  • ...aby stosować shadery, musimy porzucić FVF na rzecz vertex declaration. Jest to piramidalna bzdura, która na szczęście rzadko bywa wygłaszana wprost, ale często wynika ze sposobu omawiania tematu VD w tutorialach. W istocie stałe D3DFVF tak samo dobrze opisują format danych dla vertex shaderów, jak deklaracje wierzchołków (IDirect3DVertexDeclarationX) i DirectX nie ma problemu z łączeniem jednego z drugim. Analogia działa zresztą też w drugą stroną: użycie fixed pipeline nie wymusza korzystania z FVF.
    Oczywiście bardziej zaawansowane techniki pisane z użyciem shaderów najczęściej potrzebują na wejściu danych, których w FVF zapisać się nie da (np. wektorów stycznych i binormalnych). Wtedy rzecz jasna potrzebne jest określenie formatu wierzchołków poprzez vertex declaration. Dla prostych VS-ów (jak choćby zwykłego m4x4 oPos, v0, c4) sam fakt korzystania z programowalnego potoku grafiki nic nie wymusza w zakresie formatu danych wierzchołków.
  • ...vertex i pixel shadery musimy zawsze stosować łącznie. To prawie logiczna konsekwencja częstego stwierdzenia (też niespecjalnie prawdziwego), że wyjście VS-a jest wejściem PS-a. W rzeczywistości nie jest to prawdą i łatwo można podać przykłady technik, w których możemy np. użyć pixel shadera bez vertex shadera - jak choćby zamiana wszystkich kolorów w scenie na odcienie szarości.
  • ...shadery zastępują cały potok renderowania. Chociaż oświetlenie, teksturowanie, transformacje macierzowe wierzchołków itp. to spory kawałek potoku renderowania, zastąpienie ich shaderami wcale nie oznacza, że nic już poza tym nie zostało. Rzeczy takie jak alpha blending, usuwanie tylnich ścian (culling), różne testy pikseli (głębokości, alfa, stencil) i przycinanie (np. płaszczyznami oraz scissor test) to tylko niektóre z elementów potoku, które są dostępne niezależnie od tego, czy obecne są w nim także załadowane przez użytkownika shadery.
  • Facebook
  • Twitter
  • Wykop
  • Reddit
  • del.icio.us
  • Google Bookmarks

Powtórka z DirectX

2009-10-29 20:58

Za sprawą przedmiotu o nazwie Grafika Komputerowa 3D musiałem ostatnio przypomnieć sobie, jak tak naprawdę i w praktyce koduje się w DirectX. Pewnie brzmi to dziwnie, ale w rzeczywistości przez ładnych kilka miesięcy nie pisałem większych ilości kodu, który by z tej biblioteki korzystał.

GK3D - screen
Piękna scena ;-)

Projekt, który musiałem teraz napisać, nie był ani trochę ambitny, bo polegał li tylko na wyświetleniu zupełnie statycznej sceny z kilkoma modelami, oświetleniu jej i zapewnieniu możliwości poruszania się w stylu strzelanek FPP. Oczywiście nie było też mowy o żadnych shaderach.

Niby banalne, ale jednak rzecz zajęła mi w sumie jakieś cztery znormalizowane wieczory (czyli od 3 do 4 godzin każdy). Częściowo było tak pewnie dlatego, że pokusiłem się jeszcze o teksturowanie, możliwość regulacji paru opcji renderowania czy bardzo, bardzo prosty menedżer sceny - czytaj: drzewko obiektów + stos macierzy ;)
Wydaje mi się jednak, że ważniejszą przyczyną był nieszczęsny fixed pipeline, którego byłem zmuszony używać. Jeszcze kilka lat temu nigdy bym nie przypuszczał, że to powiem, ale... shadery są po prostu łatwiejsze w użyciu. Porównując chociażby trywialne mieszanie koloru diffuse wierzchołka z teksturą przy użyciu pixel shadera:

psOut.color = psIn.diffuse * tex2D(tex0, psIn.Tex0)

oraz stanów urządzenia:

device->SetTextureStageState (0, D3DTSS_COLORARG1, D3DTA_CURRENT);
device->SetTextureStageState (0, D3DTSS_COLORARG2, D3DTA_TEXTURE);
device->SetTextureStageState (0, D3DTSS_COLOROP, D3DTOP_MODULATE);

nietrudno jest ocenić, w której znacznie lepiej widać, co faktycznie dzieje się z kolorem piksela. No, chyba że dla kogoś multum stałych w rodzaju D3DABC_SOMESTRANGEOPTION jest czytelniejsze niż po prostu mnożenie ;P

Inną sprawą jest też to, że w DirectX napisanie aplikacji od podstaw jest stosunkowo pracochłonne. Brak "złych" funkcji typu glVertex* ma rzecz jasna swoje zalety, lecz jest też jednym z powodów, dla których tak opłaca się posiadanie własnego frameworka, a może nawet i - tfu tfu - silnika ;-)

  • Facebook
  • Twitter
  • Wykop
  • Reddit
  • del.icio.us
  • Google Bookmarks

HLSL i kolorowanie składni

2007-11-25 21:38

Niby kod można pisać w Notatniku, ale własnej równowagi psychicznej chyba lepiej zaopatrzyć się w edytor, który oferuje przynajmniej podświetlanie elementów składniowych języka. Wiadomo przecież, że mnogość kolorów poprawia samopoczucie :)
Co więc zrobić, gdy zamierzamy pisać efekty w języku HLSL (lub bardzo podobnym Cg)? Trzeba zdecydować się na jakieś narzędzie. Możliwych jest kilka wyjść:

Jak widać, nie jesteśmy więc skazani na surową, czarno-białą czcionkę. A to dobrze, bo po dodaniu tej całej skomplikowanej matematyki, dziwnej semantyki dla danych wierzchołków i niezliczonych dyrektyw kompilacji warunkowej, kod shaderów jest już wystarczająco skomplikowany :)

  • Facebook
  • Twitter
  • Wykop
  • Reddit
  • del.icio.us
  • Google Bookmarks

Kompilowanie efektów w locie

2007-11-24 22:26

Model subtraktywny programowalnego potoku graficznego (nie ma to jak kilka trudnych słów na początek ;P) charakteryzuje się tym, że kody shaderów są w nim dość rozdęte objętościowo. Wynikową postać shadera otrzymuje się bowiem poprzez wybranie części kodu odpowiadającej aktualnym potrzebom związanym np. z materiałem i oświetleniem. Rzeczone części są wydzielone przy pomocy dyrektyw podobnych do preprocesora z C: #if, #else, itd.

Najprościej jest wtedy, gdy korzystamy z plików efektów (.fx) z DirectX. Wtedy można użyć funkcji D3DXCreateEffectFromFile lub D3DXCreateEffectFromFile, którym można przekazać wartości makr potrzebnych w danym przebiegu renderowania. Działa to tak, jakbyśmy użyli dyrektywy #define bezpośrednio w kodzie efektu i podobnie do makr definiowanych w wierszu poleceń kompilacji w przypadku normalnych programów.
Otrzymany w ten sposób skompilowany shader należy oczywiście zachować, aby można było szybko przełączać między potrzebnymi wersjami w czasie renderowania. Wciąż jednak wymaga to ponownej kompilacji wszystkich używanych wersji shadera przy każdym uruchomieniu aplikacji - co jest marnotrawieniem czasu, jeżeli plik z kodem efektu się nie zmienia.

Można coś na to poradzić, stosując interfejs ID3DXEffectCompiler zamiast zwykłego ID3DXEffect. Ten pierwszy ma bowiem dodatkową, bardzo przydatną metodę CompileEffect:

HRESULT ID3DXEffectCompiler::CompileEffect(
  DWORD Flags,
  LPD3DXBUFFER * ppEffect,
  LPD3DXBUFFER * ppErrorMsgs
);

W wyniku jej użycia możemy dostać bufor (czyli w gruncie rzeczy kawałek pamięci wypełniony danymi binarnymi) zawierający efekt w postaci skompilowanej. Najważniejsze jest to, że w tej postaci możemy zapisać go do pliku (zwykle z rozszerzeniem .fxo) i później tylko szybko odczytać - bez czasochłonnej rekompilacji. W ten sposób można stworzyć mechanizm cache'owania skompilowanych shaderów, który przyspieszy uruchamianie aplikacji.

  • Facebook
  • Twitter
  • Wykop
  • Reddit
  • del.icio.us
  • Google Bookmarks

Szare shadery

2007-11-19 19:58

Dawno, dawno temu - co w dziedzinie programowania grafiki oznacza perspektywę kilkuletnią - większość przetwarzania odbywała się we wbudowanym, stałym potoku graficznym. Shadery wprawdzie były, ale oferowane przez nie możliwości były dosyć ubogie (zwłaszcza jeśli chodzi o te do pikseli), a poza tym należało kodować je w niezbyt przyjaznym języku podobnym do asemblera. Pewnie dlatego, a dodatkowo także z powodu małego wsparcia kart graficznych, większość efektów realizowano przy pomocy odpowiednich tekstur, stanów ich faz i tym podobnych wynalazków. Bardzo często tak było po prostu łatwiej.
Zadziwiające jest to, że teraz nierzadko bywa dokładnie odwrotnie :) Chociaż na początku można jeszcze uważać, że ustawianie przeróżnych stanów stałego potoku i poleganie wbudowanie jest łatwiejsze, a shadery dają "tylko" większe możliwości, to jednak z czasem to myślenie zawsze powinno się zmienić. W końcu przecież napisanie algorytmu określającego dokładnie krok po kroku, co się dzieje z wierzchołkiem lub pikselem, dla programisty z założenia musi być łatwiejsze niż ustawianie jakichś dziwnych flag, będącymi swoistymi wytrychami do standardowego sposobu przetwarzania.

Zresztą nie wszystko można tymi wytrychami łatwo osiągnąć. Weźmy niezwykle prosty efekt wyświetlania w skali szarości, jak na czarno-białym zdjęciu. Jeżeli chodzi o realizację tego efektu na fixed pipeline, to chwilowo nie mam na to żadnego sensownego pomysłu - oczywiście poza podmianą wszystkich tekstur na ich 'szare' wersje.
A wystarczyłoby tylko użyć prostego pixel shadera, który jawnie implementuje wzór na obliczenie natężenia koloru:

float3 PS(float4 cl : COLOR0, /* itd */)
{
   float gray = 0.3f * cl.r + 0.59f * cl.g + 0.11f * cl.b;
   return float3(gray, gray, gray);
}

I już, efekt gotowy. Nietrudno byłoby też połączyć go z jakimkolwiek innym. W ogólności jest jednak 'trochę' trudniej - ale to już temat na inną okazję ;P

  • Facebook
  • Twitter
  • Wykop
  • Reddit
  • del.icio.us
  • Google Bookmarks

Trudny wybór: sprawa materiałów

2007-10-21 14:48

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:

  • Jedna duża klasa materiału, zawierająca pola z wartościami wszystkich możliwych ustawień. Jest więc flaga półprzezroczystości, wskaźnik na ewentualną normal-mapę, wskaźnik na tekstury powierzchni i tym podobne. Zaletą tego rozwiązania jest prostota (chociaż wynikowa klasa byłaby dość duża) i w łatwość "wyciągania" danych potrzebnych do sortowania fragmentów geometrii.
  • Kilka klas ułożonych w nieskomplikowaną hierarchię dziedziczenia. Jej 'rozpiętość pionowa' (możliwość dodawania kolejnych klas pochodnych) byłaby aczkolwiek pozorna, bo cała ta hierarchia musiałaby znaleźć odbicie w shaderze.
  • Jedna klasa materiału, składającego się z abstrakcyjnych "klocków" definiujących poszczególne jego cechy. To właśnie według nich odbywałoby się sortowanie geometrii. Konsekwencją jest jednak to, że w podobny (addytywny) sposób musiałby też być (s)konstruowany shader.

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.

  • Facebook
  • Twitter
  • Wykop
  • Reddit
  • del.icio.us
  • Google Bookmarks
 



© 2010 Karol Kuczmarski "Xion". Layout by Urszulka. Powered by WordPress with QuickLaTeX.com.