som nevnt I Hello Triangle-kapittelet, er shaders små programmer som hviler på GPU. Disse programmene kjøres for hver bestemt del av grafikkrørledningen. I en grunnleggende forstand er shaders ikke noe mer enn programmer som forvandler innganger til utganger. Shaders er også svært isolerte programmer fordi de ikke har lov til å kommunisere med hverandre; den eneste kommunikasjonen de har er via deres innganger og utganger.
i forrige kapittel berørte vi kort overflaten av shaders og hvordan du skal bruke dem riktig. Vi vil nå forklare shaders, og spesielt OpenGL-Skyggespråket, på en mer generell måte.
GLSL
Shaders er skrevet I C-lignende språk GLSL. GLSL er skreddersydd for bruk med grafikk og inneholder nyttige funksjoner spesielt rettet mot vektor og matrise manipulasjon.
Shaders begynner alltid med en versjonsdeklarasjon, etterfulgt av en liste over inngangs-og utgangsvariabler, uniformer og hovedfunksjon. Hver shaders inngangspunkt er på hovedfunksjonen der vi behandler noen inngangsvariabler og sender resultatene i utgangsvariablene. Ikke bekymre deg hvis du ikke vet hva uniformer er, vi kommer til dem snart.
en shader har vanligvis følgende 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 snakker spesifikt om vertex shader, er hver inngangsvariabel også kjent som et vertex-attributt. Det er et maksimalt antall vertex attributter vi har lov til å erklære begrenset av maskinvaren. OpenGL garanterer at det alltid er minst 16 4-komponent vertex-attributter tilgjengelig, men noe maskinvare kan tillate mer som du kan hente ved å spørre GL_MAX_VERTEX_ATTRIBS:
int nrAttributes;glGetIntegerv(GL_MAX_VERTEX_ATTRIBS, &nrAttributes);std::cout << "Maximum nr of vertex attributes supported: " << nrAttributes << std::endl;
dette returnerer ofte minimum 16
som bør være mer enn nok for de fleste formål.
Typer
GLSL har, som alle andre programmeringsspråk, datatyper for å spesifisere hvilken type variabel vi vil jobbe med. GLSL har de fleste standard grunnleggende typene vi kjenner Fra språk Som C: int
float
double
uint
og bool
. GLSL har også to containertyper som vi skal bruke mye, nemlig vectors
og matrices
. Vi vil diskutere matriser i et senere kapittel.
Vektorer
en vektor I GLSL er en 1,2,3 eller 4 komponentbeholder for noen av de grunnleggende typene som nettopp er nevnt. De kan ta følgende form (n
representerer antall komponenter):
-
vecn
: standardvektoren tiln
flyter. -
bvecn
: en vektor avn
booleans. -
ivecn
: en vektor avn
heltall. -
uvecn
: en vektor avn
usignerte heltall. -
dvecn
: en vektor avn
doble komponenter.
Mesteparten av tiden vil vi bruke den grunnleggende vecn
siden flyter er tilstrekkelig for de fleste av våre formål.
Komponenter av en vektor kan nås via vec.x
hvor x
er den første komponenten av vektoren. Du kan bruke .x
.y
.z
og .w
for å få tilgang til henholdsvis første, andre, tredje og fjerde komponent. GLSL lar deg også bruke rgba
for farger eller stpq
for teksturkoordinater, tilgang til de samme komponentene.
vektor datatypen tillater noe interessant og fleksibelt komponentvalg kalt swizzling. Swizzling tillater oss å bruke syntaks som dette:
vec2 someVec;vec4 differentVec = someVec.xyxx;vec3 anotherVec = differentVec.zyw;vec4 otherVec = someVec.xxxx + anotherVec.yxzy;
du kan bruke en hvilken som helst kombinasjon av opptil 4 bokstaver for å opprette en ny vektor (av samme type) så lenge den opprinnelige vektoren har disse komponentene; det er ikke tillatt å få tilgang til.z
komponent av envec2
for eksempel. Vi kan også sende vektorer som argumenter til forskjellige vektorkonstruksjonskall, og redusere antall argumenter som kreves:
vec2 vect = vec2(0.5, 0.7);vec4 result = vec4(vect, 0.0, 0.0);vec4 otherResult = vec4(result.xyz, 1.0);
Vektorer er dermed en fleksibel datatype som vi kan bruke for alle typer inngang og utgang. Gjennom hele boken ser du mange eksempler på hvordan vi kreativt kan håndtere vektorer.
Ins og outs
Shaders er fine små programmer på egenhånd, men de er en del av en helhet, og derfor vil vi ha innganger og utganger på de enkelte shaders slik at vi kan flytte ting rundt. GLSL definerte in
og out
nøkkelord spesielt for dette formålet. Hver shader kan angi innganger og utganger ved hjelp av disse søkeordene, og hvor en utgangsvariabel samsvarer med en inngangsvariabel i neste shader-stadium, sendes de sammen. Vertex og fragment shader varierer litt skjønt.vertex shader bør få noen form for inngang ellers ville det være ganske ineffektivt. Vertex shader er forskjellig i inngangen, ved at den mottar inngangen rett fra vertex-dataene. For å definere hvordan vertex data er organisert vi angi input variabler med plassering metadata slik at vi kan konfigurere vertex attributter PÅ CPU. Vi har sett dette i forrige kapittel som layout (location = 0)
. Vertex shader krever dermed en ekstra layoutspesifikasjon for sine innganger, slik at vi kan koble den med vertex-dataene.
det er også mulig å utelatelayout (location = 0)
spesifiserer og spørring for attributtstedene i OpenGL-koden din via glGetAttribLocation, men jeg foretrekker å sette dem i vertex shader. Det er lettere å forstå og sparer deg (Og OpenGL) litt arbeid.
det andre unntaket er at fragment shader krever en vec4
fargeutgangsvariabel, siden fragment shaders må generere en endelig utgangsfarge. Hvis Du ikke angir en utgangsfarge i fragment shader, vil fargebufferutgangen for disse fragmentene være udefinert (som vanligvis betyr At OpenGL vil gjengi dem enten svart eller hvitt).
Så Hvis vi vil sende data fra en shader til den andre, må vi deklarere en utgang i sendeskyggeren og en lignende inngang i mottakskyggeren. Når typene Og navnene er like På begge sider, Vil OpenGL koble disse variablene sammen, og det er mulig å sende data mellom shaders (dette gjøres når du kobler et programobjekt). For å vise deg hvordan dette fungerer i praksis, skal vi endre shaders fra forrige kapittel for å la vertex shader bestemme fargen for fragment 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 vi erklært en vertexColor variabel som envec4
utgang som vi satt i vertex shader og vi erklærer en lignende vertexColor inngang i fragment shader. Siden de begge har samme type og navn, vertexColor i fragment shader er knyttet til vertexColor i vertex shader. Fordi vi setter fargen til en mørk rød farge i vertex shader, bør de resulterende fragmentene også være mørkrøde. Følgende bilde viser utgangen:
Der går vi! Vi klarte bare å sende en verdi fra vertex shader til fragment shader. La oss krydre det litt og se om vi kan sende en farge fra vår søknad til fragmentet shader!
Uniformer
Uniformer Er en annen måte å overføre data fra vår søknad PÅ CPU til shaders på GPU. Uniformer er imidlertid litt forskjellige i forhold til vertex-attributter. Uniformer er globale. Global, noe som betyr at en uniform variabel er unik per shader program objekt, og kan nås fra alle shader på ethvert stadium i shader programmet. For det andre, uansett hva du angir uniformverdien til, vil uniformer beholde verdiene til de er tilbakestilt eller oppdatert.
for Å deklarere en uniform i GLSL legger vi bare til uniform
nøkkelordet til en shader med en type og et navn. Fra det tidspunktet kan vi bruke den nylig deklarerte uniformen i shader. La oss se om denne gangen kan vi sette fargen på trekanten via en uniform:
#version 330 coreout vec4 FragColor; uniform vec4 ourColor; // we set this variable in the OpenGL code.void main(){ FragColor = ourColor;}
vi erklærte en uniformvec4
ourColor i fragment shader og sette fragment utgang farge til innholdet i denne uniform verdi. Siden uniformer er globale variabler, kan vi definere dem i et hvilket som helst shader-stadium vi ønsker, så du trenger ikke å gå gjennom vertex shader igjen for å få noe til fragmentet shader. Vi bruker ikke denne uniformen i vertex shader, så det er ikke nødvendig å definere det der.
hvis du erklærer en uniform som ikke brukes hvor som helst I GLSL-koden, vil kompilatoren stille fjerne variabelen fra den kompilerte versjonen som er årsaken til flere frustrerende feil; ha dette i bakhodet!
uniformen er for øyeblikket tom; vi har ikke lagt til noen data i uniformen ennå, så la oss prøve det. Vi må først finne indeksen/plasseringen av uniformattributtet i vår shader. Når vi har indeksen/plasseringen av uniformet, kan vi oppdatere verdiene. I stedet for å sende en enkelt farge til fragment shader, la oss krydre ting opp ved gradvis å endre farge over tid:
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 henter vi kjøretiden i sekunder via glfwGetTime (). Da varierer vi fargen i området 0.0
1.0
ved å bruke sin-funksjonen og lagre resultatet i greenValue.
da spør vi om plasseringen av ourColor-uniformen ved hjelp av glGetUniformLocation. Vi leverer shader-programmet og navnet på uniformen (som vi vil hente plasseringen fra) til spørringsfunksjonen. Hvis glGetUniformLocation returnerer -1
, finner den ikke plasseringen. Til slutt kan vi sette den ensartede verdien ved hjelp av glUniform4f-funksjonen. Merk at å finne den ensartede plasseringen ikke krever at du bruker shader-programmet først, men å oppdatere en uniform krever at du først bruker programmet (ved å ringe glUseProgram), fordi det setter uniformen på det aktive shader-programmet. Fordi OpenGL i kjernen er Et C-bibliotek, har Det ikke opprinnelig støtte for funksjonsoverbelastning, så hvor en funksjon kan kalles med forskjellige typer OpenGL definerer nye funksjoner for hver type som kreves; glUniform er et perfekt eksempel på dette. Funksjonen krever en bestemt postfix for typen uniform du vil angi. Noen av de mulige postfikseringene er:
-
f
: funksjonen forventer enfloat
som sin verdi. -
i
: funksjonen forventer enint
som sin verdi. -
ui
: funksjonen forventer enunsigned int
som sin verdi. -
3f
: funksjonen forventer 3float
s som sin verdi. -
fv
: funksjonen forventer enfloat
vektor / array som sin verdi.
Når du ønsker å konfigurere Et Alternativ Til OpenGL bare plukke overbelastet funksjon som tilsvarer din type. I vårt tilfelle ønsker vi å sette 4 flyter av uniform individuelt slik at vi passerer våre data via glUniform4f(merk at vi også kunne har bruktfv
versjon). Nå som vi vet hvordan vi skal sette verdiene for ensartede variabler, kan vi bruke dem til gjengivelse. Hvis vi vil at fargen gradvis skal endres, vil vi oppdatere denne uniformen hver ramme, ellers vil trekanten opprettholde en enkelt solid farge hvis vi bare setter den en gang. Så vi beregner greenValue og oppdaterer uniformen hver gjengivelse iterasjon:
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 er en relativt enkel tilpasning av forrige kode. Denne gangen oppdaterer vi en jevn verdi hver ramme før du tegner trekanten. Hvis du oppdaterer uniformen riktig, bør du se at fargen på trekanten din gradvis endres fra grønt til svart og tilbake til grønt.
Sjekk ut kildekoden her hvis du sitter fast. som du kan se, uniformer er et nyttig verktøy for å sette attributter som kan endre hver ramme, eller for utveksling av data mellom programmet og shaders, men hva om vi ønsker å sette en farge for hvert toppunkt? I så fall må vi erklære så mange uniformer som vi har hjørner. En bedre løsning ville være å inkludere flere data i toppunktet attributter som er hva vi skal gjøre nå.
Flere attributter!
Vi så i forrige kapittel hvordan vi kan fylle EN VBO, konfigurere vertex-attributtpekere og lagre alt i EN VAO. Denne gangen vil vi også legge til fargedata i vertex-dataene. Vi skal legge til fargedata som 3 float
s til toppunktene. Vi tilordner en rød, grønn og blå farge til hver av hjørnene i vår trekant henholdsvis:
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 };
siden vi nå har flere data å sende til vertex shader, er det nødvendig å justere vertex shader for å også motta vår fargeverdi som en vertex-attributtinngang. Merk at vi setter plasseringen av acolor-attributtet til 1 med layoutspesifikatoren:
#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}
Siden vi ikke lenger bruker en uniform for fragmentets farge, men nå bruker ourcolor-utdatavariabelen, må vi også endre fragmentet shader:
#version 330 coreout vec4 FragColor; in vec3 ourColor; void main(){ FragColor = vec4(ourColor, 1.0);}
Fordi vi har lagt til et annet vertex-attributt Og oppdatert VBO-minnet vi må omkonfigurere vertex-attributtpekerne. De oppdaterte dataene i BBOS minne ser nå litt ut som dette:
. Fargeverdiene har en størrelse på3
float
s og vi normaliserer ikke verdiene.
Siden vi nå har to toppunktattributter, må vi beregne skrittverdien på nytt. For å få den neste attributtverdien (f.eks. den neste x
komponenten av posisjonsvektoren) i datarekken må vi flytte 6
float
s til høyre, tre for posisjonsverdiene og tre for fargeverdiene. Dette gir oss en skrittverdi på 6 ganger størrelsen på en float
i byte (=24
byte).
også denne gangen må vi angi en offset. For hvert toppunkt er posisjonens toppunktattributt først, så vi erklærer en forskyvning av 0
. Fargeattributtet starter etter posisjonsdataene, slik at forskyvningen er 3 * sizeof(float)
i byte (=12
byte).
Kjører programmet bør resultere i følgende bilde:
Sjekk ut kildekoden her hvis du sitter fast.
bildet er kanskje ikke akkurat det du forventer, siden vi bare leverte 3 farger, ikke den store fargepaletten vi ser akkurat nå. Dette er alt resultatet av noe som kalles fragmentinterpolering i fragment shader. Når du gjengir en trekant, resulterer rastreringstrinnet vanligvis i mye flere fragmenter enn hjørner som opprinnelig ble spesifisert. Rasterizeren bestemmer deretter posisjonene til hver av disse fragmentene basert på hvor de bor på trekantformen.
Basert på disse posisjonene, interpolerer det alle fragment shaders input variabler. Si for eksempel at vi har en linje der det øvre punktet har en grønn farge og det nedre punktet en blå farge. Hvis fragmentet shader kjøres på et fragment som ligger rundt en posisjon på70%
av linjen, vil den resulterende fargeinngangsattributtet da være en lineær kombinasjon av grønt og blått; for å være mer presis:30%
blå og70%
grønn.
dette er akkurat det som skjedde i trekanten. Vi har 3 hjørner og dermed 3 farger, og dømmer fra trekantens piksler inneholder den sannsynligvis rundt 50000 fragmenter, hvor fragmentet shader interpolerte fargene mellom disse pikslene. Hvis du ser godt på fargene, ser du at alt er fornuftig: rød til blå blir først lilla og deretter til blå. Fragment interpolering brukes på alle fragment shader inndataattributter.
Vår egen shader klasse
Skrive, kompilere og administrere shaders kan være ganske tungvint. Som en siste touch på shader emnet vi kommer til å gjøre livet litt enklere ved å bygge en shader klasse som leser shaders fra disk, kompilerer og kobler dem, sjekker for feil og er enkel å bruke. Dette gir deg også litt av en ide om hvordan vi kan innkapsle noe av kunnskapen vi lærte så langt i nyttige abstrakte objekter.
Vi vil lage shader-klassen helt i en header-fil, hovedsakelig for læringsformål og portabilitet. La oss begynne med å legge til de nødvendige inkluderer og ved å definere klassestrukturen:
#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 brukte flere preprosessordirektiver øverst i topptekstfilen. Ved hjelp av disse små linjer med kode informerer kompilatoren å bare inkludere og kompilere denne header filen hvis det ikke har blitt inkludert ennå, selv om flere filer inkluderer shader header. Dette forhindrer kobling av konflikter.
shader-klassen inneholder IDEN til shader-programmet. Dens konstruktør krever filbanene til kildekoden til vertex og fragment shader henholdsvis at vi kan lagre på disk som enkle tekstfiler. For å legge til litt ekstra, legger vi også til flere verktøyfunksjoner for å lette våre liv litt: bruk aktiverer shader-programmet, og alt sett… funksjoner spør etter en enhetlig plassering og angi verdien.
Lesing fra fil
Vi bruker C++ filestreams for å lese innholdet fra filen til flerestring
objekter:
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();
neste må vi kompilere og koble shaders. Merk at vi også vurderer om kompilering / kobling mislyktes, og i så fall skriv ut kompileringstidsfeilene. Dette er ekstremt nyttig når feilsøking (du kommer til å trenge disse feilloggene til slutt):
// 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);
bruksfunksjonen er enkel:
void use() { glUseProgram(ID);}
På samme måte for noen av de ensartede setterfunksjonene:
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); }
Og der har vi det, en fullført shader-klasse. Å bruke shader-klassen er ganske enkelt; vi lager et shader-objekt en gang og fra det punktet begynner du bare å bruke det:
Shader ourShader("path/to/shaders/shader.vs", "path/to/shaders/shader.fs");while(...){ ourShader.use(); ourShader.setFloat("someUniform", 1.0f); DrawStuff();}
Her lagret vi vertex og fragment shader kildekoden i to filer kaltshader.vs
ogshader.fs
. Du er fri til å navngi shader-filene dine, men du vil; Jeg finner personlig utvidelsene.vs
og.fs
ganske intuitivt.
du kan finne kildekoden her ved hjelp av vår nyopprettede shader klasse. Merk at du kan klikke på shader-filbanene for å finne shaders kildekode.
Øvelser
- Juster vertex shader slik at trekanten er opp ned: løsning.
- Angi en horisontal forskyvning via en uniform og flytt trekanten til hoyre side av skjermen i vertex shader ved hjelp av denne offsetverdien: losning.
- Utfør toppunktposisjonen til fragment shader ved hjelp av
out
nøkkelordet og sett fragmentets farge lik denne toppunktposisjonen (se hvordan selv toppunktposisjonsverdiene er interpolert over trekanten). Når du klarte å gjøre dette; prøv å svare på følgende spørsmål: hvorfor er nederste venstre side av vår trekant svart?: løsning.