Învățând să scriu shadere grafică, învățați să folosiți puterea GPU, cu mii de nuclee care rulează în paralel. Este un fel de programare care necesită o mentalitate diferită, dar deblocarea potențialului său merită probleme inițiale.
Practic, fiecare simulare grafică modernă pe care o vedeți este alimentată într-un fel prin codul scris pentru GPU, de la efectele realiste de iluminare în jocurile AAA de ultimă generație până la efecte post-procesare 2D și simulări de fluid.
O scenă în Minecraft, înainte și după aplicarea a câteva shadere.Programarea Shader uneori iese ca o magie neagră enigmatică și este adesea înțeleasă greșit. Există o mulțime de eșantioane de cod care vă arată cum să creați efecte incredibile, dar să oferiți o explicație puțin sau deloc. Acest ghid vizează depășirea acestui decalaj. Mă voi concentra mai mult pe elementele de bază ale scriiturii și înțelegerii codului shader, astfel încât să puteți ușor să tweak, combinați sau să scrieți propriile dvs. de la zero!
Acesta este un ghid general, așa că ceea ce învățați aici se va aplica la orice se poate executa shadere.
Un shader este pur și simplu un program care rulează în conducta grafică și spune computerului cum să facă fiecare pixel. Aceste programe sunt numite shadere, deoarece ele sunt adesea folosite pentru a controla efectele de iluminare și de umbrire, dar nu există niciun motiv pentru care nu pot face față altor efecte speciale.
Shaderele sunt scrise într-un limbaj special de umbrire. Nu vă faceți griji, nu trebuie să ieșiți și să învățați o limbă complet nouă; vom folosi GLSL (OpenGL Shading Language) care este un limbaj asemănător C. (Există o grămadă de limbi de umbrire acolo pentru diferite platforme, dar din moment ce toate sunt adaptate pentru a rula pe GPU, toate sunt foarte asemănătoare.)
Notă: Acest articol se referă exclusiv la shaderele de fragmente. Dacă sunteți curios despre ce altceva există acolo, puteți citi despre diferite etape ale conductei grafice pe OpenGL Wiki.
Vom folosi ShaderToy pentru acest tutorial. Acest lucru vă permite să începeți programarea shaders chiar în browser-ul dvs., fără a hassle de a stabili nimic! (Utilizează WebGL pentru redare, deci veți avea nevoie de un browser care să îl poată sprijini.) Crearea unui cont este opțională, dar este utilă pentru salvarea codului.
Notă: ShaderToy este în versiune beta în momentul în care scrie acest articol. Unele detalii UI / sintaxă mici pot fi ușor diferite.
Când faceți clic pe New Shader, ar trebui să vedeți ceva de genul:
Interfața dvs. ar putea părea puțin diferită dacă nu sunteți conectat (ă).Săgeata neagră mică din partea de jos este ceea ce faceți clic pentru a vă compila codul.
Sunt pe punctul de a explica cum lucrează shaders într-o propoziție. Sunteți gata? Aici merge!
Unicul scop al unui shader este de a returna patru numere: r
, g
, b
,și A
.
Asta e tot ce face vreodată sau poate face. Funcția pe care o vedeți în fața dvs. rulează pentru fiecare pixel pe ecran. Aceasta returnează cele patru valori de culoare și devine culoarea pixelului. Aceasta este ceea ce se numește a Pixel Shader(uneori denumită a Fragment Shader).
În acest sens, să încercăm să ne întoarcem un ecran roșu. Rgba (roșu, verde, albastru și "alfa", care definește transparența) valorile merg de la 0
la 1
, astfel încât tot ce trebuie să facem este să ne întoarcem r, g, b, a = 1,0,0,1
. ShaderToy se așteaptă să fie stocată culoarea finală a pixelilor fragColor
.
void mainImage (în vec4 fragColor, în vec2 fragCoord) fragColor = vec4 (1.0,0.0,0.0,1.0);
Felicitări! Acesta este primul dvs. shader de lucru!
Provocare: Puteți să-l modificați la o culoare gri solidă?
vec4
este doar un tip de date, astfel încât am fi putut declara culoarea noastră ca o variabilă, așa cum este:
void mainImage (vec4 fragColor, în vec2 fragCoord) vec4 solidRed = vec4 (1.0,0.0,0.0,1.0); fragColor = solidRed;
Acest lucru nu este foarte interesant, totuși. Avem puterea de a rula codul sute de mii de pixeli în paralel și le setăm pe toate la aceeași culoare.
Să încercăm să facem un gradient pe ecran. Ei bine, nu putem face prea mult fără să știm câteva lucruri despre pixelul pe care îl afectează, cum ar fi locația pe ecran ...
Shader-ul pixelilor trece câteva variabile pe care le puteți utiliza. Cel mai util pentru noi este fragCoord
, care păstrează coordonatele pixelilor x și y (și z, dacă lucrați în 3D). Să încercăm să întoarcem toți pixelii din jumătatea stângă a ecranului negru și toți cei de pe jumătate roșu dreapta:
void mainImage (din vec4 fragColor, în vec2 fragCoord) vec2 xy = fragCoord.xy; // Obținerea coordonatelor pentru pixelul curent vec4 solidRed = vec4 (0,0.0,0.0,1.0); // Aceasta este de fapt negru chiar acum dacă (xy.x> 300.0) // Numărul arbitrar, nu facem știți cât de mare este ecranul nostru! solidRed.r = 1.0; // Setați componenta roșie la 1.0 fragColor = solidRed;
Notă: Pentru orice vec4
, puteți accesa componentele sale prin obj.x
, obj.y
, obj.z
și obj.w
sauprin intermediulobj.r
, obj.g
, obj.b
, obj.a
. Sunt echivalente; este doar o modalitate convenabilă de a le numi pentru a face codul dvs. mai ușor de citit, astfel încât atunci când alții văd obj.r
, ei înțeleg asta obj
reprezintă o culoare.
Vedeți o problemă cu codul de mai sus? Încercați să faceți clic pe mergeți pe ecran complet în partea din dreapta jos a ferestrei de previzualizare.
Proporția ecranului roșu va diferi în funcție de dimensiunea ecranului. Pentru a vă asigura că exact jumătate din ecran este roșu, trebuie să știm cât de mare este ecranul nostru. Dimensiunea ecranului este nu o construită în locație variabilă ca pixel a fost, pentru că este, de obicei, până la tine, programatorul care a construit aplicația, pentru a stabili asta. În acest caz, dezvoltatorii ShaderToy au stabilit dimensiunea ecranului.
Dacă ceva nu este o variabilă construită, puteți trimite acele informații de la CPU (programul principal) la GPU (shaderul). ShaderToy se ocupă de asta pentru noi. Puteți vedea toate variabilele care sunt transmise la shader în Intrări Shader tab. Variabilele trecute în acest mod de la CPU la GPU sunt numite uniformă în GLSL.
Să ne tweak codul de mai sus pentru a obține corect centrul ecranului. Va trebui să folosim intrarea shader iResolution
:
void mainImage (din vec4 fragColor, în vec2 fragCoord) vec2 xy = fragCoord.xy; // obținem coordonatele pentru pixelul curent xy.x = xy.x / iResolution.x; // Împărțim coordonatele după dimensiunea ecranului xy.y = xy.y / iResolution.y; // Acum x este 0 pentru pixelul din stânga și 1 pentru pixelul din dreapta vec4 solidRed = vec4 (0,0,0,0,0,1,0); // Aceasta este de fapt negru acum dacă (xy.x> 0.5) solidRed.r = 1.0; // Setați componenta roșie la 1.0 fragColor = solidRed;
Dacă încercați să măriți fereastra de previzualizare de această dată, culorile ar trebui să împartă perfect ecranul pe jumătate.
A transforma acest lucru într-un gradient ar trebui să fie destul de ușor. Valorile noastre de culoare merg de la 0
la 1
, și coordonatele noastre merg acum 0
la 1
de asemenea.
void mainImage (din vec4 fragColor, în vec2 fragCoord) vec2 xy = fragCoord.xy; // obținem coordonatele pentru pixelul curent xy.x = xy.x / iResolution.x; // Împărțim coordonatele după dimensiunea ecranului xy.y = xy.y / iResolution.y; // Acum x este 0 pentru pixelul din stânga și 1 pentru pixelul din dreapta vec4 solidRed = vec4 (0,0,0,0,0,1,0); // Aceasta este de fapt negru chiar acum solidRed.r = xy.x; // Setați componenta roșie la valoarea x normalizată fragColor = solidRed;
Și voila!
Provocare: Puteți transforma acest lucru într-un gradient vertical? Cum rămâne cu diagonala? Ce zici de un gradient cu mai mult de o culoare?
Dacă jucați cu acest lucru suficient, puteți spune că colțul din stânga sus are coordonate (0,1)
, nu (0,0)
. Acest lucru este important să rețineți.
Jocul cu culorile este distractiv, dar dacă vrem să facem ceva impresionant, shader-ul nostru trebuie să poată lua o imagine și să o modifice. În acest fel, putem face un shader care afectează întregul nostru ecran de joc (cum ar fi un efect de fluid subacvatic sau o corecție a culorii) sau afectează numai anumite obiecte în anumite moduri, pe baza intrărilor (cum ar fi un sistem de iluminare realist).
Dacă programăm pe o platformă normală, ar trebui să trimitem imaginea (sau textura) la GPU ca un uniformă, în același mod în care ați fi trimis rezoluția ecranului. ShaderToy are grijă de asta pentru noi. Există patru canale de intrare în partea de jos:
ShaderToy patru canale de intrare.Click pe iChannel0 și selectați orice textură (imagine) care vă place.
Odată ce sa terminat, aveți acum o imagine care este transmisă shader-ului tău. Există însă o problemă: nu existăDrawImage ()
funcţie. Amintiți-vă, singurul lucru pe care îl poate face vreodată pixelul shader este modificați culoarea fiecărui pixel.
Deci, dacă putem reveni doar la o culoare, cum să ne desenăm textura pe ecran? Trebuie să cartografiem cumva pixelul curent pe care se află shader-ul nostru, la pixelul corespunzător texturii:
În funcție de locul în care este afișat (0,0) pe ecran, este posibil să fie nevoie să răsturnați axa y pentru a plasa corect textura. La momentul redactării, ShaderToy a fost actualizat pentru a avea originea în partea stângă sus, deci nu este nevoie să răsturnați nimic.Putem face acest lucru prin utilizarea funcției Textura (textureData, coordonate)
, care ia date de textură și o (X y)
coordonează perechea ca intrări și returnează culoarea texturii la acele coordonate ca a vec4
.
Puteți potrivi coordonatele cu ecranul în orice fel doriți. Ați putea desena întreaga textură pe un sfert din ecran (prin săriți pixeli, scalând-o efectiv în jos) sau purtați doar o parte din textură.
Pentru scopurile noastre, vrem doar să vedem imaginea, așa că vom potrivi pixelilor 1: 1:
void mainImage (vec4 fragColor, în vec2 fragCoord) vec2 xy = fragCoord.xy / iResolution.xy; // Condensarea într-o singură linie vec4 texColor = textură (iChannel0, xy); // Obținerea pixelului la xy din iChannel0 fragColor = texColor; // Setați pixelul de ecran la acea culoare
Cu asta, avem prima noastră imagine!
Acum, că trageți în mod corect datele dintr-o textură, puteți să o manipulați oricum vă place! Puteți să o întindeți și să o scalați sau să vă jucați cu culorile sale.
Să încercăm să modificăm acest lucru cu un gradient, similar cu ceea ce am făcut mai sus:
texColor.b = xy.x;
Felicitări, tocmai tocmai ați făcut primul dvs. efect de postprocesare!
Provocare: Puteți scrie un shader care va transforma o imagine alb-negru?
Rețineți că, deși este o imagine statică, ceea ce vedeți în fața dvs. se întâmplă în timp real. Puteți vedea acest lucru pentru dvs. prin înlocuirea imaginii statice cu un videoclip: faceți clic pe iChannel0 intrați din nou și selectați unul dintre videoclipuri.
Până acum, toate efectele noastre au fost statice. Putem să facem lucruri mult mai interesante utilizând informațiile pe care ni le oferă ShaderToy. iGlobalTime
este o variabilă în continuă creștere; îl putem folosi ca o sămânță pentru a face efecte periodice. Să încercăm să jucăm puțin în culori:
void mainImage (vec4 fragColor, în vec2 fragCoord) vec2 xy = fragCoord.xy / iResolution.xy; // Condensarea într-o singură linie vec4 texColor = textură (iChannel0, xy); // Obtineti pixelul la xy din iChannel0 texColor.r * = abs (sin (iGlobalTime)); texColor.g * = abs (cos (iGlobalTime)); texColor.b * = abs (sin (iGlobalTime) * cos (iGlobalTime)); fragColor = texColor; // Setați pixelul ecranului la acea culoare
Există funcții sinusoidale și cosinuse integrate în GLSL, precum și multe alte funcții utile, cum ar fi obținerea lungimii unui vector sau a distanței dintre doi vectori. Culorile nu ar trebui să fie negative, așa că ne asigurăm că obținem valoarea absolută folosind abs
funcţie.
Provocare: Puteți face un shader care schimbă o imagine înainte și înapoi de la alb-negru la culoarea completă?
În timp ce s-ar putea să fiți obișnuiți să vă depășiți codul și să tipăriți valorile din toate pentru a vedea ce se întâmplă, acest lucru nu este cu adevărat posibil atunci când scrieți shadere. S-ar putea să găsiți niște instrumente de depanare specifice platformei dvs., dar, în general, cel mai bun pariu este să setați valoarea pe care o testați la ceva grafic pe care îl puteți vedea.
Acestea sunt doar principiile de lucru cu shadere, dar obținerea de confortabil cu aceste fundamentale vă va permite să faceți mult mai mult. Consultați efectele asupra ShaderToy și vedeți dacă puteți înțelege sau replica unele dintre ele!
Un lucru pe care nu l-am menționat în acest tutorial este Vertex Shaders. Acestea sunt încă scrise în aceeași limbă, cu excepția faptului că rulează pe fiecare vârf în locul fiecărui pixel și returnează o poziție și o culoare. Vertex Shaders sunt de obicei responsabile pentru proiectarea unei scene 3D pe ecran (ceva ce este construit în majoritatea conductelor grafice). Pixel shaderele sunt responsabile pentru multe dintre efectele avansate pe care le vedem, de aceea ei se concentrează pe noi.
Provocare finală: Puteți scrie un shader care elimină ecranul verde în videoclipurile de pe ShaderToy și adaugă un alt videoclip ca fundal la primul?
Asta e tot pentru acest ghid! Aș aprecia foarte mult feedback-ul și întrebările dumneavoastră. Dacă există ceva specific pe care doriți să aflați mai multe, vă rugăm să lăsați un comentariu. Ghidurile viitoare ar putea include subiecte cum ar fi elementele de bază ale sistemelor de iluminare sau modul de realizare a simulării fluidelor sau crearea de shadere pentru o anumită platformă.