după cum sa menționat în capitolul Hello Triangle, shaders sunt programe mici care se bazează pe GPU. Aceste programe sunt rulate pentru fiecare secțiune specifică a conductei grafice. Într-un sens de bază, shaderele nu sunt altceva decât programe care transformă intrările în ieșiri. Shaderele sunt, de asemenea, programe foarte izolate prin faptul că nu au voie să comunice între ele; singura comunicare pe care o au este prin intrările și ieșirile lor.
în capitolul precedent am atins pe scurt suprafața umbrelor și cum să le folosim în mod corespunzător. Vom explica acum shaderele și, în special, limbajul de umbrire OpenGL, într-un mod mai general.
GLSL
shaderele sunt scrise în limbajul C-like GLSL. GLSL este adaptat pentru utilizare cu grafică și conține caracteristici utile orientate în mod specific la vector și manipulare matrice.
shaderele încep întotdeauna cu o declarație de versiune, urmată de o listă de variabile de intrare și ieșire, uniforme și funcția sa principală. Punctul de intrare al fiecărui shader se află la funcția sa principală, unde procesăm orice variabile de intrare și scoatem rezultatele în variabilele sale de ieșire. Nu vă faceți griji dacă nu știți ce uniforme sunt, vom ajunge la cele în scurt timp.
un shader are de obicei următoarea structură:
#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;}
când vorbim în mod specific despre shader-ul vertex, fiecare variabilă de intrare este cunoscută și ca atribut vertex. Există un număr maxim de atribute vertex pe care ni se permite să le declarăm limitate de hardware. OpenGL garantează că există întotdeauna cel puțin 16 atribute vertex cu 4 componente disponibile, dar unele hardware pot permite mai multe pe care le puteți prelua interogând GL_MAX_VERTEX_ATTRIBS:
int nrAttributes;glGetIntegerv(GL_MAX_VERTEX_ATTRIBS, &nrAttributes);std::cout << "Maximum nr of vertex attributes supported: " << nrAttributes << std::endl;
aceasta returnează adesea minimul16
care ar trebui să fie mai mult decât suficient pentru majoritatea scopurilor.
Types
GLSL are, ca orice alt limbaj de programare, tipuri de date pentru specificarea tipului de variabilă cu care dorim să lucrăm. GLSL are majoritatea tipurilor de bază implicite pe care le cunoaștem din limbi precum C: int
float
double
uint
și bool
. GLSL are, de asemenea, două tipuri de containere pe care le vom folosi foarte mult, și anume vectors
și matrices
. Vom discuta despre matrice într-un capitol ulterior.
vectori
un vector în GLSL este un container de 1,2,3 sau 4 componente pentru oricare dintre tipurile de bază menționate mai sus. Ele pot lua următoarea formă (n
reprezintă numărul de componente):
-
vecn
: vectorul implicit aln
plutește. -
bvecn
: un vector den
booleeni. -
ivecn
: un vector den
numere întregi. -
uvecn
: un vector den
numere întregi nesemnate. -
dvecn
: un vector den
componente duble.
de cele mai multe ori vom folosivecn
de bază, deoarece flotoarele sunt suficiente pentru majoritatea scopurilor noastre.
componentele unui vector pot fi accesate prin vec.x
unde x
este prima componentă a vectorului. Puteți utiliza .x
.y
.z
și .w
pentru a accesa prima, a doua, a treia și a patra componentă. GLSL vă permite, de asemenea, să utilizați rgba
pentru culori sau stpq
pentru coordonatele texturii, accesând aceleași componente.
tipul de date vector permite o selecție interesantă și flexibilă a componentelor numită swizzling. Swizzling ne permite să folosim sintaxa ca aceasta:
vec2 someVec;vec4 differentVec = someVec.xyxx;vec3 anotherVec = differentVec.zyw;vec4 otherVec = someVec.xxxx + anotherVec.yxzy;
puteți utiliza orice combinație de până la 4 litere pentru a crea un vector nou (de același tip) atâta timp cât vectorul original are acele componente; nu este permisă accesarea componentei.z
a unuivec2
de exemplu. De asemenea, putem transmite vectori ca argumente la diferite apeluri de constructor vectorial, reducând numărul de argumente necesare:
vec2 vect = vec2(0.5, 0.7);vec4 result = vec4(vect, 0.0, 0.0);vec4 otherResult = vec4(result.xyz, 1.0);
vectorii sunt astfel un tip de date flexibil pe care îl putem folosi pentru toate tipurile de intrare și ieșire. De-a lungul cărții veți vedea o mulțime de exemple despre cum putem gestiona Creativ vectorii.
intrarile si iesirile
shaderele sunt programe mici si frumoase pe cont propriu, dar fac parte dintr-un intreg si din acest motiv vrem sa avem intrari si iesiri pe shaderele individuale, astfel incat sa putem muta lucrurile in jur. GLSL a definit cuvintele cheiein
șiout
special pentru acest scop. Fiecare shader poate specifica intrări și ieșiri folosind acele cuvinte cheie și ori de câte ori o variabilă de ieșire se potrivește cu o variabilă de intrare a următoarei etape shader sunt transmise de-a lungul. Vertex și fragment shader diferă un pic, deși.
shader vertex ar trebui să primească o formă de intrare altfel ar fi destul de ineficient. Shader-ul vertex diferă prin intrarea sa, prin faptul că primește intrarea sa direct din datele vertex. Pentru a defini modul în care sunt organizate datele vertex, specificăm variabilele de intrare cu metadatele locației, astfel încât să putem configura atributele vertex pe CPU. Am văzut acest lucru în capitolul anterior ca layout (location = 0)
. Shader-ul vertex necesită astfel o specificație suplimentară de aspect pentru intrările sale, astfel încât să o putem lega cu datele vertex.
de asemenea, este posibil să omiteți specificatorullayout (location = 0)
și interogarea locațiilor atributelor din Codul OpenGL prin glGetAttribLocation, dar aș prefera să le setez în shader-ul vertex. Este mai ușor de înțeles și vă salvează (și OpenGL) ceva de lucru.
cealaltă excepție este că shader fragment necesită o vec4
variabilă de ieșire de culoare, deoarece shader fragment trebuie să genereze o culoare de ieșire finală. Dacă nu reușiți să specificați o culoare de ieșire în shader-ul fragmentului, ieșirea tampon de culoare pentru acele fragmente va fi nedefinită (ceea ce înseamnă de obicei că OpenGL le va reda fie negru, fie alb).
deci, dacă vrem să trimitem date de la un shader la altul, ar trebui să declarăm o ieșire în shader-ul de trimitere și o intrare similară în shader-ul de primire. Când tipurile și numele sunt egale pe ambele părți, OpenGL va lega aceste variabile împreună și atunci este posibil să trimiteți date între shadere (acest lucru se face atunci când legați un obiect de program). Pentru a vă arăta cum funcționează acest lucru în practică, vom modifica shaderele din capitolul anterior pentru a lăsa shader-ul vertex să decidă culoarea pentru shader-ul fragmentului.
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;}
puteți vedea că am declarat o variabilă vertexColor cavec4
ieșire pe care am setat-o în vertex shader și declarăm o intrare vertexColor similară în shader fragment. Deoarece ambele au același tip și nume, vertexColor în shader fragment este legat de vertexColor în shader vertex. Deoarece setăm culoarea la o culoare Roșu închis în shader-ul vertex, fragmentele rezultate ar trebui să fie și roșu închis. Următoarea imagine arată ieșirea:
acolo mergem! Tocmai am reușit să trimitem o valoare de la Vertex shader la fragment shader. Să-l condimentăm puțin și să vedem dacă putem trimite o culoare din aplicația noastră către shader fragment!
uniformele
uniformele sunt un alt mod de a transmite datele din aplicația noastră de pe CPU către shaderele de pe GPU. Uniformele sunt totuși ușor diferite în comparație cu atributele vertex. În primul rând, uniformele sunt globale. Global, ceea ce înseamnă că o variabilă uniformă este unică pentru fiecare obiect al programului shader și poate fi accesată din orice shader în orice etapă a programului shader. În al doilea rând, indiferent de ce setați valoarea uniformă, uniformele își vor păstra valorile până când sunt fie resetate, fie actualizate.
pentru a declara o uniformă în GLSL, pur și simplu adăugămuniform
cuvânt cheie la un shader cu un tip și un nume. Din acel moment putem folosi uniforma nou declarată în shader. Să vedem dacă de data aceasta putem seta culoarea triunghiului printr-o uniformă:
#version 330 coreout vec4 FragColor; uniform vec4 ourColor; // we set this variable in the OpenGL code.void main(){ FragColor = ourColor;}
am declarat o uniformăvec4
ourColor în shader fragment și setați culoarea de ieșire a fragmentului la conținutul acestei valori uniforme. Deoarece uniformele sunt variabile globale, le putem defini în orice etapă de shader pe care am dori-o, deci nu este nevoie să parcurgem din nou shader-ul vertex pentru a obține ceva la shader-ul fragmentului. Nu folosim această uniformă în shader-ul vertex, deci nu este nevoie să o definim acolo.
dacă declarați o uniformă care nu este utilizată nicăieri în codul GLSL, compilatorul va elimina în tăcere variabila din versiunea compilată, care este cauza mai multor erori frustrante; rețineți acest lucru!
uniforma este momentan goală; încă nu am adăugat date uniformei, așa că hai să încercăm asta. Mai întâi trebuie să găsim indexul/locația atributului uniform în shader-ul nostru. Odată ce avem indexul / locația uniformei, îi putem actualiza valorile. În loc să trecem o singură culoare la shader fragment, să condimentăm lucrurile schimbând treptat culoarea în timp:
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);
În primul rând, vom prelua timpul de funcționare în câteva secunde prin glfwGetTime(). Apoi variem culoarea în intervalul 0.0
1.0
folosind funcția sin și stocăm rezultatul în greenValue.
apoi interogăm locația uniformei ourColor folosind glGetUniformLocation. Furnizăm programul shader și numele uniformei (din care dorim să preluăm locația) la funcția de interogare. Dacă glGetUniformLocation returnează-1
, nu a putut găsi locația. În cele din urmă putem seta valoarea uniformă folosind funcția glUniform4f. Rețineți că găsirea locației uniforme nu necesită să utilizați mai întâi programul shader, dar actualizarea unei uniforme necesită să utilizați mai întâi programul (apelând glUseProgram), deoarece setează uniforma pe programul shader activ în prezent.
deoarece OpenGL este în nucleul său o bibliotecă C nu are suport nativ pentru supraîncărcarea funcțiilor, deci oriunde poate fi apelată o funcție cu diferite tipuri OpenGL definește noi funcții pentru fiecare tip necesar; glUniform este un exemplu perfect în acest sens. Funcția necesită o postfixare specifică pentru tipul uniformei pe care doriți să o setați. Câteva dintre postfixurile posibile sunt:
-
f
: funcția așteaptă unfloat
ca valoare. -
i
: funcția așteaptă unint
ca valoare. -
ui
: funcția așteaptă ununsigned int
ca valoare. -
3f
: funcția așteaptă 3float
s ca valoare. -
fv
: funcția așteaptă unfloat
vector/array ca valoare.
ori de câte ori doriți să configurați o opțiune de OpenGL alegeți pur și simplu funcția supraîncărcată care corespunde tipului dvs. În cazul nostru, dorim să setăm 4 flotoare ale uniformei individual, astfel încât să transmitem datele noastre prin glUniform4f (rețineți că am fi putut folosi șifv
versiune).
acum că știm cum să setăm valorile variabilelor uniforme, le putem folosi pentru redare. Dacă dorim ca culoarea să se schimbe treptat, dorim să actualizăm această uniformă în fiecare cadru, altfel triunghiul ar menține o singură culoare solidă dacă l-am seta o singură dată. Deci, vom calcula greenValue și să actualizeze uniforma fiecare face iterație:
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();}
codul este o adaptare relativ simplă a codului anterior. De data aceasta, actualizăm o valoare uniformă a fiecărui cadru înainte de a desena triunghiul. Dacă actualizați uniforma corect, ar trebui să vedeți că culoarea triunghiului dvs. se schimbă treptat de la verde la negru și înapoi la verde.
Verificați codul sursă aici dacă sunteți blocat.
după cum puteți vedea, uniformele sunt un instrument util pentru setarea atributelor care pot schimba fiecare cadru sau pentru schimbul de date între aplicația dvs. și shaderele dvs., dar dacă vrem să setăm o culoare pentru fiecare nod? În acest caz, ar trebui să declarăm cât mai multe uniforme pe cât avem noduri. O soluție mai bună ar fi includerea mai multor date în atributele vertex, ceea ce vom face acum.
mai multe atribute!
am văzut în capitolul precedent Cum putem umple un VBO, configura indicii atributului vertex și stoca totul într-un VAO. De data aceasta, dorim, de asemenea, să adăugăm date de culoare la datele vertex. Vom adăuga date de culoare ca 3 float
s la matricea de noduri. Atribuim o culoare roșie, verde și albastră fiecăruia dintre colțurile triunghiului nostru respectiv:
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 };
deoarece acum avem mai multe date de trimis la shader-ul vertex, este necesar să ajustăm shader-ul vertex pentru a primi și valoarea culorii noastre ca intrare atribut vertex. Rețineți că setăm locația atributului aColor la 1 cu specificatorul de aspect:
#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}
deoarece nu mai folosim o uniformă pentru culoarea fragmentului, dar acum folosim variabila de ieșire ourColor va trebui să schimbăm și shader-ul fragmentului:
#version 330 coreout vec4 FragColor; in vec3 ourColor; void main(){ FragColor = vec4(ourColor, 1.0);}
pentru că am adăugat un alt atribut vertex și am actualizat memoria trebuie să reconfigurăm indicatoarele atributului Vertex. Datele actualizate din memoria VBO arată acum cam așa:
cunoscând aspectul curent putem actualiza formatul vertex cu 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);
primele argumente ale glVertexAttribPointer sunt relativ simple. De data aceasta configurăm atributul vertex pe locația atributului 1
. Valorile de culoare au o dimensiune de 3
float
s și nu normalizăm valorile.
deoarece avem acum două atribute de noduri, trebuie să recalculăm valoarea pasului. Pentru a obține următoarea valoare a atributului (de exemplu, următoarea x
componentă a vectorului de poziție) în matricea de date trebuie să mutăm 6
float
s la dreapta, trei pentru valorile poziției și trei pentru valorile culorii. Acest lucru ne oferă o valoare pas de 6 ori dimensiunea unuifloat
în octeți (=24
octeți).
De asemenea, de data aceasta trebuie să specificăm un offset. Pentru fiecare nod, atributul Vertex de poziție este primul, așa că declarăm un offset de0
. Atributul de culoare Începe după datele de poziție, astfel încât offset-ul este 3 * sizeof(float)
în octeți (= 12
octeți).
rularea aplicației ar trebui să aibă ca rezultat următoarea imagine:
Verificați codul sursă aici dacă sunteți blocat.
este posibil ca imaginea să nu fie exact ceea ce v-ați aștepta, deoarece am furnizat doar 3 culori, nu paleta imensă de culori pe care o vedem acum. Acesta este tot rezultatul a ceva numit interpolare fragment în shader fragment. La redarea unui triunghi, etapa de rasterizare are ca rezultat de obicei mult mai multe fragmente decât vârfurile specificate inițial. Rasterizatorul determină apoi pozițiile fiecăruia dintre acele fragmente în funcție de locul în care se află pe forma triunghiului.
pe baza acestor poziții, interpolează toate variabilele de intrare ale fragmentului shader. Spuneți, de exemplu, că avem o linie în care punctul superior are o culoare verde, iar punctul inferior o culoare albastră. Dacă shader fragment este rulat la un fragment care se află în jurul unei poziții la 70%
a liniei, atributul său de intrare de culoare rezultat ar fi atunci o combinație liniară de verde și albastru; pentru a fi mai precis: 30%
albastru și 70%
verde.
exact asta s-a întâmplat la triunghi. Avem 3 vârfuri și astfel 3 culori și, judecând după pixelii triunghiului, probabil conține aproximativ 50000 de fragmente, unde shader-ul fragmentului a interpolat culorile dintre acei pixeli. Dacă vă uitați bine la culori, veți vedea că totul are sens: roșu până la albastru ajunge mai întâi la violet și apoi la albastru. Interpolarea fragmentelor se aplică tuturor atributelor de intrare ale fragmentului shader.
propria noastră clasă de shader
scrierea, compilarea și gestionarea shaderelor pot fi destul de greoaie. Ca o atingere finală asupra subiectului shader, ne vom face viața puțin mai ușoară construind o clasă shader care citește shaderele de pe disc, le compilează și le leagă, verifică erorile și este ușor de utilizat. Acest lucru vă oferă, de asemenea, o idee despre cum putem încapsula unele dintre cunoștințele pe care le-am învățat până acum în obiecte abstracte utile.
vom crea clasa shader în întregime într-un fișier antet, în principal în scopuri de învățare și portabilitate. Să începem prin adăugarea includerilor necesare și prin definirea structurii clasei:
#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
am folosit mai multe directive preprocesor în partea de sus a fișierului antet. Utilizarea acestor mici linii de cod informează compilatorul dvs. să includă și să compileze acest fișier antet numai dacă nu a fost încă inclus, chiar dacă mai multe fișiere includ antetul shader. Acest lucru previne legarea conflictelor.
clasa shader deține ID-ul programului shader. Constructorul său necesită căile de fișiere ale codului sursă al vertexului și respectiv fragmentului shader pe care le putem stoca pe disc ca fișiere text simple. Pentru a adăuga un pic în plus, adăugăm și mai multe funcții de utilitate pentru a ne ușura puțin viața: utilizarea activează programul shader și totul este setat… funcțiile interoghează o locație uniformă și își stabilesc valoarea.
citirea din fișier
folosim fluxuri de fișiere C++ pentru a citi conținutul din fișier în mai multestring
obiecte:
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 continuare trebuie să compilăm și să legăm shaderele. Rețineți că analizăm, de asemenea, dacă compilarea/legarea a eșuat și, dacă da, tipăriți erorile de compilare. Acest lucru este extrem de util atunci când depanare (aveți de gând să nevoie de aceste jurnale de eroare în cele din urmă):
// 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);
funcția de utilizare este simplă:
void use() { glUseProgram(ID);}
în mod similar pentru oricare dintre funcțiile uniform setter:
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); }
și acolo avem, o clasă de shader completat. Folosind clasa shader este destul de ușor; vom crea un obiect shader o dată și din acel moment pur și simplu începe să utilizați-l:
Shader ourShader("path/to/shaders/shader.vs", "path/to/shaders/shader.fs");while(...){ ourShader.use(); ourShader.setFloat("someUniform", 1.0f); DrawStuff();}
aici am stocat nod și fragment shader codul sursă în două fișiere numite shader.vs
și shader.fs
. Sunteți liber să denumiți fișierele shader oricum doriți; Eu personal găsesc extensiile.vs
și.fs
destul de intuitive.
puteți găsi codul sursă aici folosind noua noastră clasă de shader. Rețineți că puteți face clic pe căile fișierului shader pentru a găsi codul sursă al shaderelor.
exerciții
- reglați shader vertex, astfel încât triunghiul este cu susul în jos: soluție.
- specificați un offset orizontal printr-o uniformă și mutați triunghiul în partea dreaptă a ecranului în shader vertex folosind această valoare offset: soluție.
- afișează poziția vârfului la shader-ul fragmentului folosind
out
cuvânt cheie și setați culoarea fragmentului egală cu această poziție a vârfului (vedeți cum chiar și valorile poziției vârfului sunt interpolate peste triunghi). Odată ce ați reușit să faceți acest lucru; încercați să răspundeți la următoarea întrebare: de ce partea din stânga jos a triunghiului nostru este neagră?: soluție.