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 #include
dyrektywy? Jeśli w branży są akceptowane wzorce, chciałbym je stosować.
Odpowiedzi:
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#define
stałe -d. Działa w przypadku funkcji udostępniania.Niektórzy ludzie lubią wykorzystywać tablicę ciągów przekazywanych
glShaderSource
jako sposób na przygotowanie wspólnych definicji przed kodem, ale ma to pewne wady:#version
powodu następującej instrukcji w specyfikacji GLSL:Ze względu na to oświadczenie
glShaderSource
nie można używać do dodawania tekstu przed#version
deklaracjami. Oznacza to, że#version
wiersz musi zostać uwzględniony wglShaderSource
argumentach, 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#version
spowoduje, ż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#version
skryptu w standardowy sposób, musisz w jakiś sposób wstawić#include
-s po#version
instrukcji. Można to zrobić poprzez jawne parsowanie modułu cieniującego GLSL w celu znalezienia#version
ciągu (jeśli jest obecny) i wykonania po nim inkluzji, ale mając dostęp do#include
Zaleca się łatwiejsze kontrolowanie, gdy konieczne jest dokonanie tych inkluzji. Z drugiej strony, ponieważ GLSL ignoruje komentarze przed#version
wierszem, 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_include
rozszerzenie, ale ma pewne wady:"/buffers.glsl"
(tak jak jest używany w#include "/buffers.glsl"
) odpowiada zawartości plikubuffer.glsl
(który wcześniej załadowałeś)."/"
, 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
#include
mechanizmu, ale może to być trudne, ponieważ trzeba również przeanalizować (i ocenić) inne instrukcje preprocesora, takie jak#if
w 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ć:GL_ARB_shading_language_include
).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:
(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,
#include
ponieważ 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.źródło
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ś.
źródło
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 (
#include
rozszerzenie), 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.źródło
Niektóre osoby już zauważyły, że
glShaderSource
mogą 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.
źródło