Udostępnianie kodu między wieloma modułami cieniującymi GLSL

30

Często zdarza mi się kopiować i wklejać kod między kilkoma modułami cieniującymi. Obejmuje to zarówno pewne obliczenia lub dane współdzielone między wszystkimi modułami cieniującymi w jednym potoku, a także wspólne obliczenia, których potrzebują wszystkie moje moduły cieniujące wierzchołki (lub dowolny inny etap).

Oczywiście, to okropna praktyka: jeśli muszę zmienić kod gdziekolwiek, muszę upewnić się, że zmienię go gdzie indziej.

Czy istnieje sprawdzona najlepsza praktyka utrzymywania DRY ? Czy ludzie po prostu przygotowują jeden wspólny plik dla wszystkich swoich shaderów? Czy piszą własny podstawowy procesor w stylu C, który analizuje #includedyrektywy? Jeśli w branży są akceptowane wzorce, chciałbym je stosować.

Martin Ender
źródło
4
To pytanie może być nieco kontrowersyjne, ponieważ kilka innych witryn SE nie chce pytań o najlepsze praktyki. To jest celowe, aby zobaczyć, jak ta społeczność stoi wobec takich pytań.
Martin Ender,
2
Hmm, wygląda mi dobrze. Powiedziałbym, że jesteśmy w dużym stopniu nieco „szersi” / „bardziej ogólni” w naszych pytaniach niż, powiedzmy, StackOverflow.
Chris mówi Przywróć Monikę
2
StackOverflow zmienił się z „zapytaj nas” w „nie pytaj nas, chyba że musisz ”.
wnętrza w dniu
Jeśli ma to na celu określenie tematu, to co powiesz na powiązane pytanie Meta?
SL Barth - Przywróć Monikę
2
Dyskusja na temat meta.
Martin Ender,

Odpowiedzi:

18

Istnieje wiele podejść, ale żadne nie jest idealne.

Możliwe jest współdzielenie kodu za pomocą glAttachShaderłączenia shaderów, ale nie pozwala to na współdzielenie takich rzeczy jak deklaracje struktur lub #definestałe -d. Działa w przypadku funkcji udostępniania.

Niektórzy ludzie lubią wykorzystywać tablicę ciągów przekazywanych glShaderSourcejako sposób na przygotowanie wspólnych definicji przed kodem, ale ma to pewne wady:

  1. Trudniej jest kontrolować, co musi być zawarte w module cieniującym (potrzebujesz do tego osobnego systemu).
  2. Oznacza to, że autor modułu cieniującego nie może określić GLSL z #versionpowodu następującej instrukcji w specyfikacji GLSL:

#Version dyrektywa musi nastąpić w shader zanim cokolwiek innego, z wyjątkiem uwag i białej przestrzeni.

Ze względu na to oświadczenie glShaderSourcenie można używać do dodawania tekstu przed #versiondeklaracjami. Oznacza to, że #versionwiersz musi zostać uwzględniony w glShaderSourceargumentach, co oznacza, że ​​interfejs kompilatora GLSL musi w jakiś sposób zostać poinformowany, jakiej wersji GLSL należy użyć. Dodatkowo, brak określenia a #versionspowoduje, że kompilator GLSL będzie domyślnie używał GLSL w wersji 1.10. Jeśli chcesz pozwolić autorom modułu cieniującego na określenie #versionskryptu w standardowy sposób, musisz w jakiś sposób wstawić #include-s po #versioninstrukcji. Można to zrobić poprzez jawne parsowanie modułu cieniującego GLSL w celu znalezienia #versionciągu (jeśli jest obecny) i wykonania po nim inkluzji, ale mając dostęp do#includeZaleca się łatwiejsze kontrolowanie, gdy konieczne jest dokonanie tych inkluzji. Z drugiej strony, ponieważ GLSL ignoruje komentarze przed #versionwierszem, możesz dodać metadane dla uwzględnienia w komentarzach u góry pliku (fuj.)

Pytanie brzmi teraz: czy istnieje standardowe rozwiązanie #include, czy też trzeba stworzyć własne rozszerzenie preprocesora?

Jest GL_ARB_shading_language_includerozszerzenie, ale ma pewne wady:

  1. Jest obsługiwany tylko przez NVIDIA ( http://delphigl.de/glcapsviewer/listreports2.php?listreportsbyextension=GL_ARB_shading_language_include )
  2. Działa poprzez określenie z wyprzedzeniem łańcuchów dołączania. Dlatego przed kompilacją musisz określić, że łańcuch "/buffers.glsl"(tak jak jest używany w #include "/buffers.glsl") odpowiada zawartości pliku buffer.glsl(który wcześniej załadowałeś).
  3. Jak zapewne zauważyłeś w punkcie (2), twoje ścieżki muszą zaczynać "/", podobnie jak ścieżki absolutne w stylu Linux. Ta notacja jest na ogół nieznana programistom języka C i oznacza, że ​​nie można określić ścieżek względnych.

Typowym projektem jest implementacja własnego #includemechanizmu, ale może to być trudne, ponieważ trzeba również przeanalizować (i ocenić) inne instrukcje preprocesora, takie jak #ifw celu prawidłowej obsługi kompilacji warunkowej (np. Osłony nagłówków).

Jeśli wdrożysz własne #include, masz także pewne swobody w tym, jak chcesz je wdrożyć:

  • Możesz przekazać ciągi z wyprzedzeniem (jak GL_ARB_shading_language_include).
  • Można określić funkcję zwrotną dołączania (czynność tę wykonuje biblioteka D3DCompiler biblioteki DirectX).
  • Można zaimplementować system, który zawsze odczytuje bezpośrednio z systemu plików, jak ma to miejsce w typowych aplikacjach C.

Upraszczając, możesz automatycznie wstawiać osłony nagłówków dla każdego z nich w warstwie przetwarzania wstępnego, dzięki czemu warstwa procesora wygląda następująco:

if (#include and not_included_yet) include_file();

(Podziękowania dla Trenta Reeda za pokazanie mi powyższej techniki.)

Podsumowując , nie ma automatycznego, standardowego i prostego rozwiązania. W przyszłym rozwiązaniu można użyć interfejsu SPIR-V OpenGL, w którym to przypadku kompilator GLSL na SPIR-V może znajdować się poza interfejsem GL API. Posiadanie kompilatora poza środowiskiem wykonawczym OpenGL znacznie upraszcza implementację takich rzeczy, #includeponieważ jest to bardziej odpowiednie miejsce do komunikacji z systemem plików. Uważam, że obecną szeroko rozpowszechnioną metodą jest po prostu wdrożenie niestandardowego preprocesora, który działa w sposób, który powinien znać każdy programista C.

Nicolas Louis Guillemot
źródło
Shadery można również podzielić na moduły za pomocą glslify , choć działa tylko z node.js.
Anderson Green
9

Ogólnie używam po prostu faktu, że glShaderSource (...) akceptuje tablicę ciągów jako dane wejściowe.

Korzystam z pliku definicji modułu cieniującego opartego na Jsonie, który określa sposób komponowania modułu cieniującego (lub programu, aby być bardziej poprawnym), i tam określam definicje preprocesora, które mogą być potrzebne, mundury, których używa, plik cieniowania wierzchołków / fragmentów, i wszystkie dodatkowe pliki „zależności”. To tylko zbiory funkcji dołączane do źródła przed faktycznym źródłem modułu cieniującego.

Aby dodać, AFAIK, Unreal Engine 4 wykorzystuje dyrektywę #include, która jest analizowana i dołącza wszystkie odpowiednie pliki przed kompilacją, jak sugerowałeś.

Matteo Bertello
źródło
4

Nie sądzę, aby istniała wspólna konwencja, ale jeśli zgadnę, powiedziałbym, że prawie wszyscy wdrażają jakąś prostą formę włączania tekstu jako etap wstępnego przetwarzania ( #includerozszerzenie), ponieważ jest to bardzo łatwe do zrobienia więc. (W JavaScript / WebGL możesz to zrobić na przykład za pomocą prostego wyrażenia regularnego). Zaletą tego jest to, że można wykonać wstępne przetwarzanie w kroku offline dla kompilacji „release”, gdy kod modułu cieniującego nie musi już być zmieniany.

W rzeczywistości, wskazanie, że takie podejście jest wspólną cechą jest fakt, że rozszerzenie ARB został wprowadzony do tego: GL_ARB_shading_language_include. Nie jestem pewien, czy stało się to teraz podstawową funkcją, czy nie, ale rozszerzenie zostało napisane przeciwko OpenGL 3.2.

glampert
źródło
2
GL_ARB_shading_language_include nie jest podstawową funkcją. W rzeczywistości obsługuje go tylko NVIDIA. ( delphigl.de/glcapsviewer/… )
Nicolas Louis Guillemot
4

Niektóre osoby już zauważyły, że glShaderSourcemogą przyjmować tablicę ciągów.

Ponadto w GLSL kompilacja ( glShaderSource, glCompileShader) i łączenie ( glAttachShader, glLinkProgram) modułu cieniującego jest osobne.

Użyłem tego w niektórych projektach do dzielenia shaderów pomiędzy określoną część i części wspólne dla większości shaderów, które następnie są kompilowane i udostępniane wszystkim programom shaderów. Działa to i nie jest trudne do wdrożenia: wystarczy utrzymywać listę zależności.

Jeśli chodzi o łatwość konserwacji, nie jestem pewien, czy to wygrana. Obserwacja była taka sama, spróbujmy rozłożyć na czynniki. Chociaż w rzeczywistości unika się powtórzeń, narzut techniki wydaje się znaczący. Co więcej, ostateczny moduł cieniujący jest trudniejszy do wyodrębnienia: nie można po prostu połączyć źródeł modułu cieniującego, ponieważ deklaracje kończą się w kolejności, w której niektóre kompilatory odrzucą lub zostaną zduplikowane. Dlatego trudniej jest wykonać szybki test modułu cieniującego w oddzielnym narzędziu.

W końcu ta technika rozwiązuje niektóre problemy z OSUSZANIEM, ale jest daleka od ideału.

Na pewien poboczny temat nie jestem pewien, czy to podejście ma jakikolwiek wpływ na czas kompilacji; Czytałem, że niektóre sterowniki kompilują program cieniujący tylko podczas łączenia, ale nie mierzyłem.

Julien Guertault
źródło
Z mojego zrozumienia uważam, że nie rozwiązuje to problemu udostępniania definicji struktur.
Nicolas Louis Guillemot,
@NicolasLouisGuillemot: tak masz rację, tylko kod instrukcji jest udostępniany w ten sposób, a nie deklaracje.
Julien Guertault