shaders tutorial for people too lazy to actually learn opengl, let's do this.
im gonna use no caps except when necessary because my hands are cold and i'm a lazy ass, ok? don't judge. i'm trying my hardest. a final notice, every time i say "(i think)" that means i'm legitimately making a guess and that it's a likely possibility that what i said isn't fucking true and that problems are going to spew everywhere and make a mess. notify me about any problems immediately, ok? thanks buddy.
onto the tutorial.
basically, opengl is its own language (you probably figured that out by now.) it's a crazy world full of variable types and syntax errors, and personally it gives me nightmares. i'll walk you through this fair and square so you don't have to, ok?
variable types
getting started. there's lots of variable types which you need to understand in order to do things correctly. there's 2, 3, and 4-value vectors, which are defined by "vec2", "vec3", and "vec4". they are defined like "vec2 vectorname = (value1, value2)". there's also a lot of different number variables, which personally i hate their existence with all my guts. you've got "int" which is an integer, "bool" which is a true/false value, and "float" which is a real number.
syntax errors
next, there's syntax errors, which are literally gigantic pains in the ass. 90% of the time it'll be because of arithmetic operations on two different variable types, and personally variable type-related errors are ridiculous to me. here's an example of what'll be considered a syntax error:
float two = 2;
perfectly normal, right? well, apparently it's fucking wrong, since it reads 2 as an integer. see what i mean? ridiculous. luckily, the fix is easy (and just as ridiculous): change the 2 to a 2.0 to make it a true float value. there's also arithmetic errors such as 3.5 + 2, since it'll read that as an integer plus a float, which is not okay. to fix that and make it work as wanted, you'll have to change that 2 to a 2.0 yet again. so next time you get a compilation error telling you that you can't add an integer to a float, you'll know how to deal with it. also, adding in constants like pi won't work either since those are seen as read only variables. yep, you have to determine the float value yourself (i think). pretty stupid. anyways, just add a decimal and it should work. onto how shaders work.
how shaders work
shaders have two parts, a vertex and fragment shader. the vertex shader is basically texture coordinates and other texture-related things like color. its purpose is to read the texture's properties and pass them on to the fragment shader which then does the drawing. that's about as simple as it gets.
variable prefixes
now, there's going to be some "variable prefixes" as i like to call them, and they seriously are required if you don't want an infuriating syntax error. the first one is no prefix at all, which means it will only be run in the one shader code (either vertex or fragment, whichever one it appears in). then, there's "attribute" which is the information collected from the texture. then, there's "varying" which means it's a value that gets passed from the vertex code to the fragment code. then, there's "uniform" which gets determined from an outside source (in this case, we use gamemaker functions.) here's how you would define one: "uniform float genericvariable;". this currently won't do anything since nothing defined it, but at least the variable exists now. here's what you would do in gamemaker code:
in the draw event:
shader_set(shadername);
asdf = shader_get_uniform(shadername,asdf);
shader_set_uniform_f(asdf,value);
shader_reset();
or, if you were using a vector and not just a float (or integer???) you would do "shader_set_uniform(asdf,value1,value2)" and so on.
got that? alright. shits gonna get nasty.
passthrough vertex shader
we're gonna work on our vertex shader. we'll make some attribute variables, as follows:
attribute vec3 in_Position;
attribute vec4 in_Colour;
attribute vec2 in_TextureCoord;
varying vec2 v_vTexCoord;
varying vec4 v_vColour;
basically, in_Position is the actual position of the thing, in_Colour is red,green,blue,alpha values of the thing, and in_TexCoord is the coordinates of the texture primitive, which ranges from 0 to 1. the varying variables will be passed on to the fragment shader for it to use, and go figure what they mean lmfao.
now, we're gonna do the actual code, and since openGL is a weirdo we're gonna use this wackass code to begin:
void main()
{
}
see what i mean? edgy shit right there. anyways, in the space between those brackets, we'll have to tell the shader to define the position for the fragment shader to draw, and also determine the values of the varying variables for the fragment shader to use.
void main()
{
gl_Position = gm_Matrices[MATRIX_WORLD_VIEW_PROJECTION] * vec4(in_Position, 1.0);
v_vColour = in_Colour;
v_vTexCoord = in_TextureCoord;
}
alright, that was super hard to copypaste so i'll just tell you what all that does from the getgo. basically, the gl_Position variable is for the fragment shader to use to draw all of its shit. we set that to the game's view transformed into the position of the texture. don't try to understand it harder (i think). then, we take those undefined varying variables from earlier and set them in the void main() code so that they get put into the fragment shader as well. speaking of which, that's all done, so let's just paste the whole vertex code and then move onto the fragment:
attribute vec3 in_Position;
attribute vec4 in_Colour;
attribute vec2 in_TextureCoord;
varying vec2 v_vTexCoord;
varying vec4 v_vColour;
void main()
{
gl_Position = gm_Matrices[MATRIX_WORLD_VIEW_PROJECTION] * vec4(in_Position, 1.0);
v_vColour = in_Colour;
v_vTexCoord = in_TextureCoord;
}
passthrough fragment shader
now for the fragment code. take mind-note that this code is going to use the stuff it got from the void main() to do the actual drawing. we're not going to do any special twists or turns yet since you don't understand this completely (i predict). here we go.
varying vec2 v_vTexCoord;
varying vec4 v_vColour;
so what we just did right there was we pulled the variable from the vertex shader. this time, though, it'll already have been defined, so go opengl i guess!!!!!! on to the next part of the code. we're going to pull a void main() as before, but you already get that so i'll just post the whole rest of it here.
void main()
{
gl_FragColor = v_vColour * texture2D( gm_BaseTexture, v_vTexcoord );
}
pretty short, huh? we've already got the gl_Position which it'll use to draw, so now we just need the color set, which just so conveniently is the gl_FragColor variable. it's naturally a vec4 (r,g,b,a), and we set that to the texture's colors * the texture coordinates (primitive coordinates from 0 to 1, NOT the actual positions it will be drawn to lmfao).
so if you use shader_set(shader) and then draw something inside it (be sure to call shader_reset() afterwards) it should pass things right through as if nothing happened.
so all's good in the end, right? nope. we've got a lot fucking more to cover, and shits about to enter a new level of confusing. luckily, i'm still here to un-confuse you. you have the right to sue me if that hasn't held true so far.
example: sinewave shader
we're going to start with a sinewave shader. you know the one. it makes the screen wave around!!! you could do that with surfaces, but let's be honest, that uses a lot of fucking processing power if you make it precise enough, and at that point shaders are our only hope. luckily, we don't even need to change the vertex code this time.
nah, the fragment code is about to get shat on.
we're going to need some special variables for this, ones of the uniform variant. this means we'll be defining these variables inside of gamemaker and not in the actual shader because shaders are run once when drawing something and then everything resets. if you tried to set some special variable which is meant to increase every time the shader is run, that variable would just get dumped straight the fuck out as soon as the fragment shader did its work. luckily, this problem is fixed by using uniform variables. put all this in the fragment code under those other 2 variables:
float xdist;
float ydist;
float two = 1.0;
float pie = 3.14159265;
uniform float time;
uniform float wave_amount;
uniform float wave_distortion;
uniform float wave_speed;
note that four of those variables don't use the uniform prefix (and are particularly weirdly named). this just means that they'll be used in the fragment code only, and the only reason i included them is to avoid syntax errors. anyways, the uniform variables are as follows: time is the radians of the sine wave, wave_amount is the amount of waves (don't be fooled by its description, it actually takes its value in radians), wave_distortion is used to keep the thing being drawn within reasonable bounds (since it'll go fucking nuts otherwise), and finally wave_speed which is just sort of there to see how fast the wave goes. it's okay to cry at this point, since i cried typing this out. we're even now.
now, we've got our unholy monster of a void main() code. i'm prepared to make this as painless as possible, so just follow along and don't try to question anything too much. ready?
void main()
{
vec2 uv = v_vTexcoord;
xdist = sin(v_vTexcoord.y*(two*pie)+time*wave_speed);
vec2 newPos = vec2(xdist/wave_distortion, 0);
uv += newPos;
gl_FragColor = v_vColour * texture2D(gm_BaseTexture, uv);
}
alright. maybe it was overhyped. explaining this will be sort of easy, actually. basically, we make a one-time vector called "uv" which is just those same primitive texture coordinates. by the way, "primitive" just sort of means triangle in this case. anyways, now we define a new one time variable, xdist, which is (deep breath) our sinewave! it uses the y position of the texture coordinates (from 0 to 1, primitives, remember???) times the wave_amount (therefore the wave amount in radians will give us exactly however many waves we want). we've now got our sinewave screen, and if you want it to wave around like any human would want to, all you have to do now is add time*wave_speed, which is just the offset.
then, we define a temporary vec2, newPos. it's going to be the change in x of the texture coordinates we'll add to the unchanged variable "uv". it's equal to the xdist/wave_distortion, with the y change being 0 (what did you expect?). the reason it is divided by wave distortion is because instead of the texture coordinates being (0,0) to (1,1), it's going to be from (-1,-1) to (2,2) and that's going to make it three times as expansive as the original texture so we'll just divide it by wave_distortion to smooth the difference down a bit. finally, we add the two vectors uv and newPos, and that becomes the texture that is used in our full-drawn texture.
so, basically, you make that shader and put in that code, and then make an object which sets the shader, grabs those uniform variables, and then sets tham to whatever you want (at this point you can set the time variable so it'll wave around). finally, you can draw the screen or an object or whatever the hell you want. you then just reset the shader and this hell is finally over.
end of actual tutorial
we made it pretty far, huh? nah, you probably decided it was too long and scrolled down here, although i commend your willingness to learn if you did reach this point legitimately. regardless, you ain't gonna get anywhere without reading this thing and getting a basic overview, even if you don't count the optional example (the sinewave shader). don't worry, though, i don't blame you if you did skip all that and just looked at the code all confused-like. we all know how painful it is to absorb a whole bunch of knowledge at once and just sort of have it all squeeze back out, so eat this elephant in however many bites you want.
this ends the tutorial. be sure to give proper feedback, and i don't mean complimenting/hating it. tell me for the love of god how useful it was. saying it was not nice isn't specific enough for me to be able to do anything except give a gigantic "ok". finally, please report any problems you had with this as my lazy ass self was too busy typing this to actually test any of what i just said.
also, if i sounded mean to you in any parts, i didn't mean it. i wouldn't have posted this tutorial just for you in the first place if i was trying to be mean. i want to help gamemakers however i can, you know?
finally, sorry about sounding so melodramatic lmfao, i just lost all actual emotional thinking over the course of today. imagine it as my brain being fried. i ain't normally this monotonous-sounding xd
in retrospect, have a nice day and i hope you learned a lot.