Ich finde mich oft dabei, Code zwischen mehreren Shadern einzufügen. Dies umfasst sowohl bestimmte Berechnungen oder Daten, die von allen Shadern in einer einzelnen Pipeline gemeinsam genutzt werden, als auch allgemeine Berechnungen, die alle meine Vertex-Shader benötigen (oder eine andere Phase).
Das ist natürlich eine schreckliche Praxis: Wenn ich den Code irgendwo ändern muss, muss ich sicherstellen, dass ich ihn überall anders ändere.
Gibt es eine bewährte Methode für die Aufbewahrung von DRY ? Stellen die Benutzer allen Shadern nur eine einzige gemeinsame Datei voran? Schreiben sie ihren eigenen rudimentären Präprozessor im C-Stil, der #include
Direktiven analysiert ? Wenn es in der Branche akzeptierte Muster gibt, würde ich ihnen gerne folgen.
Antworten:
Es gibt eine Reihe von Ansätzen, aber keiner ist perfekt.
Es ist möglich, Code gemeinsam zu nutzen
glAttachShader
, indem Shader kombiniert werden. Dies macht es jedoch nicht möglich, Dinge wie Strukturdeklarationen oder#define
-d-Konstanten gemeinsam zu nutzen. Es funktioniert für das Teilen von Funktionen.Einige Leute möchten das Array von Zeichenfolgen, an das übergeben wird, verwenden
glShaderSource
, um allgemeine Definitionen vor Ihren Code zu stellen. Dies hat jedoch einige Nachteile:#version
aufgrund der folgenden Anweisung in der GLSL-Spezifikation nicht angeben kann :glShaderSource
Kann aufgrund dieser Aussage nicht verwendet werden, um Text vor den#version
Deklarationen voranzustellen . Dies bedeutet, dass die#version
Zeile in IhrenglShaderSource
Argumenten enthalten sein muss, was bedeutet, dass Ihrer GLSL-Compiler-Schnittstelle irgendwie mitgeteilt werden muss, welche Version von GLSL verwendet werden soll. Wenn Sie kein angeben#version
, verwendet der GLSL-Compiler standardmäßig die GLSL-Version 1.10. Wenn Sie möchten, dass Shader-Autoren das#version
innerhalb des Skripts auf standardmäßige Weise angeben , müssen Sie#include
nach der#version
Anweisung irgendwie -s einfügen . Dies könnte geschehen, indem der GLSL-Shader explizit analysiert wird, um die#version
Zeichenfolge (falls vorhanden) zu finden und Ihre Einschlüsse danach zu machen, aber Zugriff auf eine#include
Richtlinie ist möglicherweise vorzuziehen, um leichter kontrollieren zu können, wann diese Einschlüsse vorgenommen werden müssen. Andererseits, da GLSL Kommentare vor der#version
Zeile ignoriert , können Sie Metadaten für Includes in Kommentaren am Anfang Ihrer Datei hinzufügen (yuck.)Die Frage ist nun: Gibt es eine Standardlösung für
#include
oder müssen Sie Ihre eigene Präprozessorerweiterung rollen?Es gibt die
GL_ARB_shading_language_include
Erweiterung, aber sie hat einige Nachteile:"/buffers.glsl"
(wie in verwendet#include "/buffers.glsl"
) dem Inhalt der Dateibuffer.glsl
(die Sie zuvor geladen haben) entspricht."/"
absoluten Pfaden im Linux-Stil beginnen. Diese Notation ist C-Programmierern im Allgemeinen unbekannt und bedeutet, dass Sie keine relativen Pfade angeben können.Ein gängiger Entwurf ist die Implementierung eines eigenen
#include
Mechanismus. Dies kann jedoch schwierig sein, da Sie auch andere Präprozessoranweisungen analysieren (und auswerten)#if
müssen, um die bedingte Kompilierung ordnungsgemäß zu handhaben (z. B. Header-Guards).Wenn Sie Ihre eigenen implementieren
#include
, haben Sie auch einige Freiheiten bei der Implementierung:GL_ARB_shading_language_include
).Zur Vereinfachung können Sie automatisch Header-Guards für jedes Include in Ihre Vorverarbeitungsebene einfügen, sodass Ihre Prozessorebene wie folgt aussieht:
(Dank an Trent Reed, der mir die oben beschriebene Technik gezeigt hat.)
Zusammenfassend gibt es keine automatische, standardmäßige und einfache Lösung. In einer zukünftigen Lösung könnten Sie eine SPIR-V OpenGL-Schnittstelle verwenden. In diesem Fall könnte sich der GLSL-zu-SPIR-V-Compiler außerhalb der GL-API befinden. Wenn der Compiler außerhalb der OpenGL-Laufzeitumgebung installiert ist, vereinfacht sich die Implementierung erheblich,
#include
da die Schnittstelle zum Dateisystem besser geeignet ist. Ich glaube, die derzeit weit verbreitete Methode besteht darin, nur einen benutzerdefinierten Präprozessor zu implementieren, der so funktioniert, wie es sich jeder C-Programmierer vorstellen sollte.quelle
Ich benutze im Allgemeinen nur die Tatsache, dass glShaderSource (...) ein Array von Strings als Eingabe akzeptiert.
Ich verwende eine json-basierte Shader-Definitionsdatei, die angibt, wie ein Shader (oder ein Programm, das genauer sein soll) aufgebaut ist, und dort spezifiziere ich die Präprozessor-Definitionen, die ich möglicherweise benötige, die verwendeten Uniformen, die Vertex- / Fragment-Shader-Datei. und alle zusätzlichen "Abhängigkeit" -Dateien. Dies sind nur Sammlungen von Funktionen, die vor der eigentlichen Shader-Quelle an die Quelle angehängt werden.
Um es hinzuzufügen, AFAIK, verwendet die Unreal Engine 4 eine # include-Direktive, die vor der Kompilierung analysiert wird und alle relevanten Dateien anhängt, wie Sie vorgeschlagen haben.
quelle
Ich glaube nicht, dass es eine gemeinsame Konvention gibt, aber wenn ich raten würde, würde ich sagen, dass fast jeder eine einfache Form der Texteinfügung als Vorverarbeitungsschritt (eine
#include
Erweiterung) implementiert , da dies sehr einfach ist so. (In JavaScript / WebGL können Sie dies beispielsweise mit einem einfachen regulären Ausdruck tun.) Der Vorteil davon ist, dass Sie die Vorverarbeitung in einem Offline-Schritt für "Release" -Erstellungen durchführen können, wenn der Shader-Code nicht mehr geändert werden muss.Ein Hinweis darauf , in der Tat, dass dieser Ansatz gemeinsam ist , ist die Tatsache , dass eine ARB - Erweiterung wurde die eingeführt:
GL_ARB_shading_language_include
. Ich bin mir nicht sicher, ob dies zu einem Kernfeature wurde oder nicht, aber die Erweiterung wurde gegen OpenGL 3.2 geschrieben.quelle
Einige Leute haben bereits darauf hingewiesen, dass
glShaderSource
eine Reihe von Zeichenfolgen nehmen können.Darüber hinaus sind in GLSL die Kompilierung (
glShaderSource
,glCompileShader
) und Verknüpfung (glAttachShader
,glLinkProgram
) von Shader getrennt.Ich habe das in einigen Projekten verwendet, um Shader zwischen dem spezifischen Teil und den Teilen aufzuteilen, die den meisten Shadern gemeinsam sind, und das dann kompiliert und mit allen Shader-Programmen geteilt wird. Dies funktioniert und ist nicht schwer zu implementieren: Sie müssen lediglich eine Abhängigkeitsliste führen.
In Bezug auf die Wartbarkeit bin ich mir jedoch nicht sicher, ob es ein Gewinn ist. Die Beobachtung war dieselbe, versuchen wir zu faktorisieren. Zwar vermeidet es Wiederholungen, der Aufwand der Technik fühlt sich jedoch erheblich an. Darüber hinaus ist der endgültige Shader schwieriger zu extrahieren: Sie können die Shader-Quellen nicht einfach verketten, da die Deklarationen in einer Reihenfolge enden, die einige Compiler ablehnen oder duplizieren. Daher ist es schwieriger, einen schnellen Shader-Test in einem separaten Tool durchzuführen.
Letztendlich behebt diese Technik einige DRY-Probleme, ist aber alles andere als ideal.
Bei einem Nebenthema bin ich mir nicht sicher, ob dieser Ansatz Auswirkungen auf die Kompilierungszeit hat. Ich habe gelesen, dass einige Treiber das Shader-Programm nur beim Verlinken wirklich kompilieren, aber ich habe nicht gemessen.
quelle