Hey! In this guide, you’ll learn the basics of shaders in GameMaker. So let’s get started!
Shaders use OpenGL which is independent of GameMaker and GML, so the shader code should work with all GameMaker versions that support shaders.
Parts of a Shader
There are two parts in a shader: the vertex shader, and the fragment shader. For a start, the fragment shader is the only part that you need to learn – so we’ll leave the vertex shader as is.
So in this tutorial, whenever I mention “shaders”, I am essentially talking about the fragment shader.
What shaders do
A sprite is stored in texture pages as a texture. When a sprite is drawn, its texture from the texture page is drawn on the screen.
When you use a shader to draw a sprite, you basically control the process of getting the sprite data from the texture page.
A fragment shader works with a single pixel. If you write a shader, you write it to manipulate a single pixel (its position, color, etc.). That shader then runs for each pixel. This way, it manipulates the whole texture: pixel-by-pixel.
A surface is a texture too. To a shader, it is the same as a sprite. The main difference is that while a texture page may have multiple sprites/sub-images, a surface has a separate texture page to itself.
Using a Shader
Using a shader is simple: You use shader_set(shader), draw what you want to, and then use shader_reset().
Basic Fragment Shader
If you create a shader, its vertex (vsh) and fragment (fsh) shaders will open. If you open the fragment shader, you’ll see something like this:
This is a simple shader that draws the sprite texture normally.
Area A: The area above void main() is where you initialize all the variables and vectors.
Area B: The area inside void main() is where you manipulate the pixel data and pass a color to be drawn.
varying signifies that the variable being declared is getting its value from the vertex shader.
A float is a variable holding numerical data that can have decimals.
An int is a variable holding only integer values.
vec2, vec3 and vec4 are 2D, 3D and 4D vectors, respectively. They are nothing complicated: They’re just groups of floats. So a vec2 contains two floats, a vec3 contains three, and so on.
v_vTexcoord is a 2D vector which contains the (x and y) position of the pixel on the texture page. It lies in the range of 0-1.
v_vColour is a 4D vector which contains the blend color data for the sprite. It’s the color set through image_blend or draw_sprite_ext()‘s “colour” argument.
It might look like these are empty variables being initialized, but they are actually getting their values from the vertex shader.
gl_FragColor is a 4D vector that is the final output of the shader for a pixel. It has four values: (R, G, B and A), all in the range of 0-1. They stand for red, green, blue and alpha, respectively.
texture2D() gets the colour of a pixel from a texture. Its first argument is the texture (which here is gm_BaseTexture, the texture of the sprite being drawn) and its second argument is the position (which here is v_vTexcoord, the position of the pixel being drawn).
As told above, v_vColour is the image blend colour. It will be multiplied with the pixel colour to get the final colour. Normally, if an image blend colour is not set, there would be no effect.
Setting this 4D vector will change the colour of a sprite.
Its components can be separately accessed or edited by using r, g, b, a after its name and a point. So, if you wanted to change the red value of the pixel, you would do this:
gl_FragColor.r = 1.0;
This will set the red value of the color to 1.
gl_FragColor.a = 0.5;
This will set the alpha to 0.5, making the sprite half transparent.
You can also access multiple components:
gl_FragColor.rgb = vec3(0.0, 0.0, 0.0);
The vec3() function will return a 3D vector with the values inside its parentheses. Here, we’re setting the red, green and blue values to zero, so it would result in a black colour.
When dealing with floats, it's important to use decimal numbers. So, instead of typing "1", we type "1.0".
You can manipulate this 2D vector to change the pixel position.
If the current position is, say, (0.4, 0.4), and you add 0.1 to it, making it (0.5, 0.5), it will get that pixel from the texture, instead of the pixel at (0.4, 0.4). So by changing this vector, it is not the final drawing position of the pixel that is changed, but the original position of the pixel to be used from the texture.
Uniforms are variables that are passed from the object drawing the sprite. This way a dynamic value can be passed to the shader.
As an example, I will make an uniform and pass the current_time value to it.
To create an uniform, you initialize it in Area A:
Here I’ve initialized a float uniform called “time”.
Then you get its ID in a Create event, which can be used later to pass a value to it:
Then you pass a value to the uniform after you set the shader:
This way, the shader will be able to use any value you pass.
Now that you know how shaders work, check out my tutorial about making a water shader: