Wie im Kapitel Hello Triangle erwähnt, sind Shader kleine Programme, die auf der GPU ruhen. Diese Programme werden für jeden spezifischen Abschnitt der Grafikpipeline ausgeführt. Im Grunde genommen sind Shader nichts anderes als Programme, die Eingaben in Ausgaben umwandeln. Shader sind auch sehr isolierte Programme, da sie nicht miteinander kommunizieren dürfen; Die einzige Kommunikation, die sie haben, ist über ihre Ein- und Ausgänge.
Im vorherigen Kapitel haben wir kurz die Oberfläche von Shadern berührt und wie man sie richtig benutzt. Wir werden nun Shader und insbesondere die OpenGL-Shading-Sprache allgemeiner erklären.
GLSL
Shader sind in der C-ähnlichen Sprache GLSL geschrieben. GLSL ist auf die Verwendung mit Grafiken zugeschnitten und enthält nützliche Funktionen, die speziell auf die Vektor- und Matrixmanipulation abzielen.
Shader beginnen immer mit einer Versionsdeklaration, gefolgt von einer Liste von Eingabe- und Ausgabevariablen, Uniformen und ihrer Hauptfunktion. Der Einstiegspunkt jedes Shaders befindet sich an seiner Hauptfunktion, wo wir alle Eingabevariablen verarbeiten und die Ergebnisse in seinen Ausgabevariablen ausgeben. Mach dir keine Sorgen, wenn du nicht weißt, was Uniformen sind, wir werden uns in Kürze darum kümmern.
Ein Shader hat typischerweise die folgende Struktur:
#version version_numberin type in_variable_name;in type in_variable_name;out type out_variable_name; uniform type uniform_name; void main(){ // process input(s) and do some weird graphics stuff ... // output processed stuff to output variable out_variable_name = weird_stuff_we_processed;}
Wenn wir speziell über den Vertex-Shader sprechen, wird jede Eingabevariable auch als Vertex-Attribut bezeichnet. Es gibt eine maximale Anzahl von Vertex-Attributen, die wir durch die Hardware begrenzt deklarieren dürfen. OpenGL garantiert, dass immer mindestens 16 4-Komponenten-Vertex-Attribute verfügbar sind, aber einige Hardware ermöglicht möglicherweise mehr, die Sie durch Abfragen von GL_MAX_VERTEX_ATTRIBS abrufen können:
int nrAttributes;glGetIntegerv(GL_MAX_VERTEX_ATTRIBS, &nrAttributes);std::cout << "Maximum nr of vertex attributes supported: " << nrAttributes << std::endl;
Dies gibt oft das Minimum von 16
was für die meisten Zwecke mehr als genug sein sollte.
Types
GLSL hat wie jede andere Programmiersprache Datentypen, um anzugeben, mit welcher Art von Variable wir arbeiten möchten. GLSL hat die meisten der Standardgrundtypen, die wir aus Sprachen wie C kennen: int
float
double
uint
und bool
. GLSL bietet auch zwei Containertypen, die wir häufig verwenden werden, nämlich vectors
und matrices
. Wir werden Matrizen in einem späteren Kapitel diskutieren.
Vektoren
Ein Vektor in GLSL ist ein 1,2,3- oder 4-Komponenten-Container für einen der gerade genannten Basistypen. Sie können die folgende Form annehmen (n
repräsentiert die Anzahl der Komponenten):
-
vecn
: Der Standardvektor vonn
schwimmt. -
bvecn
: ein Vektor vonn
booleschen Werten. -
ivecn
: ein Vektor vonn
ganzen Zahlen. -
uvecn
: ein Vektor vonn
vorzeichenlosen Ganzzahlen. -
dvecn
: ein Vektor vonn
Doppelkomponenten.
Die meiste Zeit werden wir das grundlegende vecn
da Floats für die meisten unserer Zwecke ausreichen.
Auf Komponenten eines Vektors kann über vec.x
zugegriffen werden, wobei x
die erste Komponente des Vektors ist. Sie können .x
.y
.z
und .w
verwenden, um auf ihre erste, zweite, dritte bzw. vierte Komponente zuzugreifen. Mit GLSL können Sie auch rgba
für Farben oder stpq
für Texturkoordinaten verwenden und auf dieselben Komponenten zugreifen.
Der Vektordatentyp ermöglicht eine interessante und flexible Komponentenauswahl, die als Swizzling bezeichnet wird. Swizzling ermöglicht es uns, eine Syntax wie diese zu verwenden:
vec2 someVec;vec4 differentVec = someVec.xyxx;vec3 anotherVec = differentVec.zyw;vec4 otherVec = someVec.xxxx + anotherVec.yxzy;
Sie können eine beliebige Kombination von bis zu 4 Buchstaben verwenden, um einen neuen Vektor (desselben Typs) zu erstellen, solange der ursprüngliche Vektor diese Komponenten enthält. Es ist beispielsweise nicht erlaubt, auf die .z
Komponente eines vec2
zuzugreifen. Wir können auch Vektoren als Argumente an verschiedene Vektorkonstruktoraufrufe übergeben, wodurch die Anzahl der erforderlichen Argumente reduziert wird:
vec2 vect = vec2(0.5, 0.7);vec4 result = vec4(vect, 0.0, 0.0);vec4 otherResult = vec4(result.xyz, 1.0);
Vektoren sind somit ein flexibler Datentyp, den wir für alle Arten von Ein- und Ausgaben verwenden können. Im gesamten Buch finden Sie viele Beispiele, wie wir Vektoren kreativ verwalten können.
Ins und outs
Shader sind nette kleine Programme für sich, aber sie sind Teil eines Ganzen und aus diesem Grund möchten wir Ein- und Ausgänge für die einzelnen Shader haben, damit wir Dinge bewegen können. GLSL definierte die Schlüsselwörter in
und out
speziell für diesen Zweck. Jeder Shader kann Ein- und Ausgänge mit diesen Schlüsselwörtern angeben und überall dort, wo eine Ausgabevariable mit einer Eingabevariablen der nächsten Shader-Stufe übereinstimmt, werden sie weitergegeben. Der Vertex- und Fragment-Shader unterscheiden sich jedoch ein wenig.
Der Vertex-Shader sollte irgendeine Form von Eingabe erhalten, sonst wäre er ziemlich ineffektiv. Der Vertex-Shader unterscheidet sich in seiner Eingabe dadurch, dass er seine Eingabe direkt von den Vertex-Daten erhält. Um zu definieren, wie die Scheitelpunktdaten organisiert sind, geben wir die Eingabevariablen mit Standortmetadaten an, damit wir die Scheitelpunktattribute auf der CPU konfigurieren können. Wir haben dies im vorherigen Kapitel als layout (location = 0)
gesehen. Der Vertex-Shader benötigt daher eine zusätzliche Layoutspezifikation für seine Eingaben, damit wir ihn mit den Vertex-Daten verknüpfen können.
Es ist auch möglich, den Spezifiziererlayout (location = 0)
wegzulassen und die Attributpositionen in Ihrem OpenGL-Code über glGetAttribLocation abzufragen, aber ich würde es vorziehen, sie im Vertex-Shader festzulegen. Es ist einfacher zu verstehen und spart Ihnen (und OpenGL) einige Arbeit.
Die andere Ausnahme ist, dass der Fragment-Shader eine vec4
Farbausgabevariable benötigt, da der Fragment-Shader eine endgültige Ausgabefarbe erzeugen muss. Wenn Sie in Ihrem Fragment-Shader keine Ausgabefarbe angeben, ist die Farbpufferausgabe für diese Fragmente nicht definiert (was normalerweise bedeutet, dass OpenGL sie entweder schwarz oder weiß rendert).
Wenn wir also Daten von einem Shader zum anderen senden möchten, müssen wir eine Ausgabe im sendenden Shader und eine ähnliche Eingabe im empfangenden Shader deklarieren. Wenn die Typen und Namen auf beiden Seiten gleich sind, verknüpft OpenGL diese Variablen miteinander und es ist dann möglich, Daten zwischen Shadern zu senden (dies geschieht beim Verknüpfen eines Programmobjekts). Um Ihnen zu zeigen, wie dies in der Praxis funktioniert, werden wir die Shader aus dem vorherigen Kapitel ändern, damit der Vertex-Shader die Farbe für den Fragment-Shader bestimmen kann.
Vertex-Shader
#version 330 corelayout (location = 0) in vec3 aPos; // the position variable has attribute position 0 out vec4 vertexColor; // specify a color output to the fragment shadervoid main(){ gl_Position = vec4(aPos, 1.0); // see how we directly give a vec3 to vec4's constructor vertexColor = vec4(0.5, 0.0, 0.0, 1.0); // set the output variable to a dark-red color}
Fragment-Shader
#version 330 coreout vec4 FragColor; in vec4 vertexColor; // the input variable from the vertex shader (same name and same type) void main(){ FragColor = vertexColor;}
Sie können sehen, dass wir eine vertexColor-Variable als vec4
Ausgabe deklariert haben, die wir im Vertex-Shader festgelegt haben, und wir deklarieren eine ähnliche vertexColor-Eingabe im Fragment-Shader. Da beide den gleichen Typ und Namen haben, ist die vertexColor im Fragment-Shader mit der vertexColor im Vertex-Shader verknüpft. Da wir die Farbe im Vertex-Shader auf eine dunkelrote Farbe setzen, sollten die resultierenden Fragmente ebenfalls dunkelrot sein. Das folgende Bild zeigt die Ausgabe:
Los geht’s! Wir haben es gerade geschafft, einen Wert vom Vertex-Shader an den Fragment-Shader zu senden. Lassen Sie es uns ein wenig aufpeppen und sehen, ob wir eine Farbe aus unserer Anwendung an den Fragment-Shader senden können!
Uniformen
Uniformen sind eine weitere Möglichkeit, Daten von unserer Anwendung auf der CPU an die Shader auf der GPU zu übergeben. Uniformen unterscheiden sich jedoch geringfügig von Vertex-Attributen. Erstens sind Uniformen global. Global, was bedeutet, dass eine einheitliche Variable pro Shader-Programmobjekt eindeutig ist und von jedem Shader in jeder Phase des Shader-Programms aus aufgerufen werden kann. Zweitens behalten Uniformen ihre Werte bei, unabhängig davon, auf welchen Wert Sie den einheitlichen Wert festlegen, bis sie entweder zurückgesetzt oder aktualisiert werden.
Um eine Uniform in GLSL zu deklarieren, fügen wir einfach das Schlüsselwort uniform
zu einem Shader mit einem Typ und einem Namen hinzu. Ab diesem Zeitpunkt können wir die neu deklarierte Uniform im Shader verwenden. Mal sehen, ob wir diesmal die Farbe des Dreiecks über eine Uniform einstellen können:
#version 330 coreout vec4 FragColor; uniform vec4 ourColor; // we set this variable in the OpenGL code.void main(){ FragColor = ourColor;}
Wir haben im Fragment-Shader eine einheitliche vec4
ourColor deklariert und die Ausgabefarbe des Fragments auf den Inhalt dieses einheitlichen Werts gesetzt. Da Uniformen globale Variablen sind, können wir sie in jeder Shader-Phase definieren, die wir möchten, sodass Sie den Vertex-Shader nicht erneut durchlaufen müssen, um etwas an den Fragment-Shader zu gelangen. Wir verwenden diese Uniform nicht im Vertex-Shader, sodass sie dort nicht definiert werden muss.
Wenn Sie eine Uniform deklarieren, die nirgendwo in Ihrem GLSL-Code verwendet wird, entfernt der Compiler die Variable stillschweigend aus der kompilierten Version, was die Ursache für mehrere frustrierende Fehler ist; Denken Sie daran!
Die Uniform ist derzeit leer; Wir haben der Uniform noch keine Daten hinzugefügt, also versuchen wir es mal. Wir müssen zuerst den Index / Speicherort des uniform Attributs in unserem Shader finden. Sobald wir den Index / Speicherort der Uniform haben, können wir ihre Werte aktualisieren. Anstatt eine einzelne Farbe an den Fragment-Shader zu übergeben, sollten wir die Dinge aufpeppen, indem wir die Farbe im Laufe der Zeit schrittweise ändern:
float timeValue = glfwGetTime();float greenValue = (sin(timeValue) / 2.0f) + 0.5f;int vertexColorLocation = glGetUniformLocation(shaderProgram, "ourColor");glUseProgram(shaderProgram);glUniform4f(vertexColorLocation, 0.0f, greenValue, 0.0f, 1.0f);
Zuerst rufen wir die Laufzeit in Sekunden über glfwGetTime() ab. Dann variieren wir die Farbe im Bereich von 0.0
1.0
mit der sin-Funktion und speichern das Ergebnis in greenValue.
Dann fragen wir mit glGetUniformLocation nach der Position der ourColor Uniform . Wir geben das Shader-Programm und den Namen der Uniform (von der wir den Speicherort abrufen möchten) an die Abfragefunktion weiter. Wenn glGetUniformLocation -1
zurückgibt, konnte der Speicherort nicht gefunden werden. Schließlich können wir den einheitlichen Wert mit der glUniform4f Funktion glUniform4f . Beachten Sie, dass Sie zum Finden des einheitlichen Speicherorts nicht zuerst das Shader-Programm verwenden müssen, aber zum Aktualisieren einer Uniform müssen Sie das Programm zuerst verwenden (durch Aufrufen von glUseProgram ), da es die Uniform für das aktuell aktive Shader-Programm festlegt.
Da OpenGL in seinem Kern eine C-Bibliothek ist, hat es keine native Unterstützung für das Überladen von Funktionen, also überall dort, wo eine Funktion mit verschiedenen Typen aufgerufen werden kann, definiert OpenGL neue Funktionen für jeden erforderlichen Typ; glUniform ist ein perfektes Beispiel dafür. Die Funktion erfordert einen bestimmten Postfix für den Typ der Uniform, die Sie festlegen möchten. Einige der möglichen Postfixes sind:
-
f
: Die Funktion erwartet einenfloat
als Wert. -
i
: Die Funktion erwartet einenint
als Wert. -
ui
: Die Funktion erwartet einenunsigned int
als Wert. -
3f
: Die Funktion erwartet 3float
s als Wert. -
fv
: die Funktion erwartet einenfloat
Vektor/Array als Wert.
Wenn Sie eine Option von OpenGL konfigurieren möchten, wählen Sie einfach die überladene Funktion aus, die Ihrem Typ entspricht. In unserem Fall möchten wir 4 Floats der Uniform einzeln setzen, damit wir unsere Daten über glUniform4f (beachten Sie, dass wir auch diefv
Version verwenden könnten).
Jetzt, da wir wissen, wie man die Werte von einheitlichen Variablen setzt, können wir sie zum Rendern verwenden. Wenn wir möchten, dass sich die Farbe allmählich ändert, möchten wir dies mit jedem Frame aktualisieren, andernfalls behält das Dreieck eine einzelne Volltonfarbe bei, wenn wir es nur einmal einstellen. Also berechnen wir den greenValue und aktualisieren die Uniform bei jeder Renderiteration:
while(!glfwWindowShouldClose(window)){ // input processInput(window); // render // clear the colorbuffer glClearColor(0.2f, 0.3f, 0.3f, 1.0f); glClear(GL_COLOR_BUFFER_BIT); // be sure to activate the shader glUseProgram(shaderProgram); // update the uniform color float timeValue = glfwGetTime(); float greenValue = sin(timeValue) / 2.0f + 0.5f; int vertexColorLocation = glGetUniformLocation(shaderProgram, "ourColor"); glUniform4f(vertexColorLocation, 0.0f, greenValue, 0.0f, 1.0f); // now render the triangle glBindVertexArray(VAO); glDrawArrays(GL_TRIANGLES, 0, 3); // swap buffers and poll IO events glfwSwapBuffers(window); glfwPollEvents();}
Der Code ist eine relativ einfache Anpassung des vorherigen Codes. Dieses Mal aktualisieren wir jeden Frame einen einheitlichen Wert, bevor wir das Dreieck zeichnen. Wenn Sie die Uniform korrekt aktualisieren, sollte sich die Farbe Ihres Dreiecks allmählich von Grün zu schwarz und wieder zu Grün ändern.
Überprüfen Sie den Quellcode hier, wenn Sie nicht weiterkommen.
Wie Sie sehen können, sind Uniformen ein nützliches Werkzeug zum Festlegen von Attributen, die jeden Frame ändern können, oder zum Austauschen von Daten zwischen Ihrer Anwendung und Ihren Shadern, aber was ist, wenn wir für jeden Scheitelpunkt eine Farbe festlegen möchten? In diesem Fall müssten wir so viele Uniformen deklarieren, wie wir Eckpunkte haben. Eine bessere Lösung wäre, mehr Daten in die Vertex-Attribute aufzunehmen, was wir jetzt tun werden.
Mehr Attribute!
Wir haben im vorherigen Kapitel gesehen, wie wir einen VBO füllen, Vertex-Attributzeiger konfigurieren und alles in einem VAO speichern können. Dieses Mal möchten wir den Scheitelpunktdaten auch Farbdaten hinzufügen. Wir werden Farbdaten als 3 float
s zum vertices Array hinzufügen. Wir weisen jeder Ecke unseres Dreiecks eine rote, grüne und blaue Farbe zu:
float vertices = { // positions // colors 0.5f, -0.5f, 0.0f, 1.0f, 0.0f, 0.0f, // bottom right -0.5f, -0.5f, 0.0f, 0.0f, 1.0f, 0.0f, // bottom left 0.0f, 0.5f, 0.0f, 0.0f, 0.0f, 1.0f // top };
Da wir jetzt mehr Daten an den Vertex-Shader senden müssen, ist es notwendig, den Vertex-Shader anzupassen, um auch unseren Farbwert als Vertex-Attributeingabe zu erhalten. Beachten Sie, dass wir die Position des AColor-Attributs mit dem Layout-Spezifizierer auf 1 setzen:
#version 330 corelayout (location = 0) in vec3 aPos; // the position variable has attribute position 0layout (location = 1) in vec3 aColor; // the color variable has attribute position 1 out vec3 ourColor; // output a color to the fragment shadervoid main(){ gl_Position = vec4(aPos, 1.0); ourColor = aColor; // set ourColor to the input color we got from the vertex data}
Da wir keine Uniform mehr für die Farbe des Fragments verwenden, sondern jetzt die ourColor-Ausgabevariable verwenden, müssen wir auch den Fragment-Shader ändern:
#version 330 coreout vec4 FragColor; in vec3 ourColor; void main(){ FragColor = vec4(ourColor, 1.0);}
Da wir ein weiteres Vertex-Attribut hinzugefügt und den Speicher des VBO aktualisiert haben, müssen wir konfigurieren Sie die Vertex-Attributzeiger neu. Die aktualisierten Daten im Speicher des VBO sehen jetzt ein bisschen so aus:
Wenn wir das aktuelle Layout kennen, können wir das Scheitelpunktformat mit glVertexAttribPointer aktualisieren:
// position attributeglVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)0);glEnableVertexAttribArray(0);// color attributeglVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)(3* sizeof(float)));glEnableVertexAttribArray(1);
Die ersten Argumente von glVertexAttribPointer sind relativ einfach. Dieses Mal konfigurieren wir das Vertex-Attribut auf attribut location 1
. Die Farbwerte haben eine Größe von 3
float
s und wir normalisieren die Werte nicht.
Da wir nun zwei Vertex-Attribute haben, müssen wir den Stride-Wert neu berechnen. Um den nächsten Attributwert (z. B. die nächste x
Komponente des Positionsvektors) im Datenarray zu erhalten, müssen wir 6
float
s nach rechts verschieben, drei für die Positionswerte und drei für die Farbwerte. Dies gibt uns einen Schrittwert von 6 mal der Größe eines float
in Bytes (= 24
Bytes).
Auch dieses Mal müssen wir einen Offset angeben. Für jeden Scheitelpunkt steht das Attribut position vertex an erster Stelle, daher deklarieren wir einen Offset von 0
. Das Farbattribut beginnt nach den Positionsdaten, so dass der Offset 3 * sizeof(float)
in Bytes (= 12
Bytes) ist.
Das Ausführen der Anwendung sollte zu folgendem Bild führen:
Überprüfen Sie den Quellcode hier, wenn Sie nicht weiterkommen.
Das Bild ist möglicherweise nicht genau das, was Sie erwarten würden, da wir nur 3 Farben geliefert haben, nicht die riesige Farbpalette, die wir gerade sehen. Dies ist alles das Ergebnis einer sogenannten Fragmentinterpolation im Fragment-Shader. Beim Rendern eines Dreiecks führt die Rasterisierungsstufe normalerweise zu viel mehr Fragmenten als ursprünglich angegeben. Der Rasterizer bestimmt dann die Positionen jedes dieser Fragmente basierend darauf, wo sie sich auf der Dreiecksform befinden.
Basierend auf diesen Positionen werden alle Eingabevariablen des Fragment-Shaders interpoliert. Angenommen, wir haben eine Linie, bei der der obere Punkt grün und der untere Punkt blau ist. Wenn der Fragment-Shader an einem Fragment ausgeführt wird, das sich um eine Position bei 70%
der Zeile befindet, wäre das resultierende Farbeingabeattribut eine lineare Kombination aus Grün und Blau. genauer gesagt: 30%
blau und 70%
grün.
Genau das ist am Dreieck passiert. Wir haben 3 Eckpunkte und damit 3 Farben, und nach den Pixeln des Dreiecks zu urteilen, enthält es wahrscheinlich etwa 50000 Fragmente, wobei der Fragment-Shader die Farben zwischen diesen Pixeln interpoliert hat. Wenn Sie sich die Farben genau ansehen, werden Sie feststellen, dass alles Sinn macht: Rot zu Blau wird zuerst zu Lila und dann zu Blau. Die Fragmentinterpolation wird auf alle Eingabeattribute des Fragment-Shaders angewendet.
Unsere eigene Shader-Klasse
Das Schreiben, Kompilieren und Verwalten von Shadern kann ziemlich umständlich sein. Als letzten Schliff zum Thema Shader werden wir unser Leben ein bisschen einfacher machen, indem wir eine Shader-Klasse erstellen, die Shader von der Festplatte liest, kompiliert und verknüpft, auf Fehler prüft und einfach zu bedienen ist. Dies gibt Ihnen auch eine Vorstellung davon, wie wir einige der bisher gelernten Kenntnisse in nützliche abstrakte Objekte kapseln können.
Wir werden die Shader-Klasse vollständig in einer Header-Datei erstellen, hauptsächlich zu Lernzwecken und zur Portabilität. Beginnen wir mit dem Hinzufügen der erforderlichen Includes und der Definition der Klassenstruktur:
#ifndef SHADER_H#define SHADER_H#include <glad/glad.h> // include glad to get all the required OpenGL headers #include <string>#include <fstream>#include <sstream>#include <iostream> class Shader{public: // the program ID unsigned int ID; // constructor reads and builds the shader Shader(const char* vertexPath, const char* fragmentPath); // use/activate the shader void use(); // utility uniform functions void setBool(const std::string &name, bool value) const; void setInt(const std::string &name, int value) const; void setFloat(const std::string &name, float value) const;}; #endif
Wir haben oben in der Header-Datei mehrere Präprozessor-Direktiven verwendet. Die Verwendung dieser kleinen Codezeilen informiert Ihren Compiler, diese Header-Datei nur einzuschließen und zu kompilieren, wenn sie noch nicht enthalten ist, selbst wenn mehrere Dateien den Shader-Header enthalten. Dies verhindert Verknüpfungskonflikte.
Die Shader-Klasse enthält die ID des Shader-Programms. Sein Konstruktor benötigt die Dateipfade des Quellcodes des Vertex- bzw. Fragment-Shaders, die wir als einfache Textdateien auf der Festplatte speichern können. Um ein wenig mehr hinzuzufügen, fügen wir auch mehrere Utility-Funktionen hinzu, um unser Leben ein wenig zu erleichtern: use aktiviert das Shader-Programm und alles eingestellt… funktionen fragen eine einheitliche Position ab und legen ihren Wert fest.
Lesen aus Datei
Wir verwenden C ++ Filestreams, um den Inhalt der Datei in mehrere string
Objekte zu lesen:
Shader(const char* vertexPath, const char* fragmentPath){ // 1. retrieve the vertex/fragment source code from filePath std::string vertexCode; std::string fragmentCode; std::ifstream vShaderFile; std::ifstream fShaderFile; // ensure ifstream objects can throw exceptions: vShaderFile.exceptions (std::ifstream::failbit | std::ifstream::badbit); fShaderFile.exceptions (std::ifstream::failbit | std::ifstream::badbit); try { // open files vShaderFile.open(vertexPath); fShaderFile.open(fragmentPath); std::stringstream vShaderStream, fShaderStream; // read file's buffer contents into streams vShaderStream << vShaderFile.rdbuf(); fShaderStream << fShaderFile.rdbuf(); // close file handlers vShaderFile.close(); fShaderFile.close(); // convert stream into string vertexCode = vShaderStream.str(); fragmentCode = fShaderStream.str(); } catch(std::ifstream::failure e) { std::cout << "ERROR::SHADER::FILE_NOT_SUCCESFULLY_READ" << std::endl; } const char* vShaderCode = vertexCode.c_str(); const char* fShaderCode = fragmentCode.c_str();
Als nächstes müssen wir die Shader kompilieren und verknüpfen. Beachten Sie, dass wir auch überprüfen, ob die Kompilierung / Verknüpfung fehlgeschlagen ist, und wenn ja, die Kompilierungsfehler drucken. Dies ist äußerst nützlich beim Debuggen (Sie werden diese Fehlerprotokolle schließlich benötigen):
// 2. compile shadersunsigned int vertex, fragment;int success;char infoLog; // vertex Shadervertex = glCreateShader(GL_VERTEX_SHADER);glShaderSource(vertex, 1, &vShaderCode, NULL);glCompileShader(vertex);// print compile errors if anyglGetShaderiv(vertex, GL_COMPILE_STATUS, &success);if(!success){ glGetShaderInfoLog(vertex, 512, NULL, infoLog); std::cout << "ERROR::SHADER::VERTEX::COMPILATION_FAILED\n" << infoLog << std::endl;}; // similiar for Fragment Shader // shader ProgramID = glCreateProgram();glAttachShader(ID, vertex);glAttachShader(ID, fragment);glLinkProgram(ID);// print linking errors if anyglGetProgramiv(ID, GL_LINK_STATUS, &success);if(!success){ glGetProgramInfoLog(ID, 512, NULL, infoLog); std::cout << "ERROR::SHADER::PROGRAM::LINKING_FAILED\n" << infoLog << std::endl;} // delete the shaders as they're linked into our program now and no longer necessaryglDeleteShader(vertex);glDeleteShader(fragment);
Die use-Funktion ist unkompliziert:
void use() { glUseProgram(ID);}
Ähnlich für jede der Uniform Setter-Funktionen:
void setBool(const std::string &name, bool value) const{ glUniform1i(glGetUniformLocation(ID, name.c_str()), (int)value); }void setInt(const std::string &name, int value) const{ glUniform1i(glGetUniformLocation(ID, name.c_str()), value); }void setFloat(const std::string &name, float value) const{ glUniform1f(glGetUniformLocation(ID, name.c_str()), value); }
Und da haben wir es, eine abgeschlossene Shader-Klasse. Die Verwendung der Shader-Klasse ist ziemlich einfach; Wir erstellen einmal ein Shader-Objekt und beginnen es von diesem Punkt an einfach zu verwenden:
Shader ourShader("path/to/shaders/shader.vs", "path/to/shaders/shader.fs");while(...){ ourShader.use(); ourShader.setFloat("someUniform", 1.0f); DrawStuff();}
Hier haben wir den Vertex- und Fragment-Shader-Quellcode in zwei Dateien mit dem Namen shader.vs
und shader.fs
gespeichert. Sie können Ihre Shader-Dateien nach Belieben benennen; Ich persönlich finde die Erweiterungen .vs
und .fs
ziemlich intuitiv.
Den Quellcode finden Sie hier mit unserer neu erstellten Shader-Klasse. Beachten Sie, dass Sie auf die Shader-Dateipfade klicken können, um den Quellcode der Shader zu finden.
Übungen
- Passen Sie den Vertex-Shader so an, dass das Dreieck auf dem Kopf steht: Lösung.
- Geben Sie einen horizontalen Versatz über eine Uniform an und verschieben Sie das Dreieck mit diesem Versatzwert im Vertex-Shader auf die rechte Seite des Bildschirms: Lösung.
- Geben Sie die Scheitelpunktposition mit dem Schlüsselwort
out
an den Fragment-Shader aus und setzen Sie die Farbe des Fragments auf diese Scheitelpunktposition (sehen Sie, wie sogar die Scheitelpunktpositionswerte über das Dreieck interpoliert werden). Sobald Sie dies geschafft haben; Versuchen Sie, die folgende Frage zu beantworten: Warum ist die untere linke Seite unseres Dreiecks schwarz?: Lösung.