Łą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:
    1. float4x4 ObjectTransform; // przekszt. lokalne obiektu
    2. float4x4 CameraWorld; // przekszt. globalne sceny
    3. float4x4 CameraWorldRotation; // jw. ale z samą rotacją
    4. float4x4 CameraView; // przekszt. do przestrzeni widoku
    5. float4x4 CameraProjection; // przekszt. do przestrzeni rzutowania
    6. float4x4 LightWorldViewProjection; // przekst. do przestrzeni światła
    7. // 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:

    1. float4x4 CameraObjectWorld = mul(ObjectTransform, CameraWorld);
    2. float4x4 CameraWVP = mul(CameraObjectWorld, mul(CameraView, CameraProjection));
    3. // dalej reszta shadera
    4. 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.



6 comments for post “Łączenie efektów graficznych”.
  1. Tomasz Dąbrowski:
    January 27th, 2010 o 21:54

    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”

    Operacje na słownikach i/lub stringach są fajne dopóki masz zapas mocy CPU. W praktyce ścieżki renderowaine hardcoded są sporo szybsze niż “uniwersalny silnik”.

  2. Reg:
    January 31st, 2010 o 17:02

    float4x4 CameraWorldRotation jest bez sensu. Do przekształcania wektorów przez macierz (w przeciwieństwie do punktów), czyli bez uwzględniania translacji, wystarczy wykorzystać podmacierz 3×3 danej macierzy, tzn.:

    float3 normalWorld = mul(normalLocal, (float3x3)cameraWorld));

  3. Charibo:
    January 31st, 2010 o 21:54

    “wywołań Begin/EndScene będzie na pewno więcej niż jedno.”

    Używanie więcej niż jednej pary Begin/EndScene w pojedyńczym przebiegu pętli realtime (klatce) też trochę nie ma sensu. Każda operacja może zawrzeć się między nimi i nie trzeba wiele razy ich wywoływać :)

    “float4x4 CameraWVP = mul(CameraObjectWorld, mul(CameraView, CameraProjection));”

    A to już dosyć zły pomysł. Ogólnie, w tej sytuacji mnożenie przez siebie trzech macierzy w shaderze to najgorsze, co możesz z nimi zrobić. :) A kompilator HLSL (jak sprawdzałem w SDK z Marca 09) nie potrafi tego zoptymalizować :)

  4. Xion:
    January 31st, 2010 o 22:18

    @Charibo: Hmm byłem przekonany, że trzeba je wywoływać dla każdego RT. Jeśli nie, tym lepiej ;)

  5. Reg:
    February 7th, 2010 o 12:32

    Wywoływanie wielu BeginScene i EndScene na klatkę to jest błąd!!! Dokumentacja wyraźnie mówi w opisie funkcji BeginScene:

    “There should be one IDirect3DDevice9::BeginScene/IDirect3DDevice9::EndScene pair between any successive calls to present”

  6. Xion:
    February 7th, 2010 o 20:37

    O, istotnie. Poprawiłem już odpowiedni fragment w notce – dzięki za to sprostowanie!

Comments are disabled.
 


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