Was sind Shader?

Einführung

Sie haben sich also entschlossen, Shader auszuprobieren. Sie haben wahrscheinlich gehört, dass sie verwendet werden können, um interessante Effekte zu erzielen, die unglaublich schnell laufen. Sie haben wahrscheinlich auch gehört, dass sie erschreckend sind. Beides ist wahr.

Shader können verwendet werden, um eine Vielzahl von Effekten zu erzeugen (tatsächlich wird alles, was in einer modernen Rendering-Engine gezeichnet wird, mit Shadern ausgeführt).

Das Schreiben von Shadern kann auch für Personen, die mit ihnen nicht vertraut sind, sehr schwierig sein. Godot versucht, das Schreiben von Shadern ein wenig zu vereinfachen, indem es viele nützliche integrierte Funktionen bereitstellt und einige der Initialisierungsarbeiten auf unterster Ebene für Sie erledigt. GLSL (die von Godot verwendete OpenGL Shading Language) ist jedoch immer noch einschränkend und nicht intuitiv, insbesondere für Benutzer, die an GDScript gewöhnt sind.

Aber was sind sie?

Shader sind eine spezielle Art von Programm, das auf Grafikprozessoren (GPUs) ausgeführt wird. Die meisten Computer verfügen über eine GPU, entweder eine in die CPU integrierte oder eine diskrete (also eine separate Hardwarekomponente, z.B. die typische Grafikkarte). GPUs sind besonders nützlich für das Rendern, da sie für die parallele Ausführung von Tausenden von Anweisungen optimiert sind.

Die Ausgabe des Shaders sind normalerweise die farbigen Pixel des Objekts, das in das Ansichtsfenster gezeichnet wird. Einige Shader ermöglichen jedoch spezielle Ausgaben (dies gilt insbesondere für APIs wie Vulkan). Shader arbeiten innerhalb der Shader-Pipeline. Der Standardprozess ist die Vertex -> Fragment Shader Pipeline. Der Vertex-Shader wird verwendet, um zu entscheiden, wohin jeder Vertex (Punkt in einem 3D-Modell oder Ecke eines Sprites) geht, und der Fragment-Shader entscheidet, welche Farbe einzelne Pixel erhalten.

Angenommen, Sie möchten alle Pixel in einer Textur einer bestimmten Farbe zuweisen, so würde man dies auf der CPU so schreiben:

for x in range(width):
  for y in range(height):
    set_color(x, y, some_color)

In einem Shader erhalten Sie nur Zugriff auf das Innere der Schleife. Das, was Sie schreiben, sieht also folgendermaßen aus:

// function called for each pixel
void fragment() {
  COLOR = some_color;
}

Sie haben keine Kontrolle darüber, wie diese Funktion aufgerufen wird. Sie müssen Ihre Shader also anders gestalten als Programme auf der CPU.

Eine Konsequenz der Shader-Pipeline ist, dass Sie nicht auf die Ergebnisse eines vorherigen Shader-Laufs zugreifen können, nicht auf andere Pixel des gezeichneten Pixels zugreifen können und nicht außerhalb des aktuell gezeichneten Pixels schreiben können. Dadurch kann die GPU den Shader für verschiedene Pixel parallel ausführen, da diese nicht voneinander abhängig sind. Dieser Mangel an Flexibilität wurde für die Arbeit mit der GPU entwickelt, wodurch Shader unglaublich schnell arbeiten können.

Was können Sie machen

  • Eckpunkte sehr schnell positionieren
  • Farbe sehr schnell berechnen
  • Beleuchtung sehr schnell berechnen
  • viel, viel Mathematik

Was sie nicht können

  • außerhalb des Mesh (Drahtgitters) zeichnen
  • auf andere Pixel von aktuellen Pixeln aus (oder Eckpunkten) zugreifen
  • frühere Iterationen speichern
  • Update während einer Berechnung (sie können, aber sie müssen kompiliert werden)

Struktur eines Shaders

In Godot, shaders are made up of 3 main functions: the vertex() function, the fragment() function and the light() function.

The vertex() function runs over all the vertices in the mesh and sets their positions as well as some other per-vertex variables.

The fragment() function runs for every pixel that is covered by the mesh. It uses the variables from the vertex() function to run. The variables from the vertex() function are interpolated between the vertices to provide the values for the fragment() function.

The light() function runs for every pixel and for every light. It takes variables from the fragment() function and from previous runs of itself.

Weitere Informationen zur Funktionsweise von Shadern in Godot finden Sie im Dokument Shaders.

Warnung

The light() function won't be run if the vertex_lighting render mode is enabled, or if Rendering > Quality > Shading > Force Vertex Shading is enabled in the Project Settings. (It's enabled by default on mobile platforms.)

Teilübersicht

GPUs are able to render graphics much faster than CPUs for a few reasons, but most notably, because they are able to run calculations massively in parallel. A CPU typically has 4 or 8 cores while a GPU typically has thousands. That means a GPU can do hundreds of tasks at once. GPU architects have exploited this in a way that allows for doing many calculations very quickly, but only when many or all cores are doing the same calculation at once, but with different data.

Hier kommen Shader ins Spiel. Die GPU ruft den Shader einige Male gleichzeitig auf und bearbeitet dann verschiedene Datenbits (Eckpunkte oder Pixel). Diese Datenbündel werden oft als Wellenfronten bezeichnet. Ein Shader wird für jeden Thread in der Wellenfront gleich ausgeführt. Wenn eine bestimmte GPU beispielsweise 100 Threads pro Wellenfront verarbeiten kann, wird eine Wellenfront auf einem 10 × 10-Pixelblock zusammen ausgeführt. Es wird weiterhin für alle Pixel in dieser Wellenfront ausgeführt, bis sie vollständig sind. Wenn Sie also ein Pixel langsamer als der Rest ist (aufgrund übermäßiger Verzweigung), wird der gesamte Block verlangsamt, was zu massiv langsameren Renderzeiten führt.

This is different from CPU-based operations. On a CPU, if you can speed up even one pixel, the entire rendering time will decrease. On a GPU, you have to speed up the entire wavefront to speed up rendering.