som nämnts i Hello Triangle-kapitlet är shaders små program som vilar på GPU. Dessa program körs för varje specifik del av grafikpipelinen. I en grundläggande mening är shaders inget annat än program som omvandlar ingångar till utgångar. Shaders är också mycket isolerade program genom att de inte får kommunicera med varandra; den enda kommunikationen de har är via sina ingångar och utgångar.
i föregående kapitel berörde vi kort ytan på shaders och hur man använder dem korrekt. Vi kommer nu att förklara shaders, och specifikt OpenGL-Skuggningsspråket, på ett mer allmänt sätt.
GLSL
Shaders är skrivna på det C-liknande språket GLSL. GLSL är skräddarsydd för användning med grafik och innehåller användbara funktioner som är specifikt inriktade på vektor-och matrismanipulation.
Shaders börjar alltid med en versionsdeklaration, följt av en lista över inmatnings-och utgångsvariabler, uniformer och dess huvudfunktion. Varje shaders ingångspunkt är vid sin huvudsakliga funktion där vi bearbetar alla ingångsvariabler och matar ut resultaten i dess utgångsvariabler. Oroa dig inte om du inte vet vad uniformer är, vi kommer till dem inom kort.
en shader har vanligtvis följande 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;}
När vi pratar specifikt om vertex shader är varje ingångsvariabel också känd som ett vertexattribut. Det finns ett maximalt antal vertex-attribut som vi får deklarera begränsat av hårdvaran. OpenGL garanterar att det alltid finns minst 16 4-komponent vertex attribut tillgängliga, men viss hårdvara kan möjliggöra mer som du kan hämta genom att fråga GL_MAX_VERTEX_ATTRIBS:
int nrAttributes;glGetIntegerv(GL_MAX_VERTEX_ATTRIBS, &nrAttributes);std::cout << "Maximum nr of vertex attributes supported: " << nrAttributes << std::endl;
detta returnerar ofta minst 16
vilket bör vara mer än tillräckligt för de flesta ändamål.
typer
GLSL har, som alla andra programmeringsspråk, datatyper för att specificera vilken typ av variabel vi vill arbeta med. GLSL har de flesta av de grundläggande standardtyperna vi känner från språk som C: int
float
double
uint
och bool
. GLSL har också två containertyper som vi kommer att använda mycket, nämligen vectors
och matrices
. Vi kommer att diskutera matriser i ett senare kapitel.
vektorer
en vektor i GLSL är en 1,2,3 eller 4 komponentbehållare för någon av de grundläggande typerna som just nämnts. De kan ha följande form (n
representerar antalet komponenter):
-
vecn
: standardvektorn förn
flyter. -
bvecn
: en vektor avn
booleaner. -
ivecn
: en vektor avn
heltal. -
uvecn
: en vektor avn
osignerade heltal. -
dvecn
: en vektor avn
dubbla komponenter.
För det mesta kommer vi att använda det grundläggande vecn
eftersom flottor är tillräckliga för de flesta av våra syften.
komponenter i en vektor kan nås via vec.x
där x
är den första komponenten i vektorn. Du kan använda .x
.y
.z
och .w
för att komma åt deras första, andra, tredje respektive fjärde komponent. GLSL låter dig också använda rgba
för färger eller stpq
för texturkoordinater, åtkomst till samma komponenter.
vektordatatypen möjliggör ett intressant och flexibelt komponentval som kallas swizzling. Swizzling tillåter oss att använda syntax så här:
vec2 someVec;vec4 differentVec = someVec.xyxx;vec3 anotherVec = differentVec.zyw;vec4 otherVec = someVec.xxxx + anotherVec.yxzy;
Du kan använda valfri kombination av upp till 4 bokstäver för att skapa en ny vektor (av samma typ) så länge den ursprungliga vektorn har dessa komponenter; det är inte tillåtet att komma åt .z
komponent i en vec2
till exempel. Vi kan också skicka vektorer som argument till olika vektorkonstruktörsamtal, vilket minskar antalet argument som krävs:
vec2 vect = vec2(0.5, 0.7);vec4 result = vec4(vect, 0.0, 0.0);vec4 otherResult = vec4(result.xyz, 1.0);
vektorer är alltså en flexibel datatyp som vi kan använda för alla typer av inmatning och utmatning. Under hela boken ser du många exempel på hur vi kreativt kan hantera vektorer.
Ins och outs
Shaders är trevliga små program på egen hand, men de är en del av en helhet och av den anledningen vill vi ha ingångar och utgångar på de enskilda shadersna så att vi kan flytta saker runt. GLSL definieradein
ochout
nyckelord specifikt för detta ändamål. Varje shader kan ange ingångar och utgångar med hjälp av dessa sökord och varhelst en utgångsvariabel matchar med en ingångsvariabel för nästa shader-steg de skickas vidare. Vertex och fragment shader skiljer sig dock lite.
vertex shader bör få någon form av inmatning annars skulle det vara ganska ineffektivt. Vertex shader skiljer sig åt i sin ingång, genom att den tar emot sin ingång direkt från vertexdata. För att definiera hur vertexdata är organiserad anger vi inmatningsvariablerna med platsmetadata så att vi kan konfigurera vertexattributen på CPU. Vi har sett detta i föregående kapitel som layout (location = 0)
. Vertex shader kräver således en extra layoutspecifikation för dess ingångar så att vi kan länka den med vertexdata.
det är också möjligt att utelämnalayout (location = 0)
specifier och Fråga för attributplatserna i din OpenGL-kod via glGetAttribLocation, men jag föredrar att ställa in dem i vertex shader. Det är lättare att förstå och sparar dig (och OpenGL) lite arbete.
det andra undantaget är att fragmentskuggaren kräver envec4
färgutgångsvariabel, eftersom fragmentskuggarna behöver generera en slutlig utgångsfärg. Om du misslyckas med att ange en utgångsfärg i din fragmentskuggarekommer färgbuffertutgången för dessa fragment att vara odefinierad (vilket vanligtvis betyder att OpenGL kommer att göra dem antingen svarta eller vita).
Så om vi vill skicka data från en shader till den andra måste vi deklarera en utgång i den sändande shader och en liknande ingång i den mottagande shader. När typerna och namnen är lika på båda sidor kommer OpenGL att länka dessa variabler tillsammans och då är det möjligt att skicka data mellan shaders (detta görs när man länkar ett programobjekt). För att visa dig hur detta fungerar i praktiken kommer vi att ändra shadersna från föregående kapitel för att låta vertex shader bestämma färgen för fragmentet shader.
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;}
Du kan se att vi förklarade en vertexcolor-variabel som en vec4
utgång som vi ställer in i vertex shader och vi förklarar en liknande vertexColor-ingång i fragment shader. Eftersom de båda har samma typ och namn, är vertexColor i fragment shader kopplad till vertexColor i vertex shader. Eftersom vi ställer in färgen till en mörkröd färg i vertex shader, bör de resulterande fragmenten också vara mörkröda. Följande bild visar utgången:
Där går vi! Vi lyckades bara skicka ett värde från vertex shader till fragment shader. Låt oss krydda upp det lite och se om vi kan skicka en färg från vår ansökan till fragment shader!
uniformer
uniformer är ett annat sätt att skicka data från vår applikation på CPU till shadersna på GPU. Uniformer är dock något annorlunda jämfört med vertex attribut. För det första är uniformer globala. Global, vilket innebär att en enhetlig variabel är unik per shader-programobjekt och kan nås från vilken shader som helst i shader-programmet. För det andra, oavsett vad du ställer in det enhetliga värdet till, kommer uniformer att behålla sina värden tills de antingen återställs eller uppdateras.
för att deklarera en uniform i GLSL lägger vi helt enkelt tilluniform
nyckelordet till en shader med en typ och ett namn. Från den tiden kan vi använda den nyligen deklarerade uniformen i shader. Låt oss se om den här gången kan vi ställa in färgen på triangeln via en uniform:
#version 330 coreout vec4 FragColor; uniform vec4 ourColor; // we set this variable in the OpenGL code.void main(){ FragColor = ourColor;}
vi förklarade en enhetlig vec4
ourColor i fragmentet shader och ställa in fragmentets utgångsfärg till innehållet i detta enhetliga värde. Eftersom uniformer är globala variabler kan vi definiera dem i vilket skuggningsstadium vi vill, så behöver du inte gå igenom vertex shader igen för att få något till fragmentet shader. Vi använder inte denna uniform i vertex shader så det finns ingen anledning att definiera den där.
om du deklarerar en uniform som inte används någonstans i din GLSL-kod kommer kompilatorn tyst att ta bort variabeln från den kompilerade versionen vilket är orsaken till flera frustrerande fel; kom ihåg detta!
uniformen är för närvarande Tom; vi har inte lagt till några data i uniformen ännu så låt oss försöka det. Vi måste först hitta indexet / platsen för det enhetliga attributet i vår shader. När vi har index/plats för uniformen kan vi uppdatera dess värden. Istället för att skicka en enda färg till fragmentskuggaren, låt oss krydda saker genom att gradvis ändra färg över tiden:
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);
först hämtar vi körtiden i sekunder via glfwGetTime(). Sedan varierar vi färgen i intervallet 0.0
1.0
genom att använda sin-funktionen och lagra resultatet i greenValue.
sedan frågar vi efter platsen för ourColor-uniformen med glGetUniformLocation. Vi levererar shader-programmet och namnet på uniformen (som vi vill hämta platsen från) till frågefunktionen. Om glGetUniformLocation returnerar -1
, kunde den inte hitta platsen. Slutligen kan vi ställa in det enhetliga värdet med glUniform4f-funktionen. Observera att att hitta den enhetliga platsen inte kräver att du använder shader-programmet först, men uppdatering av en uniform kräver att du först använder programmet (genom att ringa glUseProgram), eftersom det ställer in uniformen på det för närvarande aktiva shader-programmet. eftersom OpenGL i sin kärna är ett C-bibliotek har det inte inbyggt stöd för funktionsöverbelastning, så varhelst en funktion kan anropas med olika typer OpenGL definierar nya funktioner för varje typ som krävs; glUniform är ett perfekt exempel på detta. Funktionen kräver en specifik postfix för den typ av uniform du vill ställa in. Några av de möjliga postfixerna är:
-
f
: funktionen förväntar sig ettfloat
som dess värde. -
i
: funktionen förväntar sig ettint
som dess värde. -
ui
: funktionen förväntar sig ettunsigned int
som dess värde. -
3f
: funktionen förväntar sig 3float
s som dess värde. -
fv
: funktionen förväntar sig ettfloat
vektor/array som dess värde.
när du vill konfigurera ett alternativ för OpenGL väljer du bara den överbelastade funktionen som motsvarar din typ. I vårt fall vill vi ställa in 4 floats av uniformen individuellt så vi skickar våra data via glUniform4f (Observera att vi också kunde ha använtfv
version). nu när vi vet hur man ställer in värdena för enhetliga variabler kan vi använda dem för rendering. Om vi vill att färgen ska förändras gradvis vill vi uppdatera denna enhetliga varje ram, annars skulle triangeln behålla en enda solid färg om vi bara ställer in den en gång. Så vi beräknar greenValue och uppdaterar uniformen varje render iteration:
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();}
koden är en relativt enkel anpassning av föregående kod. Den här gången uppdaterar vi ett enhetligt värde varje ram innan vi ritar triangeln. Om du uppdaterar uniformen korrekt bör du se färgen på din triangel gradvis förändras från grönt till svart och tillbaka till grönt.
kolla in källkoden här om du har fastnat. som du kan se är uniformer ett användbart verktyg för att ställa in attribut som kan ändra varje ram eller för att utbyta data mellan din applikation och dina shaders, men vad händer om vi vill ställa in en färg för varje toppunkt? I så fall måste vi deklarera så många uniformer som vi har hörn. En bättre lösning skulle vara att inkludera mer data i vertexattributen, vilket är vad vi ska göra nu.
fler attribut!
vi såg i föregående kapitel hur vi kan fylla en VBO, konfigurera vertex attribut pekare och lagra allt i en VAO. Den här gången vill vi också lägga till färgdata till vertexdata. Vi kommer att lägga till färgdata som 3 float
s till vertices array. Vi tilldelar en röd, grön och blå färg till var och en av hörnen i vår triangel:
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 };
eftersom vi nu har mer data att skicka till vertex shader, är det nödvändigt att justera vertex shader för att också få vårt färgvärde som en vertexattributinmatning. Observera att vi ställer in platsen för acolor-attributet till 1 med layoutspecifikatorn:
#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}
eftersom vi inte längre använder en uniform för fragmentets färg, men nu använder ourcolor-utmatningsvariabeln måste vi också ändra fragmentskuggaren:
#version 330 coreout vec4 FragColor; in vec3 ourColor; void main(){ FragColor = vec4(ourColor, 1.0);}
eftersom vi har lagt till ett annat vertex-attribut och uppdaterat VBO: s minne Vi måste omkonfigurera vertexattributpekarna. De uppdaterade uppgifterna i VBO: s minne ser nu lite ut så här:
genom att känna till den aktuella layouten kan vi uppdatera vertexformatet med 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 första argumenten i glVertexAttribPointer är relativt enkla. Den här gången konfigurerar vi vertex-attributet på attributplats 1
. Färgvärdena har en storlek på 3
float
s och vi normaliserar inte värdena.
eftersom vi nu har två vertexattribut måste vi beräkna stegvärdet igen. För att få nästa attributvärde (t.ex. nästa x
komponent i positionsvektorn) i datarrayen måste vi flytta 6
float
s till höger, tre för positionsvärdena och tre för färgvärdena. Detta ger oss ett stegvärde på 6 gånger storleken på ett float
I byte (= 24
byte).
Den här gången måste vi också ange en förskjutning. För varje vertex är attributet position vertex först så vi förklarar en förskjutning av 0
. Färgattributet startar efter positionsdata så förskjutningen är 3 * sizeof(float)
I byte (= 12
byte).
kör programmet bör resultera i följande bild:
kolla in källkoden här om du har fastnat.
bilden kanske inte är exakt vad du kan förvänta dig, eftersom vi bara levererade 3 färger, inte den enorma färgpalett vi ser just nu. Allt detta är resultatet av något som kallas fragmentinterpolering i fragmentskuggaren. När du gör en triangel resulterar rasteriseringsstadiet vanligtvis i mycket fler fragment än hörn som ursprungligen specificerades. Rasteriseraren bestämmer sedan positionerna för vart och ett av dessa fragment baserat på var de bor på triangelformen.
baserat på dessa positioner interpolerar den alla fragmentskuggarens inmatningsvariabler. Säg till exempel att vi har en linje där den övre punkten har en grön färg och den nedre punkten en blå färg. Om fragmentskuggaren körs vid ett fragment som ligger runt en position vid70%
på linjen, skulle dess resulterande färginmatningsattribut då vara en linjär kombination av grönt och blått; för att vara mer exakt:30%
blå och70%
grön.
det här är exakt vad som hände vid Triangeln. Vi har 3 hörn och därmed 3 färger, och utifrån triangelns pixlar innehåller den förmodligen cirka 50000 fragment, där fragmentskuggaren interpolerade färgerna bland dessa pixlar. Om du tittar noga på färgerna ser du att allt är meningsfullt: rött till blått blir först lila och sedan till blått. Fragmentinterpolering tillämpas på alla fragmentskuggarens inmatningsattribut.
vår egen shader-klass
att skriva, sammanställa och hantera shaders kan vara ganska besvärligt. Som en sista touch på shader-ämnet kommer vi att göra vårt liv lite enklare genom att bygga en shader-klass som läser shaders från disk, sammanställer och länkar dem, kontrollerar fel och är lätt att använda. Detta ger dig också lite av en uppfattning om hur vi kan inkapsla en del av den kunskap vi lärt oss hittills i Användbara abstrakta objekt.
Vi kommer att skapa shader-klassen helt i en rubrikfil, främst för inlärningsändamål och portabilitet. Låt oss börja med att lägga till de nödvändiga inkluderingarna och genom att definiera klassstrukturen:
#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
vi använde flera preprocessordirektiv högst upp i rubrikfilen. Med hjälp av dessa små kodrader informerar din kompilator att bara inkludera och kompilera den här rubrikfilen om den inte har inkluderats ännu, även om flera filer innehåller shader-rubriken. Detta förhindrar länkning av konflikter.
shader-klassen har ID för shader-programmet. Dess konstruktör kräver filvägarna för källkoden för vertex respektive fragment shader som vi kan lagra på disken som enkla textfiler. För att lägga till lite extra lägger vi också till flera verktygsfunktioner för att underlätta våra liv lite: användning aktiverar shader-programmet och allt klart… funktioner Fråga en enhetlig plats och ange dess värde.
läsning från fil
Vi använder C++ filestreams för att läsa innehållet från filen till flera string
objekt:
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();
nästa måste vi kompilera och länka shadersna. Observera att vi också granskar om kompilering/länkning misslyckades och i så fall skriver du ut kompileringstidsfelen. Detta är extremt användbart vid felsökning (Du kommer att behöva dessa felloggar så småningom):
// 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);
use-funktionen är enkel:
void use() { glUseProgram(ID);}
På samma sätt för någon av de enhetliga setter funktioner:
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); }
och där har vi det, en färdig shader klass. Att använda shader-klassen är ganska lätt; vi skapar ett shader-objekt en gång och från den tiden börjar vi helt enkelt använda det:
Shader ourShader("path/to/shaders/shader.vs", "path/to/shaders/shader.fs");while(...){ ourShader.use(); ourShader.setFloat("someUniform", 1.0f); DrawStuff();}
här lagrade vi vertex och fragment shader-källkoden i två filer som heter shader.vs
och shader.fs
. Du är fri att namnge dina shader-filer hur du vill; Jag tycker personligen att tilläggen.vs
och.fs
är ganska intuitiva.
Du hittar källkoden här med vår nyskapade shader-klass. Observera att du kan klicka på shader-filvägarna för att hitta shaders källkod.
övningar
- justera vertex shader så att triangeln är upp och ner: lösning.
- ange en horisontell förskjutning via en uniform och flytta triangeln till höger om skärmen i vertex shader med hjälp av detta offsetvärde: lösning.
- mata ut vertexpositionen till fragmentskuggaren med hjälp av nyckelordet
out
och ställ in fragmentets färg lika med denna vertexposition (se hur även vertexpositionsvärdena interpoleras över triangeln). När du lyckats göra detta; försök att svara på följande fråga: Varför är den nedre vänstra sidan av vår triangel svart?: lösning.