Freigeben von Code zwischen mehreren GLSL-Shadern

30

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 #includeDirektiven analysiert ? Wenn es in der Branche akzeptierte Muster gibt, würde ich ihnen gerne folgen.

Martin Ender
quelle
4
Diese Frage ist möglicherweise etwas umstritten, da einige andere SE-Sites keine Fragen zu Best Practices wünschen. Dies ist beabsichtigt, um zu sehen, wie diese Community in Bezug auf solche Fragen steht.
Martin Ender
2
Hmm, sieht gut aus für mich. Ich würde sagen, wir sind in unseren Fragen zu einem großen Teil etwas "breiter" / "allgemeiner" als beispielsweise StackOverflow.
Chris sagt Reinstate Monica
2
StackOverflow wurde von einem "Fragen Sie uns" zu einem "Fragen Sie uns nicht, es sei denn, Sie müssen bitte einsteigen".
Insidesin
Wie wäre es mit einer zugehörigen Meta-Frage, wenn damit die Themenbezogenheit bestimmt werden soll?
SL Barth - Wiedereinsetzung von Monica
2
Diskussion über Meta.
Martin Ender

Antworten:

18

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:

  1. Es ist schwieriger zu steuern, was im Shader enthalten sein muss (Sie benötigen hierfür ein separates System.)
  2. Dies bedeutet, dass der Shader-Autor die GLSL #versionaufgrund der folgenden Anweisung in der GLSL-Spezifikation nicht angeben kann :

Die Anweisung #version muss in einem Shader vor allen anderen Anweisungen außer Kommentaren und Leerzeichen stehen.

glShaderSourceKann aufgrund dieser Aussage nicht verwendet werden, um Text vor den #versionDeklarationen voranzustellen . Dies bedeutet, dass die #versionZeile in Ihren glShaderSourceArgumenten 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 #versioninnerhalb des Skripts auf standardmäßige Weise angeben , müssen Sie #includenach der #versionAnweisung irgendwie -s einfügen . Dies könnte geschehen, indem der GLSL-Shader explizit analysiert wird, um die #versionZeichenfolge (falls vorhanden) zu finden und Ihre Einschlüsse danach zu machen, aber Zugriff auf eine#includeRichtlinie ist möglicherweise vorzuziehen, um leichter kontrollieren zu können, wann diese Einschlüsse vorgenommen werden müssen. Andererseits, da GLSL Kommentare vor der #versionZeile 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 #includeoder müssen Sie Ihre eigene Präprozessorerweiterung rollen?

Es gibt die GL_ARB_shading_language_includeErweiterung, aber sie hat einige Nachteile:

  1. Es wird nur von NVIDIA unterstützt ( http://delphigl.de/glcapsviewer/listreports2.php?listreportsbyextension=GL_ARB_shading_language_include ).
  2. Es funktioniert, indem die Einschlusszeichenfolgen vorab angegeben werden. Daher müssen Sie vor dem Kompilieren angeben, dass die Zeichenfolge "/buffers.glsl"(wie in verwendet #include "/buffers.glsl") dem Inhalt der Datei buffer.glsl(die Sie zuvor geladen haben) entspricht.
  3. Wie Sie vielleicht in Punkt (2) bemerkt haben, müssen Ihre Pfade mit "/"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 #includeMechanismus. Dies kann jedoch schwierig sein, da Sie auch andere Präprozessoranweisungen analysieren (und auswerten) #ifmü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:

  • Sie könnten Zeichenfolgen vorzeitig übergeben (wie GL_ARB_shading_language_include).
  • Sie können einen Include-Rückruf angeben (dies wird von der D3DCompiler-Bibliothek von DirectX ausgeführt.)
  • Sie können ein System implementieren, das immer direkt aus dem Dateisystem liest, wie in typischen C-Anwendungen.

Zur Vereinfachung können Sie automatisch Header-Guards für jedes Include in Ihre Vorverarbeitungsebene einfügen, sodass Ihre Prozessorebene wie folgt aussieht:

if (#include and not_included_yet) include_file();

(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, #includeda 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.

Nicolas Louis Guillemot
quelle
Shader können auch mit glslify in Module unterteilt werden , obwohl dies nur mit node.js funktioniert.
Anderson Green
9

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.

Matteo Bertello
quelle
4

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 #includeErweiterung) 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.

glampert
quelle
2
GL_ARB_shading_language_include ist kein Kernfeature. Tatsächlich unterstützt es nur NVIDIA. ( delphigl.de/glcapsviewer/… )
Nicolas Louis Guillemot
4

Einige Leute haben bereits darauf hingewiesen, dass glShaderSourceeine 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.

Julien Guertault
quelle
Meines Erachtens löst dies das Problem der gemeinsamen Nutzung von Strukturdefinitionen nicht.
Nicolas Louis Guillemot
@NicolasLouisGuillemot: Ja, Sie haben Recht, nur der Anweisungscode wird auf diese Weise geteilt, keine Deklarationen.
Julien Guertault