Verwenden von zwei Shadern anstelle von einem mit IF-Anweisungen

9

Ich habe daran gearbeitet, eine relativ große OpenGl ES 1.1-Quelle auf ES 2.0 zu portieren.

In OpenGL ES 2.0 (was bedeutet, dass alles Shader verwendet) möchte ich dreimal eine Teekanne zeichnen.

  1. Die erste mit einer einheitlichen Farbe (ua die alte glColor4f).

  2. Die zweite mit einer Farbe pro Scheitelpunkt (die Teekanne hat auch eine Reihe von Scheitelpunktfarben)

  3. Die dritte mit Textur pro Scheitelpunkt

  4. Und vielleicht eine vierte mit Textur und Farbe pro Scheitelpunkt. Und dann vielleicht eine fünfte, auch mit Normalen.

Soweit ich weiß, habe ich bei der Implementierung zwei Möglichkeiten. Der erste besteht darin, einen Shader zu erstellen, der alle oben genannten Funktionen unterstützt, mit einer Uniform, die das Verhalten ändert (z. B. die Singular-Farbuniform oder die Per-Vertex-Farbuniform).

Die zweite Möglichkeit besteht darin, für jede Situation einen anderen Shader zu erstellen. Bei einigen benutzerdefinierten Shader-Vorverarbeitungen ist dies nicht so kompliziert, aber das Problem sind die Leistungskosten beim Wechseln von Shadern zwischen Zeichnungsobjekten. Ich habe gelesen, dass es nicht trivial klein ist.

Ich meine, der beste Weg, dies zu tun, besteht darin, beides zu bauen und zu messen, aber es wäre gut, irgendwelche Eingaben zu hören.

Kamziro
quelle

Antworten:

10

Die Leistungskosten der Verzweigung können auch nicht trivial gering sein. In Ihrem Fall nehmen alle Scheitelpunkte und Fragmente, die gezeichnet werden, denselben Weg durch Ihre Shader. Auf moderner Desktop- Hardware wäre dies also nicht so schlimm wie es sein könnte, aber Sie verwenden ES2, was bedeutet, dass Sie nicht modern verwenden Desktop-Hardware.

Der schlimmste Fall bei der Verzweigung wird ungefähr so ​​aussehen:

  • beide Seiten des Zweigs werden ausgewertet.
  • Eine "Mix" - oder "Step" -Anweisung wird vom Shader-Compiler generiert und in Ihren Code eingefügt, um zu entscheiden, welche Seite verwendet werden soll.

Alle diese zusätzlichen Anweisungen werden für jeden Scheitelpunkt oder jedes Fragment ausgeführt, das Sie zeichnen. Das sind möglicherweise Millionen zusätzlicher Anweisungen, die gegen die Kosten eines Shaderwechsels abgewogen werden müssen.

Apples " OpenGL ES-Programmierhandbuch für iOS " (das als repräsentativ für Ihre Zielhardware angesehen werden kann) enthält folgende Informationen zur Verzweigung:

Verzweigung vermeiden

Verzweigungen werden in Shadern nicht empfohlen, da sie die Fähigkeit verringern können, Operationen auf 3D-Grafikprozessoren parallel auszuführen. Wenn Ihre Shader Zweige verwenden müssen, befolgen Sie diese Empfehlungen:

  • Beste Leistung: Verzweigen Sie auf eine Konstante, die bekannt ist, wenn der Shader kompiliert wird.
  • Akzeptabel: Verzweigen Sie auf eine einheitliche Variable.
  • Potenziell langsam: Verzweigung auf einen im Shader berechneten Wert.

Anstatt einen großen Shader mit vielen Knöpfen und Hebeln zu erstellen, erstellen Sie kleinere Shader, die auf bestimmte Rendering-Aufgaben spezialisiert sind. Es gibt einen Kompromiss zwischen der Reduzierung der Anzahl der Zweige in Ihren Shadern und der Erhöhung der Anzahl der von Ihnen erstellten Shader. Testen Sie verschiedene Optionen und wählen Sie die schnellste Lösung.

Selbst wenn Sie zufrieden sind, dass Sie sich hier im "Akzeptablen" Slot befinden, müssen Sie dennoch berücksichtigen, dass Sie bei 4 oder 5 Fällen zur Auswahl die Anzahl der Anweisungen in Ihren Shadern erhöhen werden. Sie sollten die Grenzwerte für die Anzahl der Anweisungen auf Ihrer Zielhardware kennen und sicherstellen, dass Sie diese nicht überschreiten. Zitieren Sie erneut über den obigen Apple-Link:

OpenGL ES-Implementierungen sind nicht erforderlich, um einen Software-Fallback zu implementieren, wenn diese Grenzwerte überschritten werden. Stattdessen kann der Shader einfach nicht kompiliert oder verknüpft werden.

Nichts davon bedeutet, dass die Verzweigung nicht die beste Lösung für Ihre Anforderungen ist. Sie haben die Tatsache richtig identifiziert, dass Sie beide Ansätze profilieren sollten. Das ist also die endgültige Empfehlung. Beachten Sie jedoch, dass eine verzweigungsbasierte Lösung mit zunehmender Komplexität von Shadern möglicherweise einen viel höheren Overhead verursacht als einige Shader-Änderungen.

Maximus Minimus
quelle
3

Die Kosten für das Binden von Shadern sind möglicherweise nicht trivial, aber es wird kein Engpass sein, es sei denn, Sie rendern Tausende von Elementen, ohne alle Objekte zu stapeln, die dieselben Shader verwenden.

Ich bin mir zwar nicht sicher, ob dies für mobile Geräte gilt, aber GPUs sind mit Zweigen nicht besonders langsam, wenn der Zustand zwischen einer Konstanten und einer Uniform liegt. Beide sind gültig, beide wurden in der Vergangenheit verwendet und werden auch in Zukunft verwendet. Wählen Sie die aus, die Ihrer Meinung nach in Ihrem Fall sauberer ist.

Darüber hinaus gibt es einige andere Möglichkeiten, um dies zu erreichen: "Uber-Shader" und ein wenig Trick bei der Verknüpfung von OpenGL-Shader-Programmen.

"Uber-Shader" sind im Wesentlichen die erste Wahl, abzüglich der Verzweigung, aber Sie haben mehrere Shader. Anstelle der Verwendung von ifAussagen, verwenden Sie die Prä - Prozessor - #define, #ifdef, #else, #endif, und verschiedene Versionen kompilieren, einschließlich der richtigen #defines für das, was Sie brauchen.

vec4 color;
#ifdef PER_VERTEX_COLOR
color = in_color;
#else
color = obj_color;
#endif

Sie können den Shader auch in separate Funktionen aufteilen. Lassen Sie einen Shader, der Prototypen für alle Funktionen definiert und aufruft, eine Reihe zusätzlicher Shader verknüpfen, die die richtigen Implementierungen enthalten. Ich habe diesen Trick für die Schattenzuordnung verwendet, um das Austauschen der Filterung für alle Objekte zu vereinfachen, ohne alle Shader ändern zu müssen.

//ins, outs, uniforms

float getShadowCoefficient();

void main()
{
    //shading stuff goes here

    gl_FragColor = color * getShadowCoefficient();
}

Dann könnte ich mehrere andere Shader-Dateien haben, die definieren getShadowCoefficient(), die notwendigen Uniformen und sonst nichts. shadow_none.glslEnthält zum Beispiel :

float getShadowCoefficient()
{
    return 1;
}

Und shadow_simple.glslenthält (vereinfacht von meinem Shader, der CSMs implementiert):

in vec4 eye_position;

uniform sampler2DShadow shad_tex;
uniform mat4 shad_mat;

float getShadowCoefficient()
{
    vec4 shad_coord = shad_mat * eye_position;
    return texture(shad_tex, shad_coord).x;
}

Und Sie können einfach auswählen, ob Sie eine Schattierung wünschen oder nicht, indem Sie einen anderen shadow_*Shader verknüpfen . Diese Lösung hat zwar mehr Overhead, aber ich würde gerne glauben, dass der GLSL-Compiler gut genug ist, um zusätzlichen Overhead im Vergleich zu anderen Methoden zu optimieren. Ich habe noch keine Tests durchgeführt, aber so mache ich das gerne.

Robert Rouhani
quelle