Game Engine Design - Ubershader - Shader Management Design [geschlossen]

18

Ich möchte ein flexibles Ubershader-System mit verzögerter Schattierung implementieren. Meine derzeitige Idee ist es, Shader aus Modulen zu erstellen, die sich mit bestimmten Funktionen wie FlatTexture, BumpTexture, Displacement Mapping usw. befassen. Es gibt auch kleine Module, die Farben decodieren, Tone Mapping usw. durchführen. Dies hat den Vorteil, den ich kann Ersetzen Sie bestimmte Modultypen, wenn diese von der GPU nicht unterstützt werden, damit ich mich an die aktuellen GPU-Funktionen anpassen kann. Ich bin nicht sicher, ob dieses Design gut ist. Ich fürchte, ich könnte jetzt eine schlechte Designentscheidung treffen und später dafür bezahlen.

Meine Frage ist, wo finde ich Ressourcen, Beispiele und Artikel zur effektiven Implementierung eines Shader-Management-Systems? Weiß jemand, wie die Big Game Engines das machen?

Michael Staud
quelle
3
Nicht lange genug für eine echte Antwort: Sie werden mit diesem Ansatz gut zurechtkommen, wenn Sie klein anfangen und ihn Ihren Bedürfnissen entsprechend wachsen lassen, anstatt zu versuchen, den MegaCity-One von Shadern von vornherein zu bauen. Erstens verringern Sie Ihre größte Sorge, viel zu viel Design im Voraus zu machen und später dafür zu bezahlen, wenn es nicht funktioniert, und zweitens vermeiden Sie zusätzliche Arbeit, die nie in Anspruch genommen wird.
Patrick Hughes
Leider akzeptieren wir keine Fragen zu Ressourcenanforderungen mehr.
Gnemlock

Antworten:

23

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 #linejetzt 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 glVertexAttribPointerAufrufe 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=1ZB 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.

Sean Middleditch
quelle