Een beginnershandleiding voor het coderen van grafische Shaders

Het leren schrijven van grafische shaders is leren gebruikmaken van de kracht van de GPU, met zijn duizenden kernen allemaal parallel. Het is een soort van programmering die een andere mindset vereist, maar het ontsluiten van het potentieel is de eerste problemen waard. 

Vrijwel elke moderne grafische simulatie die je ziet, wordt op de een of andere manier aangedreven door code geschreven voor de GPU, van de realistische lichteffecten in geavanceerde AAA-games tot 2D post-processing effecten en vloeibare simulaties. 

Een scène in Minecraft, voor en na het toepassen van een paar shaders.

Het doel van deze gids

Shader-programmeren komt soms uit als een raadselachtige zwarte magie en wordt vaak verkeerd begrepen. Er zijn veel codevoorbeelden die je laten zien hoe je ongelooflijke effecten kunt creëren, maar bieden weinig of geen uitleg. Deze gids is bedoeld om die kloof te overbruggen. Ik zal me meer concentreren op de basisprincipes van het schrijven en begrijpen van shader-code, zodat je gemakkelijk je eigen stijl kunt aanpassen, combineren of schrijven!

Dit is een algemene gids, dus wat u hier leert, is van toepassing op alles dat shaders kan uitvoeren.

Dus wat is een Shader?

Een arcering is gewoon een programma dat wordt uitgevoerd in de grafische pipeline en vertelt de computer hoe elke pixel moet worden gerenderd. Deze programma's worden shaders genoemd omdat ze vaak worden gebruikt om belichting- en schaduweffecten te regelen, maar er is geen reden waarom ze andere speciale effecten niet aankunnen. 

Shaders zijn geschreven in een speciale shading-taal. Maak je geen zorgen, je hoeft niet uit te gaan en een volledig nieuwe taal te leren; we zullen GLSL gebruiken (OpenGL Shading Language) wat een C-achtige taal is. (Er zijn een aantal schakeringstalen beschikbaar voor verschillende platforms, maar omdat ze allemaal zijn aangepast om op de GPU te worden uitgevoerd, zijn ze allemaal erg op elkaar.)

Opmerking: dit artikel gaat uitsluitend over fragmentenschaduwen. Als je nieuwsgierig bent naar wat er nog meer is, kun je lezen over de verschillende stadia in de grafische pipeline op de OpenGL Wiki.

Laten we naar binnen springen!

We gebruiken ShaderToy voor deze zelfstudie. Hiermee kunt u beginnen met het programmeren van shaders in uw browser, zonder dat u iets hoeft op te zetten! (Het gebruikt WebGL voor weergave, dus je hebt een browser nodig die dit kan ondersteunen.) Een account aanmaken is optioneel, maar handig om je code op te slaan.

Notitie: ShaderToy bevindt zich op het moment van schrijven van dit artikel in bèta. Sommige kleine UI / syntaxisdetails kunnen enigszins afwijken. 

Als u op Nieuwe Shader klikt, ziet u ongeveer zoiets als dit:

Uw interface kan er iets anders uitzien als u niet bent aangemeld.

De kleine zwarte pijl onderaan is waarop u klikt om uw code te compileren.

Wat is er gaande?

Ik ga uitleggen hoe shaders in één zin werken. Ben je klaar? Hier gaat!

Het enige doel van een arcering is om vier getallen te retourneren: r, g, b,en een.

Dat is alles wat het ooit doet of kan doen. De functie die u voor u ziet, loopt voor elke afzonderlijke pixel op het scherm. Het geeft die vier kleurwaarden terug en dat wordt de kleur van de pixel. Dit is wat a heet Pixel Shader(soms aangeduid als a Fragment Shader). 

Laten we met dat in gedachten proberen ons scherm effen rood te maken. De rgba-waarden (rood, groen, blauw en "alpha", die de transparantie definiëren) gaan van 0 naar 1, dus alles wat we moeten doen is terugkeren r, g, b, a = 1,0,0,1. ShaderToy verwacht dat de uiteindelijke pixelkleur wordt opgeslagen fragColor.

void mainImage (uit vec4 fragColor, in vec2 fragCoord) fragColor = vec4 (1.0,0.0,0.0.0.0); 

Gefeliciteerd! Dit is je allereerste werkende arcering!

Uitdaging: Kunt u het in een effen grijze kleur veranderen?

vec4 is slechts een gegevenstype, dus we hadden onze kleur kunnen aangeven als variabele, zoals zo:

void mainImage (uit vec4 fragColor, in vec2 fragCoord) vec4 solidRed = vec4 (1.0,0.0,0.0.0.0); fragColor = solidRed; 

Dit is echter niet erg spannend. We hebben de bevoegdheid om code uit te voeren honderdduizenden van pixels parallel en we zetten ze allemaal op dezelfde kleur. 

Laten we proberen een verloop over het scherm weer te geven. Nou, we kunnen niet veel doen zonder een paar dingen te weten over de pixel die we beïnvloeden, zoals de locatie op het scherm ...

Shader-ingangen

De pixel shader geeft enkele variabelen door die u kunt gebruiken. De meest bruikbare is voor ons fragCoorddie de x- en y-coördinaten van de pixel (en z, als je in 3D werkt) houdt. Laten we proberen alle pixels in de linker helft van het scherm zwart te maken, en al die aan de rechterkant half rood:

void mainImage (uit vec4 fragColor, in vec2 fragCoord) vec2 xy = fragCoord.xy; // We verkrijgen onze coördinaten voor de huidige pixel vec4 solidRed = vec4 (0,0.0,0.0,1.0); // Dit is momenteel zwart als (xy.x> 300.0) // arbitrair nummer, we niet weet hoe groot ons scherm is! solidRed.r = 1.0; // Stel de rode component in op 1.0 fragColor = solidRed; 

Notitie: Voor enige vec4, je hebt toegang tot de componenten via obj.x, obj.y, obj.z en obj.w ofviaobj.r, obj.g, obj.b, obj.a. Ze zijn equivalent; het is gewoon een handige manier om ze te benoemen om uw code beter leesbaar te maken, zodat anderen dit kunnen zien obj.r, ze begrijpen dat obj staat voor een kleur.

Zie je een probleem met de bovenstaande code? Klik op de Ga naar volledig scherm knop rechtsonder in uw voorbeeldvenster.

Het gedeelte van het scherm dat rood is, is afhankelijk van de grootte van het scherm. Om ervoor te zorgen dat precies de helft van het scherm rood is, moeten we weten hoe groot ons scherm is. Schermgrootte is niet een ingebouwde variabele zoals de pixellocatie was, omdat het meestal aan jou is, de programmeur die de app heeft gebouwd, om dat in te stellen. In dit geval zijn het de ShaderToy-ontwikkelaars die de schermgrootte hebben ingesteld.  

Als iets geen ingebouwde variabele is, kunt u die informatie verzenden van de CPU (uw hoofdprogramma) naar de GPU (uw arcering). ShaderToy verwerkt dat voor ons. U kunt zien dat alle variabelen worden doorgegeven aan de arcering in de Shader-ingangen tab. Variabelen die op deze manier worden doorgegeven van CPU naar GPU worden genoemd uniform in GLSL. 

Laten we onze code hierboven aanpassen om het midden van het scherm correct te verkrijgen. We zullen de arceringang moeten gebruiken iResolution:

void mainImage (uit vec4 fragColor, in vec2 fragCoord) vec2 xy = fragCoord.xy; // We verkrijgen onze coördinaten voor de huidige pixel xy.x = xy.x / iResolution.x; // We delen de coördinaten in op schermgrootte xy.y = xy.y / iResolution.y; // Nu is x 0 voor de meest linkse pixel en 1 voor de meest rechtse pixel vec4 solidRed = vec4 (0,0.0,0.0, 1.0); // Dit is momenteel zwart als (xy.x> 0.5) solidRed.r = 1.0; // Stel de rode component in op 1.0 fragColor = solidRed; 

Als u deze keer probeert het voorbeeldvenster te vergroten, moeten de kleuren het scherm nog steeds perfect in tweeën splitsen.

Van een split naar een verloop

Dit in een gradiënt veranderen zou vrij gemakkelijk moeten zijn. Onze kleurwaarden gaan van 0 naar 1, en onze coördinaten gaan nu van 0 naar 1 ook.

void mainImage (uit vec4 fragColor, in vec2 fragCoord) vec2 xy = fragCoord.xy; // We verkrijgen onze coördinaten voor de huidige pixel xy.x = xy.x / iResolution.x; // We delen de coördinaten in op schermgrootte xy.y = xy.y / iResolution.y; // Nu is x 0 voor de meest linkse pixel en 1 voor de meest rechtse pixel vec4 solidRed = vec4 (0,0.0,0.0, 1.0); // Dit is eigenlijk zwart nu solidRed.r = xy.x; // Stel de rode component in op de genormaliseerde x-waarde fragColor = solidRed; 

En voila! 

Uitdaging: Kun je dit in een verticaal verloop veranderen? Hoe zit het met diagonaal? Hoe zit het met een verloop met meer dan één kleur?

Als je hier genoeg mee speelt, kun je zien dat de linkerbovenhoek coördinaten heeft (0,1), niet (0,0). Dit is belangrijk om in gedachten te houden. 

Afbeeldingen tekenen

Met kleuren spelen is leuk, maar als we iets indrukwekkends willen doen, moet onze shader de invoer van een afbeelding kunnen aannemen en deze kunnen wijzigen. Op deze manier kunnen we een arcering maken die ons hele spelscherm beïnvloedt (zoals een onderwatervloeistofeffect of kleurcorrectie) of alleen bepaalde objecten op bepaalde manieren beïnvloeden op basis van de inputs (zoals een realistisch verlichtingssysteem).

Als we op een normaal platform zouden programmeren, zouden we onze afbeelding (of textuur) naar de GPU moeten sturen als een uniform, op dezelfde manier als waarop u de schermresolutie zou hebben verzonden. ShaderToy zorgt voor dat voor ons. Er zijn onderaan vier invoerkanalen:

ShaderToy's vier ingangskanalen.

Klik op iChannel0 en selecteer elke gewenste textuur (afbeelding).

Zodra dat is gebeurd, hebt u nu een afbeelding die wordt doorgegeven aan uw arcering. Er is echter één probleem: er is geen probleemDrawImage () functie. Onthoud dat het enige dat de pixel-shader ooit kan doen, is verander de kleur van elke pixel.

Dus als we alleen een kleur kunnen retourneren, hoe tekenen we dan onze textuur op het scherm? We moeten op de een of andere manier de huidige pixel van onze arcering in kaart brengen, naar de overeenkomstige pixel op de textuur:

Afhankelijk van waar de (0,0) op het scherm staat, moet u misschien de y-as omdraaien om uw textuur correct weer te geven. Op het moment van schrijven is ShaderToy bijgewerkt om zijn oorsprong links bovenaan te hebben, dus er hoeft niets te worden omgedraaid.

We kunnen dit doen door de functie te gebruiken textuur (textureData, coördinaten), wat textuurgegevens en een (x, y) coördineer het paar als invoer en retourneert de kleur van de textuur op die coördinaten als a vec4.

U kunt de coördinaten op elke gewenste manier aan het scherm aanpassen. Je zou de hele textuur op een kwart van het scherm kunnen tekenen (door pixels over te slaan, het effectief verkleinen) of gewoon een deel van de textuur tekenen. 

Voor onze doeleinden willen we alleen de afbeelding zien, dus matchen we de pixels 1: 1:

void mainImage (uit vec4 fragColor, in vec2 fragCoord) vec2 xy = fragCoord.xy / iResolution.xy; // Dit condenseert in één regel vec4 texColor = texture (iChannel0, xy); // Haal de pixel op xy van iChannel0 fragColor = texColor; // Stel de schermpixel in op die kleur

Daarmee hebben we onze eerste afbeelding!

Nu u de gegevens correct uit een textuur haalt, kunt u deze manipuleren zoals u maar wilt! Je kunt het uitrekken en schalen, of rond spelen met zijn kleuren.

Laten we dit aanpassen met een verloop, vergelijkbaar met wat we hierboven hebben gedaan:

texColor.b = xy.x;

Gefeliciteerd, je hebt zojuist je eerste nabewerkingseffect gemaakt!

Uitdaging: Kun je een arcering schrijven die een afbeelding zwart en wit maakt?

Merk op dat, hoewel het een statische afbeelding is, wat u voor uw ogen ziet, in realtime gebeurt. Je kunt dit zelf zien door de statische afbeelding te vervangen door een video: klik op de iChannel0 voer opnieuw in en selecteer een van de video's. 

Beweging toevoegen

Tot dusverre zijn al onze effecten statisch geweest. We kunnen veel meer interessante dingen doen door gebruik te maken van de inputs die ShaderToy ons geeft. iGlobalTimeis een constant toenemende variabele; we kunnen het gebruiken als een zaadje om periodieke effecten te maken. Laten we eens proberen een beetje met kleuren te spelen:

void mainImage (uit vec4 fragColor, in vec2 fragCoord) vec2 xy = fragCoord.xy / iResolution.xy; // Condensatie hiervan in één regel vec4 texColor = texture (iChannel0, xy); // Haal de pixel op xy van iChannel0 texColor.r * = abs (sin (iGlobalTime)); texColor.g * = abs (cos (iGlobalTime)); texColor.b * = abs (sin (iGlobalTime) * cos (iGlobalTime)); fragColor = texColor; // Stel de schermpixel in op die kleur

Er zijn sinus- en cosinusfuncties ingebouwd in GLSL, evenals vele andere nuttige functies, zoals het verkrijgen van de lengte van een vector of de afstand tussen twee vectoren. Kleuren mogen niet negatief zijn, dus we zorgen ervoor dat we de absolute waarde krijgen door de buikspieren functie.

Uitdaging: Kun je een arcering maken die een afbeelding heen en weer schakelt van zwart-wit naar volledige kleur?

Een opmerking over het debuggen van Shaders

Hoewel je misschien gewend bent om door je code te stappen en de waarden van alles af te drukken om te zien wat er aan de hand is, is dat niet echt mogelijk bij het schrijven van shaders. Misschien vindt u enkele foutopsporingstools die specifiek zijn voor uw platform, maar over het algemeen is uw beste gok om de waarde die u test in te stellen op iets dat u in plaats daarvan kunt zien.

Conclusie

Dit zijn slechts de basisprincipes van het werken met shaders, maar als u zich vertrouwd maakt met deze basisprincipes, kunt u nog veel meer doen. Blader door de effecten op ShaderToy en kijk of u sommige ervan kunt begrijpen of repliceren!

Een ding dat ik niet noemde in deze tutorial is Vertex ShadersZe zijn nog steeds in dezelfde taal geschreven, behalve dat ze op elk hoekpunt in plaats van op elke pixel worden uitgevoerd en zowel een positie als een kleur retourneren. Vertex Shaders zijn meestal verantwoordelijk voor het projecteren van een 3D-scène op het scherm (iets dat is ingebouwd in de meeste grafische pipelines). Pixelshaders zijn verantwoordelijk voor veel van de geavanceerde effecten die we te zien krijgen, dus dat is waarom ze onze focus zijn.

Laatste uitdaging: Kun je een arcering schrijven die het groene scherm in de video's op ShaderToy verwijdert en een andere video toevoegt als achtergrond voor de eerste?

Dat is alles voor deze gids! Ik stel uw feedback en vragen zeer op prijs. Als er iets specifieks is waarover u meer wilt weten, laat dan een reactie achter. Toekomstige handleidingen kunnen onderwerpen zijn zoals de basis van verlichtingssystemen, of hoe u een vloeistof-simulatie kunt maken, of shaders instellen voor een specifiek platform.