Lære å skrive grafikk shaders er å lære å utnytte kraften i GPU, med tusenvis av kjerner som kjører parallelt. Det er en slags programmering som krever en annen tankegang, men å låse opp potensialet er verdt de første problemene.
Nesten alle moderne grafikkimuleringer du ser, er drevet på en eller annen måte med kode skrevet for GPU, fra de realistiske lyseffektene i cutting edge AAA-spill til 2D etterbehandlingseffekter og væskesimuleringer.
En scene i Minecraft, før og etter å bruke noen shaders.Shader programmering kommer noen ganger ut som en gåtefull svart magi og blir ofte misforstått. Det er mange kodeprøver der ute som viser deg hvordan du oppretter utrolige effekter, men gir liten eller ingen forklaring. Denne veiledningen tar sikte på å bygge bro over det gapet. Jeg vil fokusere mer på grunnleggende skriving og forståelse av skyggekode, slik at du enkelt kan justere, kombinere eller skrive din egen fra grunnen av!
Dette er en generell guide, så det du lærer her, gjelder for alt som kan kjøre shaders.
En skygge er ganske enkelt et program som går i grafikkrøret og forteller datamaskinen hvordan du gjengir hver piksel. Disse programmene kalles shaders fordi de ofte brukes til å kontrollere belysnings- og skyggingseffekter, men det er ingen grunn til at de ikke klarer å håndtere andre spesialeffekter.
Shaders er skrevet i et spesielt skyggespråk. Ikke bekymre deg, du trenger ikke å gå ut og lære et helt nytt språk; Vi skal bruke GLSL (OpenGL Shading Language) som er et C-lignende språk. (Det er en mengde skygge språk der ute for forskjellige plattformer, men siden de er alle tilpasset å kjøre på GPU, er de alle veldig like.)
Merk: Denne artikkelen handler bare om fragment shaders. Hvis du er nysgjerrig på hva annet er der ute, kan du lese om ulike stadier i grafikkrøret på OpenGL Wiki.
Vi bruker ShaderToy for denne opplæringen. Dette lar deg begynne å programmere shaders rett i nettleseren din, uten å måtte sette noe opp! (Det bruker WebGL for gjengivelse, så du trenger en nettleser som kan støtte det.) Opprettelse av en konto er valgfri, men nyttig for å lagre koden din.
Merk: ShaderToy er i beta ved skriving av denne artikkelen. Noen små brukergrensesnitt / syntax-detaljer kan være litt forskjellige.
Når du klikker New Shader, bør du se noe slikt:
Grensesnittet ditt kan se litt annerledes ut hvis du ikke er logget inn.Den lille, svarte pilen på bunnen er det du klikker for å kompilere koden din.
Jeg skal forklare hvordan shaders jobber i en setning. Er du klar? Her går!
En sjarmers eneste formål er å returnere fire numre: r
, g
, b
,og en
.
Det er alt det noen gang gjør eller kan gjøre. Funksjonen du ser foran deg, går for hver enkelt piksel på skjermen. Den returnerer de fire fargeværdiene, og det blir fargepiksen. Dette er det som kalles a Pixel Shader(noen ganger referert til som a Fragment Shader).
Med det i tankene, la oss prøve å snu skjermen vår med en solid rød. Rgba (rød, grønn, blå og "alfa", som definerer gjennomsiktighetsverdiene), går fra 0
til 1
, så alt vi trenger å gjøre er å returnere r, g, b, a = 1,0,0,1
. ShaderToy forventer at den endelige pixelfargen skal lagres i fragColor
.
void mainImage (ut vec4 fragColor, i vec2 fragCoord) fragColor = vec4 (1,0,0,0,0,0,1,0);
Gratulerer! Dette er din aller første arbeidsskader!
Utfordring: Kan du bytte den til en solid grå farge?
vec4
er bare en datatype, så vi kunne ha erklært fargen vår som en variabel, slik som:
void mainImage (ut vec4 fragColor, i vec2 fragCoord) vec4 solidRed = vec4 (1,0,0,0,0,0,1,0); fragColor = solidRed;
Dette er ikke veldig spennende, skjønt. Vi har muligheten til å kjøre kode på hundretusenvis av piksler parallelt og vi setter dem alle i samme farge.
La oss prøve å gi en gradient over skjermen. Vel, vi kan ikke gjøre mye uten å vite noen ting om pikselet vi påvirker, for eksempel beliggenheten på skjermen ...
Pixel shader passerer noen få variabler for deg å bruke. Den mest nyttige en til oss er fragCoord
, som holder pikselets x og y (og z, hvis du jobber i 3D) koordinater. La oss prøve å snu alle pikslene på venstre halvdel av skjermen svart, og alle dem på høyre halvdel rødt:
void mainImage (ut vec4 fragColor, i vec2 fragCoord) vec2 xy = fragCoord.xy; // Vi får våre koordinater for den nåværende pixel vec4 solidRed = vec4 (0,0,0,0,0,1,0); // Dette er faktisk svart akkurat nå hvis (xy.x> 300.0) // Vilkårlig tall, vi gjør det ikke vet hvor stor skjermen vår er! solidRed.r = 1.0; // Sett sin røde komponent til 1,0 fragColor = solidRed;
Merk: For noen vec4
, Du kan få tilgang til komponentene via obj.x
, obj.y
, obj.z
og obj.w
ellerviaobj.r
, obj.g
, obj.b
, obj.a
. De er likeverdige; Det er bare en praktisk måte å navngi dem for å gjøre koden din mer lesbar, slik at når andre ser obj.r
, de forstår det obj
representerer en farge.
Ser du et problem med koden ovenfor? Prøv å klikke på gå fullskjerm knappen nederst til høyre i forhåndsvisningsvinduet.
Andelen av skjermen som er rød vil variere avhengig av skjermstørrelsen. For å sikre at nøyaktig halvparten av skjermen er rød, må vi vite hvor stor skjermen vår er. Skjermstørrelsen er ikke en innebygd variabel som pikselplassering var, fordi det er vanligvis opp til deg, programmøren som bygde appen, for å sette det. I dette tilfellet er det ShaderToy-utviklerne som angir skjermstørrelsen.
Hvis noe ikke er en innebygd variabel, kan du sende den informasjonen fra CPUen (hovedprogrammet ditt) til GPU (din skygge). ShaderToy håndterer det for oss. Du kan se alle variablene som sendes til skyggeren i Shader innganger fane. Variabler passert på denne måten fra CPU til GPU kalles uniform i GLSL.
La oss justere koden ovenfor for å få midt på skjermen. Vi må bruke shader-inngangen iResolution
:
void mainImage (ut vec4 fragColor, i vec2 fragCoord) vec2 xy = fragCoord.xy; // Vi får våre koordinater for den nåværende piksel xy.x = xy.x / iResolution.x; // Vi deler koordinatene med skjermstørrelsen xy.y = xy.y / iResolution.y; // Nå er x 0 for venstre piksel, og 1 for høyre piksel vec4 solidRed = vec4 (0,0,0,0,0,1,0); // Dette er faktisk svart akkurat nå hvis (xy.x> 0.5) solidRed.r = 1.0; // Sett sin røde komponent til 1,0 fragColor = solidRed;
Hvis du prøver å forstørre forhåndsvisningsvinduet denne gangen, bør fargene fortsatt splitte skjermen helt i halvparten.
Å gjøre dette til en gradient bør være ganske enkelt. Våre fargeverdier går fra 0
til 1
, og våre koordinater går nå fra 0
til 1
også.
void mainImage (ut vec4 fragColor, i vec2 fragCoord) vec2 xy = fragCoord.xy; // Vi får våre koordinater for den nåværende piksel xy.x = xy.x / iResolution.x; // Vi deler koordinatene med skjermstørrelsen xy.y = xy.y / iResolution.y; // Nå er x 0 for venstre piksel, og 1 for høyre piksel vec4 solidRed = vec4 (0,0,0,0,0,1,0); // Dette er faktisk svart akkurat nå solidRed.r = xy.x; // Sett sin røde komponent til den normaliserte x-verdien fragColor = solidRed;
Og voila!
Utfordring: Kan du gjøre dette til en vertikal gradient? Hva med diagonal? Hva med en gradient med mer enn en farge?
Hvis du leker med dette nok, kan du se at øverste venstre hjørne har koordinater (0,1)
, ikke (0,0)
. Dette er viktig å huske på.
Å spille med farger er morsomt, men hvis vi ønsker å gjøre noe imponerende, må vår shader kunne ta innspill fra et bilde og endre det. På denne måten kan vi lage en skygge som påvirker hele vår spillskjerm (som en undervannsfluidseffekt eller fargekorrigering) eller bare påvirker visse objekter på bestemte måter basert på inngangene (som et realistisk belysningssystem).
Hvis vi var programmering på en vanlig plattform, ville vi måtte sende bildet vårt (eller tekstur) til GPU som en uniform, På samme måte som du ville ha sendt skjermoppløsningen. ShaderToy tar vare på det for oss. Det finnes fire inngangskanaler nederst:
ShaderToy er fire inngangskanaler.Klikk på iChannel0 og velg hvilken som helst tekstur (bildet) du liker.
Når det er gjort, har du nå et bilde som sendes til din skygge. Det er ett problem, men det er neiDrawImage ()
funksjon. Husk at det eneste pikselskyggeren noensinne kan gjøre er endre fargen på hver piksel.
Så hvis vi bare kan returnere en farge, hvordan tegner vi tekstur på skjermen? Vi må på en eller annen måte kartlegge den nåværende piksel vår skygge er på, til den tilsvarende piksel på tekstur:
Avhengig av hvor (0,0) er på skjermen, må du kanskje vende på y-aksen for å kartlegge tekstur på riktig måte. På tidspunktet for skriving har ShaderToy blitt oppdatert for å ha sin opprinnelse øverst til venstre, så det er ikke nødvendig å vende noe.Vi kan gjøre dette ved å bruke funksjonen tekstur (textureData, koordinater)
, som tar teksturdata og en (x, y)
koordinere par som innganger, og returnerer fargen på teksturen til disse koordinatene som en vec4
.
Du kan matche koordinatene til skjermen på en måte du liker. Du kan tegne hele tekstur på et kvart av skjermen (ved å hoppe over piksler, effektivt skalere den ned) eller bare tegne en del av tekstur.
For våre formål ønsker vi bare å se bildet, så vi vil matche pikselene 1: 1:
void mainImage (ut vec4 fragColor, i vec2 fragCoord) vec2 xy = fragCoord.xy / iResolution.xy; // Kondenserer dette til en linje vec4 texColor = tekstur (iChannel0, xy); // Få piksel på xy fra iChannel0 fragColor = texColor; // Sett skjermpiksel til den fargen
Med det har vi vårt første bilde!
Nå som du riktig trekker data fra en tekstur, kan du manipulere det uansett! Du kan strekke den og skala den, eller leke med farger.
La oss prøve å endre dette med en gradient som ligner på hva vi gjorde over:
texColor.b = xy.x;
Gratulerer, du har nettopp laget din første etterbehandlingseffekt!
Utfordring: Kan du skrive en skygge som vil slå et bilde svart og hvitt?
Vær oppmerksom på at selv om det er et statisk bilde, skjer det i sanntid i hva du ser foran deg. Du kan se dette selv ved å erstatte det statiske bildet med en video: klikk på iChannel0 skriv inn igjen og velg en av videoene.
Så langt har alle effektene vært statiske. Vi kan gjøre mye mer interessante ting ved å benytte seg av inngangene som ShaderToy gir oss. iGlobalTime
er en stadig økende variabel; vi kan bruke det som et frø for å gjøre periodiske effekter. La oss prøve å leke med farger litt:
void mainImage (ut vec4 fragColor, i vec2 fragCoord) vec2 xy = fragCoord.xy / iResolution.xy; // Kondenserer dette til en linje vec4 texColor = tekstur (iChannel0, xy); // Få piksel på xy fra iChannel0 texColor.r * = abs (sin (iGlobalTime)); texColor.g * = abs (cos (iGlobalTime)); texColor.b * = abs (sin (iGlobalTime) * cos (iGlobalTime)); fragColor = texColor; // Sett skjermpiksel til den fargen
Det er sine og cosinusfunksjoner som er bygd inn i GLSL, samt mange andre nyttige funksjoner, som å få lengden på en vektor eller avstanden mellom to vektorer. Farger er ikke ment å være negative, så vi sørger for at vi får absolutt verdien ved å bruke abs
funksjon.
Utfordring: Kan du lage en skygge som endrer et bilde frem og tilbake fra svart-hvitt til full farge?
Mens du kanskje er vant til å gå gjennom koden din og skrive ut verdiene for alt for å se hva som skjer, er det ikke veldig mulig når du skriver shaders. Du kan finne noen feilsøkingsverktøy som er spesifikke for plattformen din, men generelt er det beste alternativet du skal angi verdien du tester til noe grafisk du kan se i stedet.
Disse er bare grunnleggende for å jobbe med shaders, men å bli komfortabel med disse grunnleggende vil tillate deg å gjøre så mye mer. Bla gjennom effektene på ShaderToy og se om du kan forstå eller kopiere noen av dem!
En ting jeg ikke nevnte i denne opplæringen er Vertex Shaders. De er fortsatt skrevet på samme språk, bortsett fra at de kjører på hvert toppunkt i stedet for hver piksel, og de returnerer både en stilling og en farge. Vertex Shaders er vanligvis ansvarlig for å projisere en 3D-scene på skjermen (noe som er bygd inn i de fleste grafikkledninger). Pixel shaders er ansvarlige for mange av de avanserte effektene vi ser, og derfor er de vårt fokus.
Endelig utfordring: Kan du skrive en skygge som fjerner den grønne skjermen i videoene på ShaderToy og legger til en annen video som bakgrunn til den første?
Det er alt for denne veiledningen! Jeg vil sterkt sette pris på din tilbakemelding og spørsmål. Hvis det er noe spesifikt du vil lære mer om, vennligst legg igjen en kommentar. Fremtidige guider kan inkludere emner som grunnlaget for belysningssystemer, eller hvordan man kan lage en væskesimulering eller sette opp shaders for en bestemt plattform.