zoals vermeld in het hoofdstuk Hello Triangle, zijn shaders kleine programma ‘ s die op de GPU rusten. Deze programma ‘ s worden uitgevoerd voor elke specifieke sectie van de grafische pijplijn. In een fundamentele zin, shaders zijn niets meer dan Programma ‘ s transformeren ingangen naar uitgangen. Shaders zijn ook erg geïsoleerde programma ‘ s in die zin dat ze niet met elkaar mogen communiceren; de enige communicatie die ze hebben is via hun in-en uitgangen.
In het vorige hoofdstuk hebben we kort het oppervlak van shaders aangeraakt en hoe ze goed te gebruiken. We zullen nu shaders, en in het bijzonder de OpenGL Shading Language, op een meer algemene manier uitleggen.
GLSL
Shaders zijn geschreven in de C-achtige taal GLSL. GLSL is op maat gemaakt voor gebruik met afbeeldingen en bevat handige functies die specifiek gericht zijn op vector-en matrixmanipulatie.
Shaders beginnen altijd met een versiedeclaratie, gevolgd door een lijst van invoer-en uitvoervariabelen, uniformen en zijn hoofdfunctie. Het ingangspunt van elke shader bevindt zich op zijn hoofdfunctie waar we alle invoervariabelen verwerken en de resultaten in de uitvoervariabelen uitvoeren. Maak je geen zorgen als je niet weet wat uniformen zijn, daar komen we zo aan.
een shader heeft meestal de volgende structuur:
#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;}
wanneer we het specifiek hebben over de vertex shader wordt elke input variabele ook wel een vertex attribuut genoemd. Er is een maximum aantal vertex attributen die we mogen verklaren beperkt door de hardware. OpenGL garandeert dat er altijd ten minste 16 4-component vertex attributen beschikbaar zijn, maar sommige hardware kan zorgen voor meer die u kunt ophalen door gl_max_vertex_attribs te bevragen:
int nrAttributes;glGetIntegerv(GL_MAX_VERTEX_ATTRIBS, &nrAttributes);std::cout << "Maximum nr of vertex attributes supported: " << nrAttributes << std::endl;
Dit geeft vaak het minimum van 16
terug, wat meer dan genoeg zou moeten zijn voor de meeste doeleinden.
Types
GLSL heeft, net als elke andere programmeertaal, gegevenstypen om aan te geven met welk soort variabele we willen werken. GLSL heeft de meeste standaard basistypen die we kennen van talen zoals C: int
float
double
uint
en bool
. GLSL bevat ook twee containertypes die we veel zullen gebruiken, namelijk vectors
en matrices
. We zullen matrices bespreken in een later hoofdstuk.
vectoren
een vector in GLSL is een container met 1,2,3 of 4 componenten voor een van de zojuist genoemde basistypes. Ze kunnen de volgende vorm aannemen (n
vertegenwoordigt het aantal componenten):
-
vecn
: de standaardvector vann
drijft. -
bvecn
: een vector vann
booleans. -
ivecn
: een vector vann
gehele getallen. -
uvecn
: een vector vann
gehele getallen zonder teken. -
dvecn
: een vector vann
dubbele componenten.
meestal gebruiken we de basis vecn
omdat floats voldoende zijn voor de meeste van onze doeleinden.
componenten van een vector zijn toegankelijk via vec.x
waarbij x
het eerste component van de vector is. U kunt .x
.y
.z
en .w
gebruiken om toegang te krijgen tot respectievelijk hun eerste, tweede, derde en vierde component. Met GLSL kunt u ook rgba
gebruiken voor kleuren of stpq
voor textuurcoördinaten, met toegang tot dezelfde componenten.
Het Vector datatype zorgt voor een interessante en flexibele component selectie genaamd swizzling. Swizzling stelt ons in staat om syntaxis als deze te gebruiken:
vec2 someVec;vec4 differentVec = someVec.xyxx;vec3 anotherVec = differentVec.zyw;vec4 otherVec = someVec.xxxx + anotherVec.yxzy;
u kunt elke combinatie van maximaal 4 letters gebruiken om een nieuwe vector (van hetzelfde type) aan te maken zolang de oorspronkelijke vector deze componenten heeft; het is bijvoorbeeld niet toegestaan om toegang te krijgen tot het .z
component van een vec2
. We kunnen vectoren ook als argumenten doorgeven aan verschillende vectorconstructeuraanroepen, waardoor het aantal vereiste argumenten wordt verminderd:
vec2 vect = vec2(0.5, 0.7);vec4 result = vec4(vect, 0.0, 0.0);vec4 otherResult = vec4(result.xyz, 1.0);
vectoren zijn dus een flexibel datatype dat we kunnen gebruiken voor alle soorten invoer en uitvoer. Doorheen het boek zie je tal van voorbeelden van hoe we vectoren creatief kunnen beheren.
ins en outs
Shaders zijn leuke kleine programma ‘ s op zichzelf, maar ze maken deel uit van een geheel en om die reden willen we ingangen en uitgangen hebben op de individuele shaders zodat we dingen kunnen verplaatsen. GLSL definieerde dein
enout
sleutelwoorden specifiek voor dat doel. Elke shader kan ingangen en uitgangen opgeven met behulp van deze zoekwoorden en waar een output variabele overeenkomt met een input variabele van de volgende shader fase ze worden doorgegeven. De vertex en fragment shader verschillen echter een beetje.
de vertex-shader zou enige vorm van invoer moeten ontvangen, anders zou het behoorlijk ineffectief zijn. De vertex shader verschilt in zijn input, in die zin dat het zijn input rechtstreeks van de vertex data ontvangt. Om te bepalen hoe de vertex gegevens worden georganiseerd specificeren we de invoervariabelen met locatie metadata, zodat we de vertex attributen op de CPU kunnen configureren. We hebben dit in het vorige hoofdstuk gezien als layout (location = 0)
. De vertex shader vereist dus een extra lay-out specificatie voor zijn ingangen, zodat we het kunnen koppelen met de vertex data.
het is ook mogelijk om delayout (location = 0)
specifier weg te laten en de attribuutlocaties in uw OpenGL-code op te vragen via glGetAttribLocation, maar ik stel ze liever in de vertex-shader. Het is gemakkelijker te begrijpen en bespaart u (en OpenGL) wat werk.
de andere uitzondering is dat de fragment shader een vec4
kleuruitvoervariabele vereist, omdat de fragment shaders een uiteindelijke uitvoerkleur moeten genereren. Als u geen uitvoerkleur opgeeft in uw fragment-shader, is de uitvoer van de kleurbuffer voor die fragmenten niet gedefinieerd (wat meestal betekent dat OpenGL ze zwart of wit maakt).
dus als we gegevens van de ene shader naar de andere willen verzenden, moeten we een uitvoer in de verzendende shader en een soortgelijke invoer in de ontvangende shader declareren. Wanneer de types en de namen aan beide zijden gelijk zijn zal OpenGL deze variabelen aan elkaar koppelen en dan is het mogelijk om gegevens tussen shaders te sturen (Dit wordt gedaan bij het koppelen van een programma object). Om je te laten zien hoe dit in de praktijk werkt gaan we de shaders uit het vorige hoofdstuk veranderen om de vertex shader de kleur voor de fragment shader te laten bepalen.
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;}
u kunt zien dat we een vertexColor variabele hebben gedeclareerd als een vec4
uitvoer die we hebben ingesteld in de vertex shader en we verklaren een soortgelijke vertexColor invoer in de fragment shader. Omdat ze allebei hetzelfde type en dezelfde naam hebben, is de vertexColor in de fragment shader gekoppeld aan de vertexColor in de vertex shader. Omdat we de kleur in de vertex shader op een donkerrode kleur zetten, moeten de resulterende fragmenten ook donkerrood zijn. De volgende afbeelding toont de uitvoer:
daar gaan we! We zijn er net in geslaagd om een waarde van de vertex shader naar de fragment shader te sturen. Laten we het wat pittiger maken en kijken of we een kleur van onze applicatie naar de fragment shader kunnen sturen!
uniformen
uniformen zijn een andere manier om gegevens van onze toepassing op de CPU door te geven aan de shaders op de GPU. Uniformen zijn echter iets anders dan vertex attributen. Ten eerste, uniformen zijn wereldwijd. Globaal, wat betekent dat een uniforme variabele is uniek per shader programma object, en kan worden benaderd vanuit elke shader in elk stadium van het shader programma. Ten tweede, wat je de uniforme waarde ook instelt, uniformen zullen hun waarden behouden totdat ze ofwel worden gereset of bijgewerkt.
om een uniform te verklaren in GLSL voegen we gewoon het uniform
sleutelwoord toe aan een shader met een type en een naam. Vanaf dat moment kunnen we het nieuw gedeclareerde uniform in de shader gebruiken. Laten we eens kijken of we deze keer de kleur van de driehoek kunnen instellen via een uniform:
#version 330 coreout vec4 FragColor; uniform vec4 ourColor; // we set this variable in the OpenGL code.void main(){ FragColor = ourColor;}
we hebben een uniforme vec4
onze kleur in de fragment shader gedeclareerd en de uitvoerkleur van het fragment ingesteld op de inhoud van deze uniforme waarde. Aangezien uniformen globale variabelen zijn, kunnen we ze definiëren in elke shader-fase die we willen, dus je hoeft niet opnieuw door de vertex-shader te gaan om iets naar de fragment-shader te krijgen. We gebruiken dit uniform niet in de vertex shader, dus je hoeft het daar niet te definiëren.
als je een uniform declareert dat nergens in je GLSL code wordt gebruikt, zal de compiler stilletjes de variabele uit de gecompileerde versie verwijderen, wat de oorzaak is van een aantal frustrerende fouten; houd dit in gedachten!
het uniform is momenteel leeg; we hebben nog geen gegevens aan het uniform toegevoegd, dus laten we dat proberen. We moeten eerst de index/locatie van het uniform attribuut in onze shader vinden. Zodra we de index/locatie van het uniform hebben, kunnen we de waarden bijwerken. In plaats van het doorgeven van een enkele kleur aan de fragment shader, laten we spice dingen door geleidelijk veranderen van kleur na verloop van tijd:
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);
eerst halen we de looptijd in seconden op via glfwGetTime(). Vervolgens variëren we de kleur in het bereik van 0.0
1.0
met behulp van de sin-functie en slaan het resultaat op in greenValue.
dan vragen we naar de locatie van het ourcolor uniform met behulp van glgetuniformlocatie. We leveren het shader programma en de naam van het uniform (waarvan we de locatie willen ophalen) aan de query functie. Als glGetUniformLocation -1
retourneert, kon het de locatie niet vinden. Tot slot kunnen we de uniforme waarde instellen met behulp van de glUniform4f-functie. Merk op dat het vinden van de uniforme locatie niet vereist dat je eerst het shader programma gebruikt, maar het bijwerken van een uniform vereist wel dat je eerst het programma gebruikt (door glUseProgram aan te roepen), omdat het het uniform instelt op het huidige actieve shader programma.
omdat OpenGL in de kern een C bibliotheek is, heeft het geen native ondersteuning voor functie overloading, dus waar een functie met verschillende types aangeroepen kan worden, definieert OpenGL nieuwe functies voor elk type dat nodig is; glUniform is een perfect voorbeeld hiervan. De functie vereist een specifieke postfix voor het type uniform dat u wilt instellen. Enkele mogelijke postfixes zijn:
-
f
: de functie verwacht eenfloat
als waarde. -
i
: de functie verwacht als waarde eenint
. -
ui
: de functie verwacht als waarde eenunsigned int
. -
3f
: de functie verwacht 3float
s als waarde. -
fv
: de functie verwacht eenfloat
vector/array als zijn waarde.
wanneer u een optie van OpenGL wilt configureren, kiest u gewoon de overbelaste functie die overeenkomt met uw type. In ons geval willen we 4 floats van het uniform individueel instellen zodat we onze gegevens doorgeven via glUniform4f (merk op dat we ook defv
versie hadden kunnen gebruiken).
nu we weten hoe we de waarden van uniforme variabelen moeten instellen, kunnen we ze gebruiken voor het renderen. Als we willen dat de kleur geleidelijk verandert, willen we dit uniform elk frame bijwerken, anders zou de driehoek een enkele effen kleur behouden als we het maar één keer instellen. Dus we berekenen de greenValue en werken de uniform elke render iteratie bij:
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();}
de code is een relatief eenvoudige aanpassing van de vorige code. Deze keer werken we een uniforme waarde bij elk frame voordat we de driehoek tekenen. Als u het uniform correct bijwerkt, ziet u de kleur van uw driehoek geleidelijk veranderen van groen naar zwart en weer terug naar groen.
Bekijk hier de broncode als u vastzit.
zoals u kunt zien, zijn Uniformen een handig hulpmiddel voor het instellen van attributen die elk frame kunnen veranderen, of voor het uitwisselen van gegevens tussen uw toepassing en uw shaders, maar wat als we een kleur willen instellen voor elk hoekpunt? In dat geval moeten we zoveel uniformen aangeven als we hoekpunten hebben. Een betere oplossing zou zijn om meer gegevens in de vertex attributen op te nemen, wat we nu gaan doen.
meer attributen!
we zagen in het vorige hoofdstuk hoe we een VBO kunnen vullen, vertex attribuut pointers kunnen configureren en het allemaal in een VAO kunnen opslaan. Deze keer willen we ook kleurgegevens toevoegen aan de vertex-gegevens. We gaan kleurgegevens toevoegen als 3 float
s aan de hoekpunten array. We kennen een rode, groene en blauwe kleur toe aan elk van de hoeken van onze driehoek respectievelijk:
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 };
aangezien we nu meer gegevens hebben om naar de vertex-shader te sturen, is het noodzakelijk om de vertex-shader aan te passen om ook onze kleurwaarde als vertex-attribuut-invoer te ontvangen. Merk op dat we de locatie van de aColor kenmerk 1 met de lay-aanduiding:
#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}
Omdat we niet langer gebruik maken van een uniform voor het fragment van de kleur, maar nu gebruik maken van de ourColor output variabele moeten we veranderen de fragment shader ook:
#version 330 coreout vec4 FragColor; in vec3 ourColor; void main(){ FragColor = vec4(ourColor, 1.0);}
Want we hebben nog een andere vertex kenmerk en de VBO geheugen hebben we opnieuw configureren van de vertex kenmerk van pointers. De bijgewerkte data in het geheugen van de VBO ziet er nu een beetje zo uit:
wetende de huidige lay-out kunnen we het vertex-formaat bijwerken met glVertexAttribPointer:
// 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);
de eerste paar argumenten van glVertexAttribPointer zijn relatief eenvoudig. Deze keer configureren we het vertex attribuut op attribuutlocatie 1
. De kleurwaarden hebben een grootte van 3
float
s en we normaliseren de waarden niet.
omdat we nu twee vertex attributen hebben, moeten we de stride waarde opnieuw berekenen. Om de volgende attribuutwaarde te krijgen (bijvoorbeeld de volgende x
component van de positievector) in de data array moeten we 6
float
s naar rechts verplaatsen, drie voor de positiewaarden en drie voor de kleurwaarden. Dit geeft ons een stapwaarde van 6 keer de grootte van een float
in bytes (= 24
bytes).
ook moeten we deze keer een offset specificeren. Voor elk vertex is het attribuut positie vertex eerst dus verklaren we een offset van 0
. Het kenmerk kleur begint na de positiegegevens, zodat de verschuiving 3 * sizeof(float)
in bytes is (= 12
bytes).
het uitvoeren van de toepassing moet resulteren in de volgende afbeelding:
Bekijk hier de broncode als u vastzit.
de afbeelding is misschien niet precies wat je zou verwachten, omdat we slechts 3 kleuren hebben geleverd, niet het enorme kleurenpalet dat we nu zien. Dit is allemaal het resultaat van iets genaamd fragment interpolatie in de fragment shader. Bij het renderen van een driehoek resulteert de rasterisatie fase meestal in veel meer fragmenten dan hoekpunten oorspronkelijk opgegeven. De rasterizer bepaalt vervolgens de posities van elk van deze fragmenten op basis van waar ze zich bevinden op de driehoek vorm.
gebaseerd op deze posities, interpoleert het alle invoervariabelen van de fragment shader. Stel dat we bijvoorbeeld een lijn hebben waar het bovenste punt een groene kleur heeft en het onderste punt een blauwe kleur. Als de fragment-shader wordt uitgevoerd op een fragment dat zich rond een positie op 70%
van de lijn bevindt, zou het resulterende kleurinvoerattribuut een lineaire combinatie van groen en blauw zijn; om precies te zijn: 30%
blauw en 70%
groen.
Dit is precies wat er gebeurde bij de driehoek. We hebben 3 hoekpunten en dus 3 kleuren, en te oordelen naar de pixels van de driehoek bevat het waarschijnlijk ongeveer 50000 fragmenten, waar de fragment shader de kleuren tussen die pixels interpoleerde. Als je goed naar de kleuren kijkt zie je dat het allemaal logisch is: rood naar blauw wordt eerst paars en dan blauw. Fragment interpolatie wordt toegepast op alle invoerattributen van de fragment shader.
onze eigen shader klasse
het schrijven, compileren en beheren van shaders kan behoorlijk omslachtig zijn. Als laatste aanraking op de shader onderwerp gaan we ons leven een beetje gemakkelijker te maken door het bouwen van een shader klasse die shaders leest van schijf, compileert en koppelt ze, controleert op fouten en is makkelijk te gebruiken. Dit geeft je ook een beetje een idee hoe we een deel van de kennis die we tot nu toe hebben geleerd, kunnen inkapselen in nuttige abstracte objecten.
We zullen de shader-Klasse volledig in een header-bestand aanmaken, voornamelijk voor leerdoeleinden en draagbaarheid. Laten we beginnen met het toevoegen van de vereiste includes en door het definiëren van de class structuur:
#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
we gebruikten verschillende preprocessor richtlijnen aan de bovenkant van het header bestand. Met behulp van deze kleine regels code informeert uw compiler om alleen deze header bestand op te nemen en te compileren als het nog niet is opgenomen, zelfs als meerdere bestanden bevatten de shader header. Dit voorkomt het koppelen van conflicten.
de shader klasse bevat de ID van het shader programma. De constructor vereist de bestandspaden van de broncode van de vertex en fragment shader respectievelijk die we kunnen opslaan op de schijf als eenvoudige tekstbestanden. Om een beetje extra toe te voegen voegen we ook een aantal utility functies om ons leven een beetje te verlichten: gebruik activeert de shader programma, en alles is ingesteld… functies vragen een uniforme locatie op en stellen de waarde ervan in.
lezen uit bestand
We gebruiken C++ filestreams om de inhoud van het bestand in verschillende string
objecten te lezen:
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();
vervolgens moeten we de shaders compileren en koppelen. Merk op dat we ook controleren of compilatie/koppeling mislukt is en als dat zo is, print dan de compilatie-tijd fouten. Dit is zeer nuttig bij het debuggen (u zult deze foutlogboeken uiteindelijk nodig hebben):
// 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);
De functie gebruik is eenvoudig:
void use() { glUseProgram(ID);}
hetzelfde geldt voor alle uniforme setter functies:
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); }
en daar hebben we het, een voltooide shader klasse. Het gebruik van de shader klasse is vrij eenvoudig; we maken een shader object en vanaf dat moment gewoon beginnen met het te gebruiken:
Shader ourShader("path/to/shaders/shader.vs", "path/to/shaders/shader.fs");while(...){ ourShader.use(); ourShader.setFloat("someUniform", 1.0f); DrawStuff();}
Hier slaan we de vertex en fragment shader broncode op in twee bestanden genaamd shader.vs
en shader.fs
. U bent vrij om de naam van uw shader bestanden zoals u wilt; Persoonlijk vind ik de extensies .vs
en .fs
vrij intuïtief.
u kunt de broncode hier vinden met behulp van onze nieuw aangemaakte shader klasse. Merk op dat u op de shader bestandspaden kunt klikken om de broncode van de shaders te vinden.
oefeningen
- Pas de vertex-shader aan zodat de driehoek ondersteboven is: oplossing.
- Specificeer een horizontale offset via een uniform en verplaats de driehoek naar de rechterkant van het scherm in de vertex shader met behulp van deze offset waarde: oplossing.
- Voer de vertex positie uit naar de fragment shader met behulp van het
out
sleutelwoord en stel de kleur van het fragment gelijk aan deze vertex positie (zie hoe zelfs de vertex positie waarden worden geïnterpoleerd over de driehoek). Als het je eenmaal gelukt is om dit te doen, probeer dan de volgende vraag te beantwoorden: Waarom is de linkerbenedenzijde van onze driehoek zwart?: oplossing.