Advertisement
  1. Game Development
  2. Shaders

Ein Anfängerhandbuch zum Kodieren von Grafik-Shadern

Scroll to top
Read Time: 11 min
This post is part of a series called A Beginner's Guide to Coding Graphics Shaders.
A Beginner's Guide to Coding Graphics Shaders: Part 2

() translation by (you can also view the original English article)

Das Schreiben von Grafik-Shadern lernt, die Leistung der GPU mit ihren mehreren tausend Kernen zu nutzen, die alle parallel laufen. Es ist eine Art von Programmierung, die eine andere Denkweise erfordert, aber die Erschließung ihres Potenzials ist die anfänglichen Schwierigkeiten wert.

Praktisch jede moderne Grafiksimulation, die Sie sehen, wird in gewisser Weise durch den für die GPU geschriebenen Code angetrieben, von den realistischen Lichteffekten in hochmodernen AAA-Spielen über 2D-Nachbearbeitungseffekte bis hin zu Flüssigkeitssimulationen.

Eine Szene in Minecraft, vor und nach der Anwendung einiger Shader.

Das Ziel dieses Leitfadens

Shader-Programmierung kommt manchmal als rätselhafte schwarze Magie vor und wird oft missverstanden. Es gibt viele Code-Beispiele, die Ihnen zeigen, wie Sie unglaubliche Effekte erstellen können, aber wenig oder keine Erklärung bieten. Dieser Leitfaden soll diese Lücke schließen. Ich werde mich mehr auf die Grundlagen des Schreibens und Verstehens von Shader-Code konzentrieren, so dass Sie ganz einfach von Grund auf neu anpassen, kombinieren oder schreiben können!

Dies ist ein allgemeiner Leitfaden. Was Sie hier lernen, wird also auf alles angewendet, was Shader ausführen kann.

Was ist ein Shader?

Ein Shader ist einfach ein Programm, das in der Grafikpipeline ausgeführt wird und dem Computer mitteilt, wie jedes Pixel gerendert wird. Diese Programme werden als Shader bezeichnet, da sie oft zur Steuerung von Beleuchtungs- und Schattierungseffekten verwendet werden, aber es gibt keinen Grund, warum sie nicht mit anderen Spezialeffekten umgehen können.

Shader werden in einer speziellen Schattierungssprache geschrieben. Mach dir keine Sorgen, du musst nicht rausgehen und eine komplett neue Sprache lernen; Wir werden GLSL (OpenGL Shading Language) verwenden, eine C-ähnliche Sprache. (Es gibt eine Reihe von Shading-Sprachen für verschiedene Plattformen, aber da sie alle auf der GPU laufen, sind sie alle sehr ähnlich)

Lass uns hineinspringen!

Wir werden ShaderToy für dieses Tutorial verwenden. Dadurch können Sie beginnen, Shader direkt in Ihrem Browser zu programmieren, ohne dass Sie etwas einrichten müssen! (Es verwendet WebGL zum Rendern, also benötigen Sie einen Browser, der das unterstützt.) Das Erstellen eines Kontos ist optional, aber praktisch zum Speichern Ihres Codes.

Hinweis: ShaderToy befindet sich zum Zeitpunkt des Verfassens dieses Artikels in der Betaversion. Einige kleine UI / Syntax-Details können geringfügig abweichen.

Wenn Sie auf Neuer Shader klicken, sollten Sie Folgendes sehen:

Ihre Benutzeroberfläche sieht möglicherweise etwas anders aus, wenn Sie nicht angemeldet sind.

Der kleine schwarze Pfeil unten ist das, worauf Sie klicken, um Ihren Code zu kompilieren.

Was ist los?

Ich werde gerade erklären, wie Shader in einem Satz funktionieren. Sind Sie bereit? Hier geht!

Der einzige Zweck eines Shaders besteht darin, vier Zahlen zurückzugeben: r, g, b und a.

Das ist alles, was es jemals tun kann oder kann. Die Funktion, die Sie vor sich sehen, läuft für jedes einzelne Pixel auf dem Bildschirm. Es gibt diese vier Farbwerte zurück, und das wird die Farbe dieses Pixels. Dies nennt man einen Pixel Shader (manchmal auch als Fragment Shader bezeichnet).

In diesem Sinne versuchen wir, unseren Bildschirm rot zu färben. Die Werte rgba (rot, grün, blau und "alpha", die die Transparenz definieren) gehen von 0 bis 1, also müssen wir nur r,g,b,a = 1,0,0,1 zurückgeben. ShaderToy erwartet, dass die endgültige Pixelfarbe in fragColor gespeichert wird.

1
void mainImage( out vec4 fragColor, in vec2 fragCoord )
2
{
3
    fragColor = vec4(1.0,0.0,0.0,1.0);
4
}

Herzliche Glückwünsche! Dies ist dein allererster funktionierender Shader!

Herausforderung: Kannst du es in eine feste graue Farbe ändern?

vec4 ist nur ein Datentyp, also hätten wir unsere Farbe als Variable deklarieren können:

1
void mainImage( out vec4 fragColor, in vec2 fragCoord )
2
{
3
    vec4 solidRed = vec4(1.0,0.0,0.0,1.0);
4
    fragColor = solidRed;
5
}

Das ist jedoch nicht sehr aufregend. Wir sind in der Lage, Code auf Hunderttausenden von Pixeln parallel auszuführen und setzen alle auf die gleiche Farbe.

Lassen Sie uns versuchen, einen Farbverlauf über den Bildschirm zu rendern. Nun, wir können nicht viel tun, ohne ein paar Dinge über die Pixel zu wissen, auf die wir Einfluss haben, wie zum Beispiel die Position auf dem Bildschirm ...

Shader-Eingänge

Der Pixelshader übergibt einige Variablen, die Sie verwenden können. Am nützlichsten ist uns fragCoord, das die Koordinaten x und y (und z, wenn Sie in 3D arbeiten) enthält. Lassen Sie uns versuchen, alle Pixel auf der linken Bildschirmhälfte schwarz und alle auf der rechten Hälfte rot zu machen:

1
void mainImage( out vec4 fragColor, in vec2 fragCoord )
2
{
3
    vec2 xy = fragCoord.xy; //We obtain our coordinates for the current pixel

4
    vec4 solidRed = vec4(0,0.0,0.0,1.0);//This is actually black right now

5
    if(xy.x > 300.0){//Arbitrary number, we don't know how big our screen is!

6
        solidRed.r = 1.0;//Set its red component to 1.0

7
    }
8
    fragColor = solidRed;
9
}

Hinweis: Für jedes vec4 können Sie auf seine Komponenten über obj.x, obj.y, obj.z und obj.w oder über obj.r, obj.g, obj.b, obj.a. Sie sind gleichwertig; Es ist nur eine bequeme Möglichkeit, sie so zu benennen, dass ihr Code lesbarer wird, sodass andere, wenn sie obj.r sehen, verstehen, dass obj eine Farbe darstellt.

Siehst du ein Problem mit dem obigen Code? Klicke auf den Go Fullscreen-Button rechts unten in deinem Vorschaufenster.

Der Anteil des Bildschirms, der rot ist, hängt von der Größe des Bildschirms ab. Um sicherzustellen, dass genau die Hälfte des Bildschirms rot ist, müssen wir wissen, wie groß unser Bildschirm ist. Die Bildschirmgröße ist keine eingebaute Variable wie die Pixelposition, weil es normalerweise an Ihnen liegt, dem Programmierer, der die App erstellt hat, um das festzulegen. In diesem Fall bestimmen die ShaderToy-Entwickler die Bildschirmgröße.

Wenn etwas keine eingebaute Variable ist, können Sie diese Information von der CPU (Ihrem Hauptprogramm) an die GPU (Ihren Shader) senden. ShaderToy erledigt das für uns. Auf der Registerkarte Shader-Eingaben können Sie alle Variablen sehen, die an den Shader übergeben werden. Auf diese Weise von CPU zu GPU übergebene Variablen werden in GLSL als einheitlich bezeichnet.

Lassen Sie uns unseren Code oben optimieren, um die Mitte des Bildschirms korrekt zu erhalten. Wir müssen den Shader-Eingang iResolution verwenden:

1
void mainImage( out vec4 fragColor, in vec2 fragCoord )
2
{
3
    vec2 xy = fragCoord.xy; //We obtain our coordinates for the current pixel

4
    xy.x = xy.x / iResolution.x; //We divide the coordinates by the screen size

5
    xy.y = xy.y / iResolution.y;
6
    // Now x is 0 for the leftmost pixel, and 1 for the rightmost pixel

7
    vec4 solidRed = vec4(0,0.0,0.0,1.0); //This is actually black right now

8
    if(xy.x > 0.5){
9
        solidRed.r = 1.0; //Set its red component to 1.0

10
    }
11
    fragColor = solidRed;
12
}

Wenn Sie dieses Mal versuchen, das Vorschaufenster zu vergrößern, sollten die Farben den Bildschirm immer noch in zwei Hälften teilen.

Von einem Split zu einem Farbverlauf

Es sollte ziemlich einfach sein, dies zu einem Gradienten zu machen. Unsere Farbwerte gehen von 0 bis 1 und unsere Koordinaten gehen jetzt auch von 0 nach 1.

1
void mainImage( out vec4 fragColor, in vec2 fragCoord )
2
{
3
    vec2 xy = fragCoord.xy; //We obtain our coordinates for the current pixel

4
    xy.x = xy.x / iResolution.x; //We divide the coordinates by the screen size

5
    xy.y = xy.y / iResolution.y;
6
    // Now x is 0 for the leftmost pixel, and 1 for the rightmost pixel

7
    vec4 solidRed = vec4(0,0.0,0.0,1.0); //This is actually black right now

8
     solidRed.r = xy.x; //Set its red component to the normalized x value

9
    fragColor = solidRed;
10
}

Und voila!

Herausforderung: Kannst du das in einen vertikalen Gradienten verwandeln? Was ist mit Diagonale? Was ist mit einem Farbverlauf mit mehr als einer Farbe?

Wenn Sie genug damit spielen, können Sie feststellen, dass die obere linke Ecke Koordinaten (0,1), nicht (0,0) hat. Dies ist wichtig zu beachten.

Bilder zeichnen

Mit Farben herumzuspielen macht Spaß, aber wenn wir etwas eindrucksvolles machen wollen, muss unser Shader in der Lage sein, Input von einem Bild zu nehmen und es zu verändern. Auf diese Weise können wir einen Shader erstellen, der sich auf unseren gesamten Spielbildschirm auswirkt (wie ein Unterwasserfluid-Effekt oder eine Farbkorrektur) oder nur bestimmte Objekte auf bestimmte Weise beeinflussen (wie ein realistisches Beleuchtungssystem).

Wenn wir auf einer normalen Plattform programmieren würden, müssten wir unser Bild (oder Textur) als Uniform an die GPU senden, genauso wie Sie die Bildschirmauflösung gesendet hätten. ShaderToy kümmert sich darum für uns. Es gibt vier Eingangskanäle unten:

ShaderToys vier Eingangskanäle.

Klicken Sie auf iChannel0 und wählen Sie eine beliebige Textur (Bild) aus.

Sobald das erledigt ist, hast du nun ein Bild, das an deinen Shader weitergegeben wird. Es gibt jedoch ein Problem: Es gibt keine DrawImage() -Funktion. Denken Sie daran, dass der Pixelshader nur die Farbe jedes Pixels ändern kann.

Wenn wir also nur eine Farbe zurückgeben können, wie zeichnen wir dann unsere Textur auf dem Bildschirm? Wir müssen irgendwie das aktuelle Pixel, auf dem sich unser Shader befindet, dem entsprechenden Pixel auf der Textur zuordnen:

Denken Sie daran, dass das obere linke Pixel in unserem Bildschirm (0,1) ist, während das obere linke Pixel der Textur (0,0) ist, also müssen wir die y-Achse spiegeln.

Wir können dies tun, indem wir die Funktion texture2D (Textur, Koordinaten) verwenden, die eine Textur und ein (x, y) -Koordinatenpaar als Eingaben nimmt und die Farbe der Textur an diesen Koordinaten als vec4 zurückgibt.

Sie können die Koordinaten auf dem Bildschirm beliebig anpassen. Sie können die gesamte Textur auf einem Viertel des Bildschirms zeichnen (indem Sie Pixel überspringen, sie effektiv verkleinern) oder einfach nur einen Teil der Textur zeichnen.

Für unsere Zwecke wollen wir nur das Bild sehen, also passen wir die Pixel 1: 1 an:

1
void mainImage( out vec4 fragColor, in vec2 fragCoord )
2
{
3
    vec2 xy = fragCoord.xy / iResolution.xy;//Condensing this into one line

4
    xy.y = 1.0 - xy.y;
5
    vec4 texColor = texture2D(iChannel0,xy);//Get the pixel at xy from iChannel0

6
    fragColor = texColor;//Set the screen pixel to that color

7
}

Damit haben wir unser erstes Image!

Jetzt, da Sie Daten korrekt aus einer Textur ziehen, können Sie sie beliebig manipulieren! Sie können es dehnen und skalieren oder mit seinen Farben spielen.

Lassen Sie uns versuchen, dies mit einem Farbverlauf zu ändern, ähnlich dem, was wir oben getan haben:

1
texColor.b = xy.x;

Herzlichen Glückwunsch, Sie haben gerade Ihren ersten Post-Processing-Effekt erzielt!

Herausforderung: Können Sie einen Shader schreiben, der ein Bild schwarz-weiß macht?

Beachten Sie, dass, obwohl es sich um ein statisches Bild handelt, das, was Sie vor Ihnen sehen, in Echtzeit geschieht. Sie können dies selbst sehen, indem Sie das statische Bild durch ein Video ersetzen: Klicken Sie erneut auf den iChannel0-Eingang und wählen Sie eines der Videos aus.

Eine Bewegung hinzufügen

Bisher waren alle unsere Effekte statisch. Wir können viel interessantere Dinge tun, indem wir die Inputs nutzen, die ShaderToy uns gibt. iGlobalTime ist eine ständig wachsende Variable; wir können es als Samen verwenden, um periodische Effekte zu erzielen. Lass uns ein wenig mit Farben experimentieren:

1
void mainImage( out vec4 fragColor, in vec2 fragCoord )
2
{
3
    vec2 xy = fragCoord.xy / iResolution.xy; // Condensing this into one line

4
       xy.y = 1.0-xy.y; // Flipping the y

5
    vec4 texColor = texture2D(iChannel0,xy); // Get the pixel at xy from iChannel0

6
       texColor.r *= abs(sin(iGlobalTime));
7
    texColor.g *= abs(cos(iGlobalTime));
8
    texColor.b *= abs(sin(iGlobalTime) * cos(iGlobalTime));
9
    fragColor = texColor; // Set the screen pixel to that color

10
}

Es gibt Sinus- und Kosinusfunktionen, die in GLSL integriert sind, sowie eine Menge anderer nützlicher Funktionen, wie zum Beispiel die Länge eines Vektors oder die Entfernung zwischen zwei Vektoren. Farben sollen nicht negativ sein, also stellen wir sicher, dass wir den Absolutwert erhalten, indem wir die ABS-Funktion verwenden.

Herausforderung: Können Sie einen Shader erstellen, der ein Bild von Schwarz-Weiß zu Vollfarbe hin und her ändert?

Ein Hinweis zum Debuggen von Shadern

Während Sie vielleicht daran gewöhnt sind, durch Ihren Code zu gehen und die Werte von allem auszudrucken, um zu sehen, was vor sich geht, ist das beim Schreiben von Shadern nicht wirklich möglich. Möglicherweise finden Sie einige Debugging-Tools speziell für Ihre Plattform, aber im Allgemeinen ist es am besten, den Wert, den Sie testen, auf etwas Grafisches zu setzen, das Sie stattdessen sehen können.

Fazit

Dies sind nur die Grundlagen der Arbeit mit Shadern, aber wenn Sie sich mit diesen Grundlagen vertraut machen, können Sie noch viel mehr tun. Stöbere durch die Effekte auf ShaderToy und finde heraus, ob du einige von ihnen verstehen oder replizieren kannst!

Eine Sache, die ich in diesem Tutorial nicht erwähnt habe, ist Vertex Shaders. Sie sind immer noch in der gleichen Sprache geschrieben, außer dass sie auf jedem Eckpunkt statt auf jedem Pixel ausgeführt werden, und sie geben sowohl eine Position als auch eine Farbe zurück. Vertex-Shader sind normalerweise dafür verantwortlich, eine 3D-Szene auf den Bildschirm zu projizieren (etwas, das in den meisten Grafikpipelines eingebaut ist). Pixel-Shader sind für viele der fortgeschrittenen Effekte verantwortlich, die wir sehen. Deshalb sind sie unser Fokus.

Letzte Herausforderung: Kannst du einen Shader schreiben, der den grünen Bildschirm in den Videos auf ShaderToy entfernt und ein weiteres Video als Hintergrund zum ersten hinzufügt?

Das ist alles für diesen Guide! Ich würde Ihr Feedback und Ihre Fragen sehr schätzen. Wenn es etwas Bestimmtes gibt, über das Sie mehr erfahren möchten, hinterlassen Sie bitte einen Kommentar. Zukünftige Anleitungen könnten Themen wie die Grundlagen von Beleuchtungssystemen oder die Erstellung einer Flüssigkeitssimulation oder die Einrichtung von Shadern für eine bestimmte Plattform umfassen.

Advertisement
Did you find this post useful?
Want a weekly email summary?
Subscribe below and we’ll send you a weekly email summary of all new Game Development tutorials. Never miss out on learning about the next big thing.
Advertisement
Looking for something to help kick start your next project?
Envato Market has a range of items for sale to help get you started.