Shaders - de basisShaders zijn programmaatjes die uitgevoerd worden op de grafische kaart van je computer. Ze kunnen worden gebruikt om de dingen die je op het scherm tekent aan te passen. Je zou met een shader bijvoorbeeld alle rode pixels van een sprite tijdens het uitvoeren van het spel kunnen vervangen door blauwe pixels. Doordat shaders worden uitgevoerd op de grafische kaart belasten ze de processor niet, waardoor je geavanceerde effecten kunt maken die geen effect hebben op de snelheid van je spel.
In deze tutorial zal ik een aantal basisfuncties en -methodes uitleggen waarmee je shaders kunt maken en gebruiken. Deze tutorial zal dus lang niet alles uitleggen wat met shaders mogelijk is, maar wel genoeg om mee te starten en een aantal leuke shaders mee te maken. Ik houd het in deze tutorial op uitleg over het gebruik van shaders in 2d. Dat betekent niet dat je er niets aan hebt als je een 3d-spel maakt, maar dat dingen die speciaal in 3d mogelijk zijn met shaders hier niet uitgelegd zullen worden.
Let op: Ondanks dat ik hier alleen de basis uitleg van shaders, is dit simpelweg een lange en waarschijnlijk voor velen ook moeilijke tutorial. Dat is omdat er gewoon heel veel is om uit te leggen, waaronder een hele programmeertaal die best wat verschillen heeft met GML. Als je het uiteindelijk begrijpt heb je er echter wel heel veel aan om je spellen mooier te maken.

(en je kunt iets wat weinig anderen op dit forum ook kunnen, wat natuurlijk ook altijd leuk is

)
GameMaker-versie:
Platforms: Alle platforms worden ondersteund. Voor HTML5 wordt echter alleen WebGL ondersteund, waardoor bepaalde browsers het niet zullen uitvoeren.
Betaalde versie vereist: Nee, je kunt shaders in alle versies gebruiken. Er is wel een limiet van twee shaders in de gratis versie, tenzij je deze hebt geregistreerd.
Niveau: Shaders zijn echt iets voor gevorderden, en misschien zelfs wel experts. Je moet goed weten hoe alle (teken)functies werken, om iets met shaders te kunnen doen. Ook moet je een nieuwe programmeertaal leren om shaders te kunnen gebruiken.
InhoudsopgaveDeze tutorial is verdeeld over twee posts, omdat hij te lang was om in één post te passen.
Wat is een shader?In dit hoofdstuk leg ik kort uit wat een shader is, waar die uit bestaat, en hoe je hem schrijft.
Wat heb ik nodig?In dit hoofdstuk staat wat jij en je spelers nodig hebben om een shader te kunnen gebruiken.
BeginnenHier leg ik uit hoe shaders werken in GameMaker. Ook de basiscode waarmee je een shader kunt gebruiken in GameMaker staat in dit hoofdstuk.
Een shader schrijvenDit hoofdstuk gaat over de programmeertaal GLSL ES. Dat is de taal waarin wij shaders gaan schrijven. Dit hoofdstuk legt uit hoe basisdingen zoals variabelen, functies en berekeningen werken in deze taal. Dit hoofdstuk is verdeeld in verschillende subhoofdstukjes:
Puntkomma'sCommentsVariabelenVariabelen een waarde gevenBelangrijke ingebouwde variabelenRekenenHet if-statement en expressiesConstantenWaarden doorgeven aan een shaderFunctiesMeer over variabelen en constantenLoopsTextures doorgeven van GameMaker naar GLSL ESOverige dingetjes--Vanaf nu staat alles in de
volgende post--
VoorbeeldenDit hoofdstuk bevat met GLSL ES geschreven programmaatjes met hun resultaat. In het hoofdstuk 'Een shader schrijven' zal ik af en toe verwijzen naar een voorbeeld dat je dan goed kunt bekijken.
Controleren of shaders werkenShaders zullen niet, of niet zonder extra download, werken op alle apparaten. In dit hoofdstuk worden de twee functies omschreven waarmee je kunt controleren of shaders werken op het apparaat van de speler, zodat je je spel daarop kunt aanpassen.
Handige dingetjesHier staan twee kleine dingetjes die handig kunnen zijn als je shaders gebruikt.
Wat is een shader?Zoals ik hierboven al heb uitgelegd is een shader een programmaatje dat uitgevoerd wordt op de grafische kaart, waarmee je dingen die je op het scherm tekent kunt aanpassen. Een shader bestaat in GameMaker altijd uit twee onderdelen: een vertex shader en een fragment shader. De vertex shader wordt één keer uitgevoerd voor elke hoek van de driehoeken die getekend worden (zoals je hopelijk wel weet bestaat een getekende sprite eigenlijk uit twee driehoeken). Je kunt de vertex shader gebruiken om de posities van de hoeken van het getekende object aan te passen. De fragment shader wordt gebruikt om de kleuren van de getekende pixels aan te passen. Deze tutorial zal vooral focusen op het laatste type shader, dat overigens soms ook wel pixel shader genoemd wordt. Deze is namelijk in 2d het handigst.
Shaders worden niet geschreven in GML, maar in een speciale shader-taal. In GameMaker kun je kiezen:
- Je schrijft je shaders in GLSL ES. Je shaders werken dan op elk platform, maar je mist wel bepaalde geavanceerde functies. Dit betekent trouwens niet dat je hier niet mee kan, integendeel: je kunt heel mooie en geavanceerde effecten maken met deze taal.
- Je schrijft je shaders in één van de andere drie ondersteunde talen: GLSL, HLSL 9 of HLSL 11. De shaders zijn dan alleen maar geschikt voor bepaalde platformen, maar je kunt wel bepaalde geavanceerde functies gebruiken (afhankelijk van het platform). Als je je shaders alsnog op meerdere platforms wilt gebruiken, zul je ze in meerdere keren moeten schrijven in meerdere talen.
In deze tutorial zal ik alleen het schrijven van shaders in GLSL ES uitleggen. Als je een shader wilt schrijven in een andere taal, is de basisuitleg wel van toepassing. Alle uitleg over de taal GLSL ES is dan echter niet op jou van toepassing. Je zult dan zelf uitleg moeten opzoeken over de taal waarin je je shaders wilt schrijven.
Wat heb ik nodig?Om shaders te kunnen gebruiken heb je een recente versie van GameMaker: Studio nodig. Je kunt elke versie gebruiken, maar de Standard-versie of hoger is aangeraden, aangezien je met de gratis versie maar twee shaders kunt gebruiken. Daarnaast moet je misschien je
DirectX user runtime updaten als je shaders wilt gebruiken met Windows als target platform. Dit heb je alleen nodig als de komende voorbeeldshaders het niet doen, omdat dit op veel computers al geïnstalleerd is. Als de shaders het nog niet doen na het installeren van deze update, moet je waarschijnlijk je videokaartdrivers updaten.
Let op: Je spelers hebben ook deze update van de DirectX runtime nodig als je shaders in je Windows-spel gebruikt. Als je shaders gebruikt terwijl deze update niet is geinstalleerd zal je spel en error vertonen en crashen. Gelukkig is het wel mogelijk om te controleren of de shaders het doen, ik zal verderop in deze tutorial uitleggen hoe.
BeginnenHet wordt nu eindelijk tijd om echt iets te gaan doen met shaders! Open dus GameMaker: Studio (als dat nog niet open was) en start een leeg project. Maak een sprite en vul deze met wat kleurtjes en vormpjes. Het maakt niet zo veel uit welke kleuren en vormen je gebruikt, maar het is handig als er veel verschillende kleuren zijn zodat je kleureffecten die we later zullen gebruiken goed kunt zien. Ik heb de volgende afbeelding gebruikt:
Maak een object en geef het de net aangemaakte sprite. Maak een room aan en plaats het object erin. Zorg er voor dat je achtergrondkleur een beetje handig is zodat je je sprite goed kunt zien. Tot nu toe weinig bijzonders. Nu gaan we echt een shader aanmaken. Dat gaat eigenlijk hetzelfde als bij de andere resource-types en kan op de volgende manieren:
- In de resource tree: het mapje Shaders staat onder het mapje Scripts. Rechtsklik op het mapje en kies 'Create Shader' uit het menu dat verschijnt.
- Op de toolbar: klik op het knopje "Create a Shader". Dit staat rechts van de knop waarmee je een script kunt aanmaken.
- In het menu: kies "Create Shader" onder "Resources".
- Sneltoets: druk Ctrl+Shift+A
Je zult na het aanmaken van de shader het volgende scherm zien:

Dit lijkt een beetje op het script-venster wat je al wel kent, maar er zijn wat dingen anders. Zo is er geen syntaxis-check. Dat betekent dat je code niet wordt gecontroleerd tijdens het typen. Er worden wel gewoon kleurtjes gegeven aan onderdelen van je code zoals dat bij GML zou gebeuren. Verder zie je twee tabbladen, voor beide soorten shaders ééntje. Ook kun je naast de naam aangeven welke programmeertaal je gaat gebruiken voor je shader. Tijdens deze tutorial kun je dat gewoon op de standaardwaarde laten staan. Geef je shader een naam.
Je zult zien dat er bij beide tabbladen al een code is ingevuld. Dit is een zogenaamde passthrough-shader. Dat betekent dat die shader alleen maar de waarden "doorgeeft" zonder er daadwerkelijk iets mee te doen. Als je een eigen shader wilt maken zijn deze standaardscripts een goed begin. Laat voor nu het script onder Vertex zoals het is en pas het script onder het tabblad Fragment hiernaartoe aan:
//
// Maak alles donkerder.
//
varying vec2 v_vTexcoord;
varying vec4 v_vColour;
void main()
{
gl_FragColor = vec4(0.5, 0.5, 0.5, 1.0) * v_vColour * texture2D(gm_BaseTexture, v_vTexcoord);
}
Later zul je leren hoe dit werkt, maar voor nu hoef je nog niet precies te weten wat dit doet. Druk op het vinkje en sla zo de wijzigingen in je shader op.
We gaan nu de zojuist aangemaakte shader gebruiken. Ga naar je object en geef het een draw-event. We willen de sprite van het object hier tekenen met de zojuist aangemaakte shader. Dat gaat met de volgende code:
shader_set(jeshadernaam);
draw_self();
shader_reset();
Dit werkt een beetje zoals bij surfaces: eerst zeg je dat je voortaan deze shader wilt gebruiken bij alles wat getekend wordt. Dat gaat met de functie shader_set(shader). Daarna ga je daadwerkelijk tekenen. In dit geval tekenen we alleen de sprite van het object met draw_self(). Als laatste stop je het gebruik van de shader. Dit gaat met de functie shader_reset().
Voer nu het project uit. Je ziet nu als het goed is je sprite, maar dan een stuk donkerder. Dus zoiets:

Als je nu plaats daarvan een error ziet, moet je waarschijnlijk je DirectX-runtime updaten via
deze link. Als het dan nog niet werkt, moet je waarschijnlijk je videokaartdrivers updaten. Als het wel werkt is dat mooi.

Dat was je eerste shader! Nu door naar het echte werk.
Een shader schrijvenHierboven heb ik je de code voor een shader gegeven. Maar waarschijnlijk zul je ook zelf wel een shader willen kunnen schrijven. Hiervoor gebruiken we zoals ik al eerder heb gezegd de programmeertaal GLSL ES. Ik zal wat uitleggen over de basis van deze taal. Deze uitleg heb ik verdeeld in een aantal onderdelen, elk in hun subhoofdstukje. Het is waarschijnlijk het beste om dit hoofdstuk eerst een keer door te lezen, en later deze tutorial er weer bij te pakken als je een bepaald onderdeel nodig hebt. Mede om dat beter mogelijk te maken en dit hoofdstuk overzichtelijker te houden, zal ik geen complete voorbeelden van shaders in dit hoofdstuk zetten. In plaats daarvan verwijs ik af en toe naar het hoofdstuk 'voorbeelden' om een voorbeeld van een complete shader te geven, dat je met de kennis uit de hoofdstukjes die je tot dan toe hebt gelezen kunt bekijken en aanpassen.
Als je een onderdeel niet meteen begrijpt is dat niet per se erg. Lees dan gewoon even door en kijk of je het wel begrijpt als je de hoofdstukjes daarna of één van de voorbeelden hebt bekeken. Als je het dan nog niet begrijpt (en het onderdeel echt nodig hebt, je hebt niet al deze dingen nodig voor alle shaders) kun je natuurlijk altijd nog een topic in beginners/gevorderden (afhankelijk van je vraag) openen.
Puntkomma'sIn GML kun je puntkomma's bijna altijd weglaten. In GLSL ES is het gebruik van puntkomma's echter verplicht. Puntkomma's komen op dezelfde plekken als bij GML.
CommentsComments werken net als in GML, dus je kunt comments van één regel als volgt gebruiken:
//Comment
En je kunt multiline-comments als volgt gebruiken:
/*Comment
Meer comment
Nog meer comment*/
Je moet een multiline comment aan het eind van de code afsluiten. Dit kan dus niet:
varying vec2 v_vTexcoord;
varying vec4 v_vColour;
void main()
{
gl_FragColor = v_vColour * texture2D(gm_BaseTexture, v_vTexcoord);
}
/*
Comment
En moet vervangen worden door:
varying vec2 v_vTexcoord;
varying vec4 v_vColour;
void main()
{
gl_FragColor = v_vColour * texture2D(gm_BaseTexture, v_vTexcoord);
}
/*
Comment*/
VariabelenIn GML zijn er twee types variabelen: string en real. Ook hoef je niet expliciet het type aan te geven, je kunt zelfs wisselen tussen types met één variabele. In GLSL ES zijn er veel meer types, en je moet een variabele
declareren voordat je hem kunt gebruiken. Bij het declareren maak je de variabele als het ware aan. Je moet daarbij het type opgeven. Optioneel kun je de variabele meteen een waarde geven. De basisdeclaratie gaat als volgt:
type naam;
Als je gelijk een waarde aan je variabele wilt geven gaat dat als volgt:
type naam=waarde;
Voor 2d-shaders zijn de volgende types het belangrijkst:
floatDit is een kommagetal. Het type werkt ongeveer net zo als een real in GML. Declareren werkt als volgt:
float naam;
Je kunt gelijk een waarde meegeven, als volgt:
float naam = 0.5;
Merk op dat je
altijd een kommagetal (in GameMaker dus een getal met een punt) moet typen als een waarde een float moet zijn:
float naam = 1; //Dit is fout en levert een error op!
float naam = 1.0; //Dit is goed.
Het getal kan uiteraard ook negatief zijn.
intEen geheel getal, oftwel een integer. Dit werkt net zoals bij floats, maar hier kan het getal alleen geheel zijn. Typ dus ook geen punt! Declareren:
int naam;
Als je gelijk een waarde wil geven:
int naam = 100;
boolEen boolean geeft aan of iets waar of onwaar is. Een bool kan dus twee waardes hebben: true of false. Declareren gaat als volgt:
bool naam;
Je kunt ook hier gelijk een waarde meegeven:
bool naam = false;
vec2, vec3, vec4, ivec2, ivec3, ivec4, bvec2, bvec3, bvec4Een
vector bevat 2, 3 of 4
componenten (waarden), en kan gebruikt worden voor speciale berekeningen. Er zijn vector-typen voor floats (vec2, vec3, vec4), integers (ivec2, ivec3, ivec4) en booleans (bvec2, bvec3, bvec4). In GLSL ES worden vooral de float-vectors veel gebruikt, bijvoorbeeld voor het opslaan van en rekenen met kleuren en coördinaten. Declareren gaat als volgt:
vec2 naam;
ivec2 naam;
bvec2 naam;
Je kunt gelijk waardes meegeven. Dit gaat met een zogenaamde
constructor. Dat werkt als volgt:
vec2 naam = vec2(0.1, 0.2);
ivec2 naam = ivec2(1, 2);
bvec2 naam = bvec2(false, true);
Voor vectoren met meer dan twee waarden moet je uiteraard de nummers vervangen en meer waarden opgeven.
Een vector bestaat uit meerdere componenten. Je kunt één of meerdere van deze componenten selecteren. Dat doe je door de naam van de vector te typen, gevolgd door een punt, gevolgd door een of meerdere componenten van de vectoren. Elk component heeft een letter. Je hebt de keuze uit het gebruik van de letters r, g, b en a voor kleuren, x,y en z voor posities, of u en v voor posities op textures. Dus:
kleur.r //Selecteert het eerste component van een kleurvector
kleur.rga //Selecteert het eerste, tweede en vierde component van een kleurvector
positie.y //Selecteert het tweede component van een positievector
positie.xy //Selecteert het eerste en tweede component van een positievector
texturecoordinaat.u //Selecteert het eerste component van een vector die informatie over texturecoordinaten bevat
Overigens kun je deze letters ook voor hele andere soorten vectoren gebruiken, dus bijvoorbeeld de letters r en g voor een vector die informatie over texturecoordinaten bevat. Ik raad dat echter niet aan, omdat het je code minder duidelijk maakt.
Als je het selecteren van vectorcomponenten nog niet helemaal begrijpt is dat niet erg, want in de volgende hoofdstukken volgen voorbeelden die dit waarschijnlijk duidelijker zullen maken.
Naast deze datatypes zijn er nog de datatypes mat2, mat3, mat4, sampler2D en samplerCube. In deze tutorial zal ik deze types echter niet verder uitleggen, met uitzondering van het type sampler2D, dat helemaal aan het einde van deze tutorial nog aan bod zal komen.
Variabelen een waarde gevenJe kunt variabelen natuurlijk ook na de declaratie een waarde geven. Dat gaat net zo als het meegeven bij de declaratie, maar hier geef je het type niet meer op. Dus bijvoorbeeld bij een float:
variabele = 2.0;
Bij de andere types:
variabele = 100; //int
variabele = false; //bool
variabele = vec2(100.0, 200.0); //vec2
Bij een vector-type hoef je niet alle vectorcomponenten tegelijk een waarde te geven. Je kunt ook één of meerdere componenten selecteren, en die een waarde geven:
variabele.u = 100.0; //het u-component van een vec2
Je kunt natuurlijk ook een variabele gelijk maken aan een andere. Als de types van de variabelen hetzelfde zijn is dat heel simpel:
variabele = anderevariabele;
Als de types van de variabelen niet gelijk zijn moet je de variabele achter de = overzetten naar het andere type. Dit gaat als volgt van floats naar integers:
int variabele;
float variabele2 = 2.0;
variabele = int(variabele2);
Voor integers naar floats:
float variabele;
int variabele2 = 2;
variabele = float(variabele2);
Van en naar booleans:
float i = 1.0;
bool j = true;
int k = 1;
i = float(j); //i is nu 1.0
k = int(j); //k is nu 1
j = bool(i);
j = bool(k);
Bij vectoren kun je één component selecteren om die gelijk te maken met een andere variabele. Daarbij gelden dezelfde type-omzettingen als hierboven. Voorbeeld:
vec4 kleur;
float rood = 0.5;
int geel = 1;
bool blauw = false;
kleur.r = rood;
kleur.g = float(geel);
kleur.b = float(blauw);
Je kunt ook meerdere of alle componenten selecteren en de constructor gebruiken:
vec4 kleur;
float rood = 0.5;
int geel = 1;
bool blauw = false;
kleur.rgb = vec3(rood, float(geel), float(blauw)); //3 componenten
kleur = vec4(kleur.r, kleur.g, kleur.b, 0.0); //alle componenten
Belangrijke ingebouwde variabelenIn zowel de vertexshader als in de fragmentshader is er één variabele waarnaar je verplicht moet schrijven. Je hoeft deze echter niet zelf aan te maken.
Bij de vertex shader moet je verplicht schrijven naar de vec4 gl_Position. De eerste twee waarden (x,y) zijn in coördinaten van het spelvenster, waarbij (0.0,0.0) het midden is van het venster, (-1.0,-1.0) de linksonderkant, en (1.0,1.0) de rechtsbovenkant. De laatste twee waarden heb je voor 2d (meestal) niet nodig, deze kun je op 0.0 en 1.0 zetten.
Bij de fragment shader moet je verplicht schrijven naar de vec4 gl_FragColor. Deze vec4 moet de kleur van de pixel waar je naar schrijft bevatten. Deze moet de rood-, groen-, blauw-, en transparantiewaarde van deze kleur bevatten. Deze zijn niet zoals in GML van 0 t/m 255, maar van 0 t/m 1.
GameMaker zal standaard naar deze beide variabelen schrijven met de volgende codes:
vec4 object_space_pos = vec4( in_Position.x, in_Position.y, in_Position.z, 1.0);
gl_Position = gm_Matrices[MATRIX_WORLD_VIEW_PROJECTION] * object_space_pos;
gl_FragColor = v_vColour * texture2D( gm_BaseTexture, v_vTexcoord );
De simpelste manier om een shader te maken is meestal deze regel(s) te laten staan, en de relevante variabele aan te passen nadat deze voor het eerst ingesteld is door de standaardcode van GameMaker. Dan heb je namelijk de normale waarde al (zoals die zou zijn zonder shader) en kun je die veranderen.
Je kunt nu voorbeeld 1 bekijken.RekenenUiteraard kun je ook rekenen met en zonder variabelen. Dit werkt zo ongeveer zoals je dat gewend bent in GML, maar let ook hier op de types! Bijvoorbeeld:
float variabele;
variabele = 1 + 1.0; //Mag niet!
variabele = 1 + 1; //Mag niet!
variabele = 1.0 + 1.0; //Mag!
variabele = float(1) + 1.0; //Mag!
Je kunt gewoon optellen, aftrekken, vermenigvuldigen en delen:
float variabele;
int anderevariabele = 100;
bvec3 laatstevariabele = bvec3(true, false, false);
variabele = 1.5 + 7.0;
variabele += 1.0;
variabele -= 1.09;
variabele = float(anderevariabele) + variabele * 0.5 / (float(laatstevariabele.x) + float(laatstevariabele.y) + 0.1);
variabele *= variabele;
Je kunt een aantal berekeningen doen met vectoren, ook zonder slechts een van de vectorcomponenten te selecteren. We gaan hier uit van de volgende vector:
vec3 eenvector = vec3(0.1, 0.2, 0.3);
Je kunt er een andere vector van dezelfde grootte bij optellen en van aftrekken. Ook kun je de vector met een andere vector van dezelfde grootte vermenigvuldigen of hem erdoor delen:
eenvector += vec3(0.4, 0.5, 0.2); //eenvector bevat nu (0.5,0.7,0.5)
eenvector -= vec3(0.4, 0.5, 0.2); //eenvector bevat nu (0.1,0.2,0.3)
eenvector *= vec3(0.4, 0.5, 0.2); //eenvector bevat nu (0.04,0.1,-0.06)
eenvector /= vec3(0.4,0.5,0.2); //eenvector bevat nu (0.1,0.2,0.3)
Zoals je ziet, gebeurt er hetzelfde als wanneer je de berekeningen afzonderlijk per component had uitgevoerd, dus alsof je het volgende had gedaan:
eenvector.x += 0.4;
eenvector.y += 0.5;
eenvector.z += 0.2;
eenvector.x += 0.4;
eenvector.y -= 0.5;
eenvector.z -= 0.2;
eenvector.x -= 0.4;
eenvector.y *= 0.5;
eenvector.z *= 0.2;
eenvector.x *= 0.4;
eenvector.y /= 0.5;
eenvector.z /= 0.2;
eenvector.x /= 0.4;
Je kunt echter ook een float of int (afhankelijk van het vectortype) optellen bij, aftrekken van, vermenigvuldigen met of delen door een vector. Dan wordt de bewerking uitgevoerd voor alle componenten van de vector. Dat werkt zo:
eenvector += 2.0; //eenvector bevat nu (2.1,2.2,2.3)
eenvector -= 3.0; //eenvector bevat nu (-0.9,-0.8,-0.7)
eenvector *= 4.0; //eenvector bevat nu (-3.6,-3.2,-2.8)
eenvector /= 5.0; //eenvector bevat nu (-0.72,-0.64,-0.56)
Je kunt nu voorbeeld 2 en 3 bekijken.Het if-statement en expressiesHet if statement ziet er zo uit:
if (boolean)
{
//Doe iets
}
//eventueel
else
{
//Doe iets anders
}
Bij boolean kun je een boolean-variabele invullen. Als deze true is, wordt de code uitgevoerd, als deze false is, wordt de code niet uitgevoerd (de code na else wordt dan uitgevoerd, als die er is). Dus bijvoorbeeld:
bool boolean = true;
if (boolean)
{
//Dit wordt uitgevoerd
}
boolean = false;
if (boolean)
{
//Dit wordt niet uitgevoerd
}
else
{
//Dit wordt uitgevoerd
}
Je kunt ook een expressie invullen. Dat kan, omdat daar altijd een boolean uitkomt (true of false). Dit werkt net zoals in GML. Je kunt == gebruiken om te controleren of de twee kanten gelijk zijn (je kunt hier geen enkele = voor gebruiken, anders dan in GameMaker!):
float i = 10.0;
float j = 11.5;
if (i == j)
{
//Dit wordt niet uitgevoerd
}
j = 10.0;
if (i == j)
{
//Dit wordt uitgevoerd
}
Let weer op datatypes!
if (10 == 10.0) //Fout!
if (10 == 11.0) //Fout!
if (10.0 == 10.0) //Goed
if (10.0 == 11.0) //Goed (hoewel de code erna uiteraard niet wordt uitgevoerd)
if (1 == true) //Fout!
if (1 == int(true)) //Goed
if (vec3(10.0) == 10.0) //Fout!
if (vec3(10.0) == vec3(10.0)) //Goed!
Je kunt ook !=, >, <, >= en <= gebruiken, met hetzelfde resultaat als bij GML:
if (gl_FragColor.a != 0.1)
if (gl_FragColor.a > 0.1)
if (gl_FragColor.a < 0.1)
if (gl_FragColor.a >= 0.1)
if (gl_FragColor.a <= 0.1)
Merk op dat je expressies ook kunt opslaan in een boolean, bijvoorbeeld zo:
bool boolean;
boolean = (gl_FragColor.a <= 0.1);
Je kunt in je expressies de ook de logische operatoren && (en), ^^ (exclusieve of) en || (inclusieve of) gebruiken. Dit werkt hetzelfde als in GML. Een paar voorbeelden:
if ((i == j) && (j == k) && (k == l)) //Alle drie de vergelijkingen moet op waar uitkomen
if ((i == j) ^^ (j == k) ^^ (k == l)) //Eén vergelijking moet op waar uitkomen
if ((i == j) || (j == k) || (k == l)) //Eén of meer vergelijkingen moet op waar uitkomen
Let op!Alle processors hebben ondersteuning voor het if-statement. Niet alle videokaarten hebben echter hardware-ondersteuning voor dit statement. Op videokaarten waar if-statements niet direct worden ondersteund kun je nog steeds if-statements gebruiken, wat ook het gewenste effect heeft, maar werkt het een stuk langzamer. Het belangrijkste effect is dat het gebruiken van if-statements als snelheidsverbetering niet altijd werkt! Bijvoorbeeld:
if (gl_FragColor.a != 0.0)
{
//Ingewikkelde code die iets met de kleur doet
}
Bij de bovenstaande code wordt eerst gecontroleerd of er überhaubt iets te zien gaat zijn (of de alpha niet 0 is) voordat er iets ingewikkelds met de kleur gebeurt. Bij normale GML zijn dit soort codes vrij gebruikelijk om te voorkomen dat er teveel code wordt uitgevoerd en zo de code te optimaliseren. Op een grafische kaart kan het echter voorkomen dat alle code alsnog wordt uitgevoerd, maar dan op een manier waardoor er uiteindelijk geen effect is. Dat gaat dan ongeveer zo:
Jouw code:
if (gl_FragColor.a != 0.0)
{
gl_FragColor.r *= 1.5;
gl_FragColor.g += 0.5;
}
Wordt op bepaalde videokaarten zo ongeveer uitgevoerd als:
gl_FragColor.r *= 1.0 + 0.5 * float(gl_FragColor.a != 0.0);
gl_FragColor.g += 0.5 * float(gl_FragColor.a != 0.0);
Samengevat: if-statements hebben wel altijd het gewenste effect, maar op sommige videokaarten niet de gewenste snelheid(sverbetering).
Je kunt nu voorbeeld 4 bekijken.
Je kunt vanaf nu prima zelf een simpele shader maken die iets met kleurtjes of posities aanpast.ConstantenNaast variabelen kun je in shaders ook constanten declareren. Constanten zijn een prima manier om je code duidelijker te maken. Verder zijn ze heel handig als je een bepaald nummer vaker gebruikt, zodat je niet alle keren dat je het nummer gebruikt af hoeft te gaan. Oftewel, het heeft soortgelijke voordelen als in GameMaker. Er is echter niet één of ander constanten-declareer-scherm zoals in GameMaker, maar je declareert ze in je code. Een constant declareren werkt net zoals bij een variabele, maar je zet er const voor. Dus zo:
const float eenconstant = 10.0;
const int eenandereconstant = 11;
Merk op dat je altijd direct een waarde moet geven aan een constant als je hem aanmaakt. Als je dat niet doet krijg je een error. Ook moet de waarde die je de constant geeft constant zijn! De volgende dingen zijn dus fout:
const float eenconstant; //Je moet een constant meteen een waarde meegeven
const float eenandereconstant = gl_FragColor.r; //De waarde die je de constant meegeeft moet constant zijn
Na het declareren van een constant kun je hem gebruiken in berekeningen net als een variabele (hoewel je er natuurlijk niet mee kunt rekenen):
const float eenconstant = 0.5;
gl_FragColor.r *= eenconstant;
Naast "echte" constanten zijn er ook een paar variabelen die als constant dienen in een shader. Dat betekent dat ze buiten de shader zijn aangemaakt, en in de shader bruikbaar zijn als constante. Deze staan standaard bovenaan de code van beide typen shaders. Ik zal deze constanten allemaal langsgaan, eerst voor de vertex shader en dan voor de fragment shader. Als je de standaardcode van GameMaker laat staan, hoef je niet per se te weten wat ze betekenen, maar het is misschien wel leuk om dat te weten en soms ook wel nuttig.
Voor de vertex shader staan de volgende constanten bovenaan je code:
attribute vec3 in_Position; // (x,y,z)
attribute vec4 in_Colour; // (r,g,b,a)
attribute vec2 in_TextureCoord; // (u,v)
//attribute vec3 in_Normal; // (x,y,z) unused in this shader.
in_Position geeft de positie van dit punt, zoals dat in GameMaker is. Dus als je een sprite tekent op (100,200) geeft dit (100.0,200.0,0.0) (de 0.0 is voor de z-waarde, die in 2d altijd 0 is). in_Colour geeft de kleur van dit punt. Dit is niet de kleur van het punt op de texture, maar de kleur van bijvoorbeeld image_blend of één van de hoekkleuren bij draw_sprite_general(). in_TextureCoord geeft de positie op de texture van dit punt. in_Normal is bij 2d-shaders niet van toepassing. Als je het wilt gebruiken moet je het 'uncommenten'.
Bij fragment shader staan er normaal twee constanten boven je code. Deze worden in de vertex shader aangemaakt (zie 'variabelen gebruiken in zowel de vertex als fragment shader' hieronder voor meer informatie over hoe je zelf ook dit soort variabelen kunt gebruiken) en zijn daar variabelen.
varying vec2 v_vTexcoord;
varying vec4 v_vColour;
v_vTexcoord geeft de positie op de texture van de pixel die deze fragment shader momenteel bewerkt. Deze positie is een vec2 met twee waarden (een x en een y) die allebei van 0 t/m 1 gaan, waarbij 0 de linkerkant of bovenkant van de texture is en 1 de rechterkant of onderkant. v_vColour geeft de kleur (van bijvoorbeeld de image_blend, niet per se van de pixel). Deze twee variabelen zijn eigenlijk simpelweg equivalenten voor in_Colour en in_TextureCoord, maar dan in de fragment shader.
Je kunt nu voorbeeld 5a bekijken.Waarden doorgeven aan een shaderTot nu toe hebben we alleen gebruikt gemaakt van de waarden die standaard aan de shader doorgegeven worden. Daar zit bijvoorbeeld de texture bij die gebruikt wordt voor hetgeen dat je tekent en de coördinaten van de pixel. Maar heel vaak zul je ook zelf waarden willen doorgeven aan de shader. Als je bijvoorbeeld een shader maakt die de kleuren lichter maakt, wil je kunnen doorgeven hoe veel lichter de kleuren dan moeten worden, in plaats van dat te hardcoden in de shader. Als je een blur-shader maakt, wil je hetzelfde met de hoeveelheid blur die moet worden toegepast. Gelukkig kun je vrij makkelijk waarden doorgeven aan de shader. Ik zal hiervoor eerst de code behandelen die je in GLSL ES moet gebruiken, en dan de code die je in GameMaker moet gebruiken. Vanaf GameMaker kun je floats, integers en matrixen doorgeven. Het doorgeven van matrixen wordt in deze tutorial niet behandeld, het doorgeven van floats en integers (ook als vector) wel.
Zo'n waarde die je doorgeeft vanaf "het spel" naar de shader heet een uniform. Je declareert hem op eenzelfde manier als een constant, maar dan gebruik je het keyword uniform. Dus zo:
uniform float naam;
uniform int naam;
uniform vec2 naam; //ook voor vec3 en vec4
uniform ivec2 naam; //ook voor ivec3 en ivec4
Een goede plaats om uniforms te declareren is net boven "void main()", dus hier:
varying vec2 v_vTexcoord;
varying vec4 v_vColour;
uniform float naam;
void main()
{
gl_FragColor = v_vColour * texture2D( gm_BaseTexture, v_vTexcoord );
}
Nadat je de waarde op die manier hebt aangemaakt kun je hem
als constant gebruiken in de rest van je code.
We hebben nu de manier behandeld waarop je uniforms gebruikt in GLSL ES. Je moet de waarde echter ook nog in GameMaker doorgeven. Je kunt daarvoor de volgende functies gebruiken (na shader_set, maar voordat je echt gaat tekenen):
shader_set_uniform_f(handle, value1 [, value2, value3, value4]);
shader_set_uniform_i(handle, value1 [, value2, value3, value4]);
Beide functies vragen om een aantal values. Hier geef je de waardes op. Het aantal waardes dat je moet doorgeven hangt af van het type van de uniform. Als de uniform een float of int is moet je één waarde opgeven. Als de uniform van een vectortype moet je evenveel waardes opgeven als het aantal componenten van de vector. Deze functies vragen beide ook nog om een handle. Die moet je eerst nog opvragen met de functie:
shader_get_uniform(shader, uniform);
Deze functie vraagt om het geven van een shader-id en de naam (als string) van een uniform. Het maakt niet uit of de uniform in de vertex shader of fragment shader zit. Dus bijvoorbeeld bij bovenstaande uniform:
shader_get_uniform(shadernaam, "naam");
Het is het beste om deze functie slechts één keer aan te roepen en het resultaat op te slaan, in plaats van deze functie elke keer als je shader_get_uniform gebruikt aan te roepen. Dat is sneller en als je het goed doet overzichtelijker. De ene keer aanroepen kan bijvoorbeeld plaatsvinden in een create, game start of room start event.
Je kunt nu voorbeeld 5b bekijken.FunctiesIn GameMaker worden scripts gebruikt. In GLSL ES heten scripts echter 'functies'. Wat in GameMaker functies zijn, noemen we hier
ingebouwde functies (maar soms ook wel gewoon functies).
Tot nu toe hebben we alle code gezet onder void main(). Dat is een voorbeeld van een niet-ingebouwde functie. Het is verplicht een void main() in al je shaders te hebben. Dit is namelijk de functie die standaard wordt uitgevoerd als de shader wordt uitgevoerd. Vanaf daar kun je andere functies aanroepen als je wilt.
In GLSL ES is dit de basisstructuur van een functie:
returntype functienaam(argumenten)
{
//Je code
return iets;
}
Bij returntype geef je het type op dat teruggegeven wordt door de functie. Bijvoorbeeld float, int of ivec3. Bij argumenten geef je je argumenten op. Dat gaat op de volgende manier:
(argumenttype1 argumentnaam1, argumenttype2 argumentnaam2, etc...)
In de functie kun je de variabelen die je hier hebt gedeclareerd gebruiken. Anders dan bij GML gebruik je dus niet argument0, argument1, argument2, etc... of argument[0], argument[1], argument[2], maar kun je zelf de namen voor de argumenten bepalen, waarbij je ook gelijk de types bepaalt. Dit maakt ook iets "speciaals" mogelijk: function
overloading! Je kunt twee functies met dezelfde naam en een verschillend aantal argumenten of verschillende argumenttypes maken, bijvoorbeeld bij de volgende functie, die zowel een float als een int accepteert en er hetzelfde type als je erin stopt teruggeeft:
int functie(int getal)
{
return getal / 100;
}
float functie(float getal)
{
return getal / 100.0;
}
Als je een returntype opgeeft
moet je functie uiteindelijk iets teruggeven met
return. En wat nou als je een functie wilt maken die niets teruggeeft? Dan gebruik je het speciale type
void. Dat is ook het type wat je gebruikt bij de main-functie, omdat die functie niets teruggeeft. Een functie met void (en geen argumenten) ziet er zo uit:
void functie()
{
//Doe iets
}
Er zijn uiteraard ook ingebouwde functies. Een belangrijke daarvan is texture2D(sampler2D sampler, vec2 coord). Deze geeft een vec4 terug. Deze functie geeft de kleur (als vec4) van een bepaalde pixel in een texture op. GameMaker gebruikt deze functie in de standaardcode:
gl_FragColor = v_vColour * texture2D( gm_BaseTexture, v_vTexcoord );
Hierbij moet je weten dat gm_BaseTexture een constante die GameMaker voor het gebruik in shaders voor je aanmaakt, die de index van de texture van hetgene wat je aan het tekenen bent bevat. Zoals je weet, is v_vTextcoord een constante vec2 die de positie op de texture bevat van de pixel waar deze fragment shader "over gaat" en is v_vColour de kleur die bepaald is door, bijvoorbeeld, de image_blend. Wat bovenstaande code dus eigenlijk doet is de gl_FragColor bepalen door v_vColour te vermenigvuldigen met de kleur in de texture gm_BaseTexture op positie v_vTexcoord. Je kunt de positie (zie voorbeeld 6) en texture (meer over textures doorgeven aan shaders in een later hoofdstuk) echter ook veranderen, wat gebruikt wordt voor veel effecten. Ook kun je uiteraard deze functie zelf gebruiken buiten deze standaardcode van GameMaker.
Andere belangrijke ingebouwde functiesAls er
genType staat kan dat argument of die teruggegeven variabele zowel een float, vec2, vec3 als een vec4 zijn. De
genType moet bij de hele functie hetzelfde zijn. Als er
float/genType staat kun je kiezen of je het genType of een float gebruikt.
angle moet ingevuld worden in radialen.
Hieronder staan in een tabel een aantal wiskundige functies waarvan een GML-equivalent is. Links staat de GLSL ES-variant, rechts staat de GML-variant. Bij de GLSL ES-functies kun je naast een normaal getal (float) een vector invullen. Als je dat doet wordt bij deze functies de berekening voor elk component van de vector apart uitgevoerd.
genType radians (genType degrees) | degtorad(deg) |
genType degrees (genType radians) | radtodeg(rad) |
|
genType sin (genType angle) | sin(val) |
genType cos (genType angle) | cos(val) |
genType tan (genType angle) | tan(val) |
genType asin (genType x) | arcsin(x) |
genType acos (genType x) | arccos(x) |
genType atan (genType y_over_x) | arctan(x) |
genType atan (genType y, genType x) | arctan2(y, x) |
|
genType pow (genType x, genType y) | power(x, n) |
genType exp (genType x) | exp(n) |
genType log (genType x) | logn(2.71828182846, val) |
genType log2 (genType x) | log2(n) |
genType sqrt (genType x) | sqrt(val) |
genType inversesqrt (genType x) | 1/sqrt(val) |
|
genType abs (genType x) | abs(val) |
genType sign (genType x) | sign(n) |
genType floor (genType x) | floor(n) |
genType ceil (genType x) | ceil(n) |
genType fract (genType x) | frac(n) |
genType mod (genType x, float/genType y) | x mod y |
genType min (genType x, float/genType y) | min(x,y) |
genType max (genType x, float/genType y) | max(x,y) |
genType clamp (genType x, float/genType minVal, float/genType maxVal) | clamp(val, min, max) |
genType mix (genType x, genType y, float/genType a) | lerp(a, b, amt) |
De volgende functies werken op vectoren. Ze werken op de hele vector, niet per component. Je moet hier altijd een vector invullen bij genType, niet een float!
float length (genType x) | Geeft de lengte van de vector. |
float distance (genType p0, genType p1) | Geeft de afstand tussen de punten p0 en p1. |
Zie voor meer ingebouwde functies
de GLSL ES specificatie hoofdstuk 8 (let op: Engels en op bepaalde plekken zeer technisch!).
Je kunt nu voorbeeld 6 bekijken.Meer over variabelen en constantenArraysArrays komen in zeer veel talen voor, waaronder GML. Ook de taal GLSL ES heeft arrays. Bij GLSL ES moeten alle elementen uit een array wel hetzelfde type hebben. Ook moet de arraylengte al van te voren vaststaan. Een array declareren werkt zo:
type arraynaam[arraylengte];
Dus bijvoorbeeld:
float lichten[3];
Daarna kun je hem gewoon gebruiken zoals je het in GML ook zou kunnen. Je kunt bijvoorbeeld index 0 van een array als volgt naar 0 veranderen:
arraynaam[0] = 0.0;
Alleen 1d-arrays worden ondersteund. Merk op dat arrays net als in GML bij index 0 beginnen. Als je de array declareert geef je niet de maximum index op, maar het aantal elementen! Als je dus een array met lengte 2 declareert met arraynaam[2] zijn de indices [
0] en [1] beschikbaar, maar de index [2] niet!
Je kunt nu voorbeeld 7 bekijken.Globale en lokale variabelen en constantenTot nu toe hebben we alle variabelen en constanten gedeclareerd in een functie, met uitzondering van uniforms. Je kunt echter andere variabelen en constanten ook buiten functies declareren. Als je dat doet zijn ze globaal, wat betekent dat ze overal in de shader gebruikt kunnen worden. Als je ze in een functie declareert zijn ze lokaal, wat betekent dat ze alleen in die functie gebruikt kunnen worden. Dus:
varying vec2 v_vTexcoord; //Deze constante is globaal
varying vec4 v_vColour; //Deze constante is globaal
float blah; //Deze variabele is globaal
void functie(float nutteloosargument) //nutteloosargument is een lokale variabele
{
return nutteloosargument * 0.2;
}
void main()
{
blah = 0.3;
gl_FragColor = v_vColour * texture2D( gm_BaseTexture, v_vTexcoord );
float nuttelozeuitkomst = functie(blah); //nuttelozeuitkomst is een lokale variable
}
Variabelen gebruiken in zowel de vertex als fragment shaderJe kunt het keyword varying gebruiken om variabelen van de vertex shader naar de fragment shader te zetten. Je kunt daarmee dus een variabele aanmaken en een waarde geven in de vertex shader en daarna gebruiken in de fragment shader. Merk op dat deze variabele in de fragment shader geldt als een constante! Je kunt hem daar dus niet meer aanpassen. Er zijn standaard al een paar varyings gedeclareerd. De beste plaats om je eigen varyings te zetten is waarschijnlijk onder die standaard varyings. Je kunt alleen varyings maken van de float-typen, dus float, vec2, vec3 en vec4. Er zijn veel meer punten waarvoor de vertex shader wordt uitgevoerd dan punten waarvoor de fragment shader wordt uitgevoerd. Daardoor worden varyings geïnterpoleerd tussen de bekende punten. Voor meer informatie over die interpolatie zie
dit stukje.
Je kunt nu voorbeeld 8 bekijken.LoopsGLSL ES bevat drie soorten loops: for, while en do...while. Alle loops moeten minimaal één keer worden uitgevoerd. Er is ook een maximum aantal keer dat loops mogen worden uitgevoerd. Dit verschilt per platform, op mijn computer ligt dat aantal op 255 keer.
De for-loop werkt zo ongeveer hetzelfde als die in GameMaker. De syntax is als volgt:
for (for-init-statement; condition; expression)
{
statement
}
Laten we een loop van GML omzetten naar GLSL ES. De loop in GML die we willen omzetten is de volgende:
for (var i=0; i < 10; i++)
{
//Doe iets.
}
Diezelfde loop in GLSL ES wordt dan:
for (int i = 0; i < 10; i++) //i++ mag in GLSL ES. Je zou hier ook i+=1 kunnen invullen.
{
//Doe iets
}
De volgende loop is de while loop. Deze heeft de volgende syntax:
while (condition)
{
//Doe iets
}
Weer een while loop in zowel GML als GLSL ES:
while (variable < 100)
{
//Doe iets
}
while (variabele < 100)
{
//Doe iets
}
Valt je iets op?

De laatste loop die bestaat, is de do...while loop. Deze loop is exact gelijk aan de while loop qua werking (aangezien loops toch niet 0 keer mogen worden uitgevoerd). Syntax:
do
{
//Doe iets
}
while (expression);
Ook hier weer een vergelijking tussen GML en GLSL ES:
do
{
//Doe iets
}
until (variabele > 100)
do
{
//Doe iets
}
while (variabele <= 100);
Let op! Je mag alleen for-loops gebruiken in HTML5 WebGL (en mogelijk bepaalde andere (mobiele?) apparaten) en alleen die for-loops waarbij het aantal keren dat de loop uitgevoerd wordt van tevoren duidelijk en bekend is. Daartoe moet de for loop de volgende sytax hebben:
for ([float of int] var; var [>>= < <= == of !=] constante; var[++ -- += constante of -= constante])
{
//Doe iets
}
Deze 'regel' is bedacht om te voorkomen dat je shader de grafische kaart kan laten vastlopen, wat een leuk trucje zou kunnen zijn voor mensen met kwaadaardige/"grappige" bedoelingen. Je shader zal dus ook niet worden uitgevoerd als de for-loop te vaak uitgevoerd zou moeten worden.
Je kunt nu voorbeeld 9 bekijken.Textures doorgeven van GameMaker naar GLSL ESTot nu toe hebben we alleen gewerkt met de standaard doorgegeven texture. Dat is de texture van het getekende object. Je kunt echter ook andere textures doorgeven aan GLSL ES, zodat je bijvoorbeeld twee verschillende surfaces kunt gebruiken in de shader, wat perfect is voor het maken van overgangen. Maar er zijn uiteraard ook veel andere dingen mogelijk als je twee of meer textures gebruikt.
Om de texture door te geven heb je weer GLSL ES-code en GML nodig. Bij de GLSL ES code hebben we een "nieuw" type variabele nodig: sampler2D. Dit type variabele bevat eigenlijk de index/handle van een texture. Je kunt variabelen/constanten van dit type dan ook gebruiken in functies als texture2D. Je declareert een uniform van dit type als volgt:
uniform sampler2D othertexture;
Hierna is het gebruik hetzelfde als de ingebouwde sampler2D gm_BaseTexture.
Nu de GML. Er zijn twee functies nodig om een texture door te geven aan de shader. Deze functies lijken een beetje op de functies die je gebruikt om een uniform door te geven. Je moet namelijk weer eerst de handle ophalen van de uniform, en dan de texture doorgeven. Het ophalen van de handle gaat zo:
handle = shader_get_sampler_index(shader, uniform);
Daarna kun je als volgt een texture doorgeven:
texture_set_stage(handle, texture);
Let op: Je mag maar een bepaald aantal textures doorgeven aan de shader. Het exacte aantal verschilt per platform. Voor goedkope telefoons zal dit maximum bijvoorbeeld twee zijn, voor computers zal dit maximum waarschijnlijk op acht liggen. Let er wel op dat GameMaker standaard al één texture doorgeeft (namelijk de texture van hetgene dat je aan het tekenen was), dus op goedkope telefoons zul je maar één extra texture kunnen doorgeven!
Je kunt nu voorbeeld 10 bekijken.Overige dingetjesIn dit hoofdstuk zal ik nog een paar kleine elementen van GLSL ES uitleggen die misschien handig kunnen zijn bij het maken van shaders.
discardIn de fragment shader kun je pixels "weggooien" die je niet nodig hebt. Als je dat doet, wordt die pixel niet getekend. De rest van de code wordt ook niet uitgevoerd. Syntax:
discard;
continue en breakDeze twee statements bestaan ook in GML, en hebben daar dezelfde werking. Ze kunnen beide alleen gebruikt worden in loops. Syntax:
continue;
break;