Game Engine Design - Ubershader - Projekt zarządzania Shaderem [zamknięte]


18

Chcę wdrożyć elastyczny system Ubershader z odroczonym cieniowaniem. Moim obecnym pomysłem jest tworzenie modułów cieniujących z modułów, które obsługują niektóre funkcje, takie jak FlatTexture, BumpTexture, Mapowanie przemieszczenia itp. Istnieje również niewiele modułów, które dekodują kolory, mapują tony itp. Ma to tę zaletę, że mogę to zrobić wymienić niektóre typy modułów, jeśli GPU ich nie obsługuje, więc mogę dostosować się do bieżących możliwości GPU. Nie jestem pewien, czy ten projekt jest dobry. Obawiam się, że mógłbym teraz dokonać złego wyboru projektu, a później za to zapłacić.

Moje pytanie brzmi: gdzie znajdę zasoby, przykłady, artykuły na temat skutecznego wdrażania systemu zarządzania modułami cieniującymi? Czy ktoś wie, jak to robią wielkie silniki gier?


3
Niewystarczająco długo, aby uzyskać prawdziwą odpowiedź: przy takim podejściu poradzisz sobie dobrze, jeśli zaczniesz od małego i pozwolisz, by rozwijał się organicznie według twoich potrzeb, zamiast próbować zbudować MegaCity-One z shaderów z przodu. Po pierwsze, łagodzisz swoje największe obawy związane ze zbyt dużym projektowaniem i płaceniem za nie później, jeśli się nie uda, po drugie, unikasz wykonywania dodatkowej pracy, która nigdy się nie przyda.
Patrick Hughes

Niestety, nie przyjmujemy już pytań dotyczących „żądania zasobów”.
Gnemlock,

Odpowiedzi:


23

Częściowo powszechne podejście polega na tworzeniu komponentów , które nazywam modułami cieniującymi , podobnych do modułów, które nazywam modułami.

Pomysł jest podobny do wykresu przetwarzania końcowego. Piszesz fragmenty kodu modułu cieniującego, który zawiera zarówno niezbędne dane wejściowe, wygenerowane dane wyjściowe, a następnie kod do faktycznej pracy na nich. Masz listę wskazującą, które moduły cieniujące należy zastosować w dowolnej sytuacji (czy ten materiał wymaga komponentu mapowania wypukłości, czy włączony jest komponent odroczony, czy przekazywany dalej itp.).

Możesz teraz pobrać ten wykres i wygenerować z niego kod modułu cieniującego. Oznacza to głównie „wklejenie” kodu fragmentów na miejsce, przy czym wykres upewnił się, że są już w odpowiedniej kolejności, a następnie wklejenie odpowiednio do wejść / wyjść modułu cieniującego (w GLSL oznacza to zdefiniowanie „globalnego” w , out i zmienne jednolite).

To nie to samo, co podejście ubershadera. Ubershadery to miejsce, w którym umieszczasz cały kod potrzebny do wszystkiego w jednym zestawie modułów cieniujących, być może używając #ifdefs i mundurów itp. Do włączania i wyłączania funkcji podczas ich kompilacji lub uruchamiania. Osobiście gardzę podejściem ubershadera, ale wykorzystują je niektóre dość imponujące silniki AAA (szczególnie Crytek).

Fragmenty modułu cieniującego można obsługiwać na kilka sposobów. Najbardziej zaawansowanym sposobem - i przydatnym, jeśli planujesz wspierać GLSL, HLSL i konsole - jest napisanie parsera dla języka shadera (prawdopodobnie tak zbliżonego do HLSL / Cg lub GLSL, jak to możliwe dla maksymalnej „zrozumiałości” przez twoich twórców ), które można następnie wykorzystać do tłumaczeń między źródłami. Innym podejściem jest po prostu zawijanie fragmentów shaderów w plikach XML itp., Np

<shader name="example" type="pixel">
  <input name="color" type="float4" source="vertex" />
  <output name="color" type="float4" target="output" index="0" />
  <glsl><![CDATA[
     output.color = vec4(input.color.r, 0, 0, 1);
  ]]></glsl>
</shader>

Zauważ, że dzięki takiemu podejściu możesz utworzyć wiele sekcji kodu dla różnych interfejsów API lub nawet zaktualizować sekcję kodu (dzięki czemu możesz mieć wersję GLSL 1.20 i GLSL 3.20). Twój wykres może nawet automatycznie wykluczyć fragmenty cieniowania, które nie mają zgodnej sekcji kodu, dzięki czemu możesz uzyskać pół-wdzięczną degradację na starszym sprzęcie (więc coś takiego jak normalne mapowanie lub cokolwiek, co jest po prostu wykluczone na starszym sprzęcie, który nie może go obsłużyć bez programisty potrzebującego wykonaj kilka jawnych kontroli).

Próbka XMl może następnie wygenerować coś podobnego do (przeprosiny, jeśli jest to nieprawidłowy GLSL, minęło trochę czasu, odkąd poddałem się temu API):

layout (location=0) in vec4 input_color;
layout (location=0) out vec4 output_color;

struct Input {
  vec4 color;
};
struct Output {
  vec4 color;
}

void main() {
  Input input;
  input.color = input_color;
  Output output;

  // Source: example.shader
#line 5
  output.color = vec4(input.color.r, 0, 0, 1);

  output_color = output.color;
}

Możesz być trochę mądrzejszy i generować bardziej „wydajny” kod, ale szczerze mówiąc, każdy kompilator modułu cieniującego, który nie jest całkowitą bzdurą, usunie dla ciebie nadmiarowość z wygenerowanego kodu. Być może nowsza wersja GLSL pozwala teraz także na umieszczanie nazwy pliku w #linepoleceniach, ale wiem, że starsze wersje są bardzo wadliwe i nie obsługują tego.

Jeśli masz wiele fragmentów, ich dane wejściowe (które nie są dostarczane jako dane wyjściowe przez fragment przodka w drzewie) są konkatenowane do bloku wejściowego, podobnie jak dane wyjściowe, a kod jest po prostu konkatenowany. Wykonano trochę dodatkowej pracy, aby zapewnić dopasowanie etapów (wierzchołek vs fragment) i że układy wejściowe atrybutów wierzchołków „po prostu działają”. Inną zaletą tego podejścia jest to, że możesz pisać jednoznaczne i jednolite wskaźniki wiązania atrybutów wejściowych, które nie są obsługiwane w starszych wersjach GLSL i obsługiwać je w bibliotece generowania / wiązania modułu cieniującego. Podobnie możesz używać metadanych do konfigurowania VBO i glVertexAttribPointerpołączeń, aby zapewnić kompatybilność i że wszystko „po prostu działa”.

Niestety nie ma już takiej dobrej biblioteki między interfejsami API. Cg jest dość blisko, ale ma bzdurną obsługę OpenGL na kartach AMD i może być wyjątkowo powolny, jeśli używasz tylko podstawowych funkcji generowania kodu. Framework efektów DirectX też działa, ale oczywiście nie ma wsparcia dla żadnego języka oprócz HLSL. Istnieją pewne niekompletne / błędne biblioteki GLSL, które naśladują biblioteki DirectX, ale biorąc pod uwagę ich stan przy ostatnim sprawdzeniu, po prostu napiszę własne.

Podejście ubershader oznacza po prostu zdefiniowanie „dobrze znanych” dyrektyw preprocesora dla niektórych funkcji, a następnie rekompilację różnych materiałów o różnej konfiguracji. np. dla dowolnego materiału z normalną mapą, którą możesz zdefiniować, USE_NORMAL_MAPPING=1a następnie w swoim pikselowym ubershaderze masz po prostu:

#if USE_NORMAL_MAPPING
  vec4 normal;
  // all your normal mapping code
#else
  vec4 normal = normalize(in_normal);
#endif

Dużym problemem jest tutaj obsługa tego wstępnie skompilowanego HLSL, gdzie trzeba wstępnie skompilować wszystkie używane kombinacje. Nawet z GLSL musisz być w stanie poprawnie wygenerować klucz wszystkich używanych dyrektyw preprocesora, aby uniknąć ponownej kompilacji / buforowania identycznych shaderów. Używanie mundurów może zmniejszyć złożoność, ale w przeciwieństwie do mundurów preprocesora nie zmniejsza liczby instrukcji i nadal może mieć niewielki wpływ na wydajność.

Żeby było jasne, oba podejścia (a także po prostu ręczne pisanie ton różnych shaderów) są używane w przestrzeni AAA. Użyj tej, która najbardziej Ci odpowiada.

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.