Welcome back. Last time we did a quick introduction to Game Maker 3D using Game Maker Studio 2. Before we can continue to create 3D objects and the rest of the stuffs, we need to discuss about shaders.
(If you missed part 1 of the tutorials, you can read it here)
First of all shaders are a whole topic on their own. So this will be just an overview of how shaders work and what sort of things we may encounter when working with them using GMS2
By default, GMS2 creates a shader using a language very similar to GLSL ES. You can follow the official specifications to get a whole overview of the language.
You can also change the language to GLSL and HLSL 11 If you are targeting a specific platform. Because GLSL ES is supported by all platforms and it is also the default selection by GMS, this is the language that we are going to use.
Types of variables
GLSL ES is a strongly-typed language, this means that all the variables must have a type that specifies what kind of data it can handle.
There are several types and modifiers you can apply to it. This is a list of the basic types, but please refer to the official documentation for the full explanation.
To put it in simple words, there are just a few types that you need to think about:
- Your basic variables from all languages:
- bool – for conditionals.
- int – for integer numbers.
- float – for numbers with a floating-point.
- Vectors – they can be of different dimensions:
- 2 dimentional (x,y)
- 3 dimentional (x,y,z)
- 4 dimentional (x,y,z,w)
- They can also have different accesors for when you are dealing with different types of data.
- The general use accesors are: (x,y,z,w).
- If you are dealing with colors you can use: (r,g,b,a).
- If you are dealing with texture data then: (s,t,p,q).
- so basically x==r==s and y==g==t and so on.
- They can be of different types depending on the first letter:
- vec is used for float vectors.
- ivec is used for integer vectors.
- bvec is used for boolean vectors.
- vec is used for float vectors.
- Matrices – The basic usage is to represent things like transformations and projections. They can also have many dimensions.
- Samplers – Used for texture data. They can also have many dimensions.
To get a full overview of how you can use vectors and matrices you can see a tutorial in lineal algebra. This tutorial from wolfire explains how to use lineal algebra for game development: Link to his blog
Vertex and Fragment Shaders
First, there are 2 types of scripts that we have to work on for every shader.
The vertex shader has 2 main tasks:
- Set the position of the geometry to be displayed in the screen. This is achieved by multiplying the geometry data with the object translation and camera projection (we’ll look at this in the next tutorial). The result must be put in gl_Position
- Pass data to the fragment shader. This data is interpolated between vertices. For example: if one vertex is red and the one next to is green, then it will be interpolate between red and green having yellow in the middle (which is why our triangle in the last tutorial displays many colors by only having red, green and blue assigned to the vertices).
The fragment shader has only one task and that is to say which color should a pixel in the screen be, this is done by combining different techniques such as: reading texture data, calculating lightning and shadow information and putting it into the gl_FragColor.
We can only work with attributes in the vertex shader.
They represent the data from the vertices of the geometry. For example, the vertices position, colors, texture coordinates, joints weights, indices, among others. It can be used in creative ways to send different data to the GPU but those are the usual uses.
The trick is that you can only handle one vertex per operation. So for example, in our example from the previous tutorial we drew a triangle. It had 3 vertices but in the vertex shader we can only handle one vertex and we don’t have any sort of access or information about the other vertices (or even what vertex are we working in).
The reason for this is that the GPU needs to work on as many vertices in parallel as possible to complete the operation.
Attributes In Game Maker
Usually when you work with shaders you can create as many attributes as you want and you can name them however you want. In Game Maker instead we only have a bunch of attributes created and we cannot change their name.
This are the basic ones you can use (You can extend them to have more with a bit of trickery, but for now this would be enough).
In order of lines they are for the vertices positions, the normals (for lightning calculations), colors (per vertex) and texture coordinates. We’ll work with them in the next tutorials.
Passing attributes in GMS2
The way we pass the attributes from Game Maker to the GPU is through a vertex format and a primitive. We already saw this in the previous tutorial:
Defining a Vertex Format
First we need to define which attributes we are going to use. In the example above we said that we want to use vertex positions (in_Position) by calling the method vertex_format_add_position_3d() and vertex colors (in_Colour) by calling the method vertex_add_color(). We store the result in a variable to work later with it (global.VERTEX_FORMAT).
Defining a Primitive
We’ll talk more about building primitives in it’s respective tutorial. But for now, the only thing you need to know is that when you build a primitive you have to follow the same order in which you specified the vertex format. You can see that first I pass a position vertex_position_3d and then I pass a color vertex_color and I always do it in that order.
You can see the constant use of the variable buffer this is the variable that contains the primitive.
Passing the Primitive to the Shader
To draw our primitive we only need to call the method vertex_submit and pass the variable that contains our primitive.
The next one to talk about are uniforms. We didn’t see them in our last example, but they are used to pass single data to the GPU. So for example, let’s say you want to color a character depending on how much health he has. If it’s full, then it displays the character with his normal colors, but if it’s almost dead then you may want to display him with a red color. You can pass a color through a uniform that specifies this level of damage.
uniform vec4 u_vHurt;
The uniforms are visible in both shaders, there is no limit on how you want to use them, as it is basically a variable you put a value in. The main usages that we are going to see however are: pass the projection and transformation matrices (We’ll do this in the next tutorial) and pass the image to texture our objects (we’ll do this in a later tutorial).
Passing Uniforms in GMS2
First you need to get the address of the uniform. This is done by a simple call in GML:
Here we are obtaining the uniform u_vHurt that we defined in the shader shd3D and we are storing it’s address in the variable u_vHurt (It’s a good idea to name this variable the same as it is in your shader).
We only have to do this once, so we usually put this in a script at the beginning of the game (in our case it would be the script scrInitSystem).
Obtaining samplers (The uniforms for the texture) Is done different. But we don’t have to worry about it unless we are working with more than 1 texture.
Sending the Data to the Shader
Now that we have our uniform in a variable we need to send the data. Depending on what type of data we want to send, we’ll have to use a different function:
- shader_set_uniform_f: This one is used to send floats into a uniform: shader_set_uniform_f(u_vHurt, hurtRed, hurtGreen, hurtBlue, hurtAlpha);
- There is a variation called shader_set_uniform_f_array which is used to send an array of floats instead of the values themselves: shader_set_uniform_f_array(u_vHurt, hurtColor);
- shader_set_uniform_i: The same behavior than the previous one but you can only send integer values with this. (You can also use the shader_set_uniform_i_array variation to send a array of values).
- shader_set_uniform_matrix_array: This one is used to send a matrix through a array: shader_set_uniform_matrix_array(u_mProjection, projectionArray); It also has a version without the _array part but it has a different behavior left for the constants of GMS2, we are not going to use that version though.
The last thing we need to talk about are the varyings.
They are defined in the shaders themselves so you don’t pass data directly to it.
They need to be included in both the vertex and fragment shaders. And must be called exactly the same with the same type as well.
They are used to interpolate values between vertices (usually passed through an attribute). Reviewing our example of the triangle again. We have 3 vertices, and each vertex has a color and a position assigned to it. We used a varying to interpolate the colors. That’s why it changes from one vertex being red, going slowly to yellow and finally getting green in the next vertex
Passing a Varying from the Vertex to the Fragment shader
First you can see we defined the varying vec4 v_Colour; Then inside the main method. In line 11, we are defining which value is this vertex going to have (one is red, the other is green, the final one is blue) by assigning the varying (v_Colour) with the value obtained from the attribute (in_Colour).
Receiving the Varying in the Fragment Shader
In the first line you can see the same definition of the varying vec4 v_Colour; Same type, same name.
In this step the variable is already interpolated. We may have a red value, a yellow, a green or a gray.
The final step is in line 5 where we output that color to the screen (gl_FragColor).
There is still a lot to be written about shaders. As I said at the beginning they are a full topic on their own. But with this little info we can start sending data to the GPU and have all sort of interesting effects going on.
This is going to be all for this tutorial. In the next one we are going to talk about the projection and transformation matrix.
Thanks for stopping by.