Ein halbwegs üblicher Ansatz besteht darin, Shader-Komponenten so zu gestalten , wie Sie Module nennen.
Die Idee ähnelt einem Nachbearbeitungsdiagramm. Sie schreiben Shader-Codeblöcke, die sowohl die erforderlichen Eingaben als auch die generierten Ausgaben und anschließend den Code enthalten, mit dem Sie tatsächlich arbeiten können. Sie haben eine Liste, die angibt, welche Shader in jeder Situation angewendet werden müssen (ob für dieses Material eine Bump-Mapping-Komponente erforderlich ist, ob die verzögerte oder die Forward-Komponente aktiviert ist usw.).
Sie können dieses Diagramm jetzt nehmen und daraus Shader-Code generieren. Dies bedeutet meistens, den Code der Chunks "einzufügen", wobei das Diagramm sichergestellt hat, dass sie bereits in der erforderlichen Reihenfolge vorliegen, und dann die entsprechenden Shader - Ein- / Ausgaben einzufügen (in GLSL bedeutet dies, dass Sie Ihr "globales" In definieren , out und uniform Variablen).
Dies ist nicht dasselbe wie ein Ubershader-Ansatz. Mit Ubershadern können Sie den gesamten Code, der für alles benötigt wird, in einem einzigen Satz Shader zusammenfassen. Möglicherweise können Sie mithilfe von #ifdefs und Uniformen und dergleichen Features beim Kompilieren oder Ausführen aktivieren und deaktivieren. Ich persönlich verachte den Ubershader-Ansatz, aber einige ziemlich beeindruckende AAA-Motoren verwenden sie (insbesondere Crytek fällt mir ein).
Sie können die Shader-Chunks auf verschiedene Arten handhaben. Der am weitesten fortgeschrittene Weg - und nützlich, wenn Sie GLSL, HLSL und die Konsolen unterstützen möchten - besteht darin, einen Parser für eine Shader-Sprache zu schreiben (wahrscheinlich so nah wie möglich an HLSL / Cg oder GLSL, um maximale "Verständlichkeit" für Ihre Entwickler zu erzielen ), die dann für Übersetzungen von Quelle zu Quelle verwendet werden können. Ein anderer Ansatz besteht darin, Shader-Chunks einfach in XML-Dateien oder dergleichen einzuschließen, z
<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>
Beachten Sie, dass Sie mit diesem Ansatz mehrere Codeabschnitte für verschiedene APIs erstellen oder sogar den Codeabschnitt versionieren können (sodass Sie eine GLSL 1.20-Version und eine GLSL 3.20-Version haben können). Ihr Graph kann sogar automatisch Shader-Chunks ausschließen, die keinen kompatiblen Codeabschnitt haben, so dass Sie auf älterer Hardware eine halbgnadige Verschlechterung erzielen können (so etwas wie normales Mapping oder was auch immer ist nur auf älterer Hardware ausgeschlossen, die es nicht unterstützt, ohne dass der Programmierer dies tun muss) eine Reihe von expliziten Prüfungen durchführen).
Das XMl-Beispiel kann dann etwas Ähnliches generieren (entschuldigt, wenn dies eine ungültige GLSL ist, ist es eine Weile her, dass ich mich dieser API unterzogen habe):
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;
}
Sie könnten ein bisschen schlauer sein und "effizienteren" Code generieren, aber ehrlich gesagt, jeder Shader-Compiler, der kein absoluter Mist ist, wird die Redundanzen aus dem generierten Code für Sie entfernen. Vielleicht können Sie mit neuerem GLSL den Dateinamen #line
jetzt auch in Befehle einfügen, aber ich weiß, dass ältere Versionen sehr fehlerhaft sind und dies nicht unterstützen.
Wenn Sie über mehrere Chunks verfügen, werden deren Eingaben (die nicht von einem Vorgänger-Chunk in der Baumstruktur als Ausgabe bereitgestellt werden) ebenso wie die Ausgaben in den Eingabeblock verkettet, und der Code wird nur verkettet. Ein wenig zusätzliche Arbeit wird geleistet, um sicherzustellen, dass die Stufen übereinstimmen (Scheitelpunkt gegen Fragment) und dass Scheitelpunktattributeingabe-Layouts "einfach funktionieren". Ein weiterer Vorteil dieses Ansatzes ist, dass Sie explizite, einheitliche und Eingabe-Attribut-Bindungsindizes schreiben können, die in älteren Versionen von GLSL nicht unterstützt werden, und diese in Ihrer Shader-Generierungs- / Bindungsbibliothek verarbeiten können. Ebenso können Sie die Metadaten zum Einrichten Ihrer VBOs und glVertexAttribPointer
Aufrufe verwenden, um die Kompatibilität sicherzustellen und um sicherzustellen, dass alles "einfach funktioniert".
Leider gibt es so eine gute Cross-API-Bibliothek bereits nicht. CG kommt ein bisschen in die Nähe, aber es hat Mist Unterstützung für OpenGL auf AMD-Karten und kann sehr langsam sein, wenn Sie nur die grundlegendsten Code-Generierungsfunktionen verwenden. Das DirectX-Effekt-Framework funktioniert ebenfalls, unterstützt jedoch keine andere Sprache als HLSL. Es gibt einige unvollständige / fehlerhafte Bibliotheken für GLSL, die die DirectX-Bibliotheken imitieren, aber ihren Status angeben, als ich das letzte Mal überprüft habe, dass ich nur meine eigenen geschrieben habe.
Der Ubershader-Ansatz besteht lediglich darin, "bekannte" Präprozessor-Direktiven für bestimmte Features zu definieren und diese dann für verschiedene Materialien mit unterschiedlichen Konfigurationen neu zu kompilieren. USE_NORMAL_MAPPING=1
ZB für jedes Material mit einer normalen Map, das Sie definieren und dann in Ihrem Pixel-Stage-Ubershader einfach haben:
#if USE_NORMAL_MAPPING
vec4 normal;
// all your normal mapping code
#else
vec4 normal = normalize(in_normal);
#endif
Ein großes Problem hierbei ist die Behandlung dieses Problems für vorkompiliertes HLSL, bei dem Sie alle verwendeten Kombinationen vorkompilieren müssen. Selbst mit GLSL müssen Sie in der Lage sein, einen Schlüssel aller verwendeten Präprozessoranweisungen ordnungsgemäß zu generieren, um ein erneutes Kompilieren / Zwischenspeichern identischer Shader zu vermeiden. Die Verwendung von Uniformen kann die Komplexität verringern, aber im Gegensatz zu Präprozessor-Uniformen wird die Anzahl der Anweisungen nicht verringert, und die Leistung kann dennoch geringfügig beeinträchtigt werden.
Um es klar auszudrücken, werden beide Ansätze (und auch das manuelle Schreiben einer Tonne Shader-Variationen) im AAA-Bereich verwendet. Verwenden Sie, was für Sie am besten funktioniert.