Author Topic: Shaders Tutorial: A better one which actually covers everything  (Read 9389 times)

lawatson

  • The Kid
  • Posts: 331
  • I do things and I make things.
  • OS:
  • Windows 7/Server 2008 R2 Windows 7/Server 2008 R2
  • Browser:
  • Chrome 47.0.2526.106 Chrome 47.0.2526.106
    • View Profile
  • Playstyle: Keyboard
Shaders Tutorial: A better one which actually covers everything
« on: January 03, 2016, 09:04:07 AM »
Shader Tutorial: Not as baseless as the previous one

Okay, so I'm back from the depths of typing in no-caps, and I've realized that my previous tutorial doesn't really get anywhere. You'll learn how to make a sinewave shader, but you don't really learn anything. Because of that, I've made this current tutorial. I'll scrape out some better and more confident descriptions of how OpenGL ES works and how you can utilize it in Studio. This time, the incoming wall will actually make sense.

Part 1: OpenGL Overview

OpenGL is a coding language which is meant for drawing things to the screen. It's a lot more controllable compared to Studio and if you ever have a surface effect or such which uses too much processing power, then OpenGL is the way to go.

OpenGL operates based on three things: The environment, which is everything outside of OpenGL's code. In this case, it's Gamemaker Studio and all of the variables you set in GMS.
The next thing is the Vertex shader, which gets the position, color, and texture coordinates (research Primitive Drawing in gamemaker) of what'll be drawn in Studio, and then modifies those and passes them on to the final thing, the Fragment Shader.
In the Fragment Shader, everything is drawn based on the information it got from the Vertex shader. Basically, it gets the texture (defined in OpenGL as gm_baseTexture), then it takes the texture coordinates to draw the shape as planned. Each fragment is a triangle which can get its position or color modified.

TL;DR Of Part 1: OpenGL is separate from gamemaker, it uses the Vertex Shader to get the positions, colors, and texture coordinates of the primitive that is drawn, and then passes it on to the Fragment Shader to draw it to the screen.

Part 2: Operating OpenGL

In OpenGL, you have to define a variable using one of many identifiers: "int" is an integer, "float" is any real number, and finally there are "vec2" "vec3" and "vec4", which are vectors with multiple values which are set in parantheses.

Before you define a variable, you need to also define how it is shared. There are "attribute" variables which are the vertices' information which are passed from GMS to the Vertex shader. There are "varying" variables which are shared with the Vertex and Fragment shaders, and there are "uniform" variables which are set within GMS and shared with the shader. You can put a variable as not being shared, and that will mean it gets used in that part of the shader only and will be dumped out after the code is finished.

After all the variables are defined, the arithmetic and code are done in the "main" section of the code. You would start the code like so:
void main()
{
//insert code here
}

The void means that it does the code but doesn't actually return a value, and the main() means that it executes the function which is what you put inside the brackets. Easy enough to understand.

Syntax errors are when something is wrong with the inputs of a function. Most of the time it will be an arithmetic error between two different variable types, so make sure that when doing arithmetic on two variables, they are the same type. You can convert a value to a new type when doing arithmetic by using float(value) or even vec2(value,value). Make sure to properly place the position of arithmetic functions as well, since a variable being set should only be done once but the Main code should handle something like a multiplication for every value of a texture coordinate, stuff like that.

Part 3: GL Variables

There are some variables which OpenGL will use for drawing. I'll just define them here.

in_Position: An attribute vec3 variable which is the position of the vertices. (x,y,z)
in_Colour: An attribute vec4 variable which is the blend color of the vertices. (r,g,b,a)
in_TextureCoord: An attribute vec2 variable which is the coordinates on the TEXTURE. (u,v). Take note that u,v means horizontal, vertical despite the u.

gl_Position: The vec4 position for where OpenGL puts each vertex. (x,y,z,w). The W value will almost always be set to 1.
gl_FragColor: The vec4 color for the fragment drawn. (r,g,b,a). This will be given some more coverage in Part 4.

Part 4: gl_FragColor Overview

Since gl_FragColor is the color of the fragment that is drawn, obviously it won't have significance to the Vertex shader and we should put it in the main code of the Fragment shader instead.

gl_FragColor is the color of the fragments drawn, but it's hard to understand and control. If gl_FragColor is (1,0,0,1), then OpenGL will not actually draw the texture with all red. It will instead just draw a solid red chunk based on the vertices. Unless your ultimate goal all along was to draw a stupid red chunk, then that's a problem. So, what we'll need is the base texture of what is being drawn. We do this with the texture2D() function which inputs a texture and the texture coordinates, and then outputs a brand new colored texture. Now that we have our colored texture, we can then blend it with any color via vec4 multiplication.

In code, our so-wanted red fragment code would look like this.

gl_FragColor = texture2D( gm_BaseTexture, v_vTexCoord) * vec4(1,0,0,1);

v_vTexCoord is a varying variable which we will cover in Part 5. Anyways, this code effectively gets the colored texture of the primitive and makes it retain its red and alpha only. Job well done.

Part 5: Organizing a passthrough shader

A passthrough shader takes the vertex information and passes it onto the fragment shader, and we get our drawn texture, as simple as that. So, how would we make one?

For the vertex shader, we'll of course need to define the position, color, and texture coordinates of the vertices. Using our GL variables from above, we would put that into code like so:

attribute vec3 in_Position;
attribute vec4 in_Colour;
attribute vec2 in_TextureCoord;

Now that we have the values set, we'll need to share that information with the Fragment shader. We'll do this by creating varying variables (covered in Part 2).

varying vec2 v_vTexCoord;
varying vec4 v_vColour;

Now that we've got all the variables we need, we'll set the varying variables within the Main code.

void main()
{
   gl_Position = gm_Matrices[MATRIX_WORLD_VIEW_PROJECTION] * vec4( in_Position, 1);
   
   v_vColour = in_Colour;
   v_vTexCoord = in_TextureCoord;
}

So, we define the position for the primitive as the Gamemaker view (MATRIX_WORLD_VIEW_PROJECTION) times the vertex's x,y,z and 1 as the w value. This gets us the position for the textures on the screen. Next, we define the varying variables as the attribute variables, and those get passed on to the Fragment shader. Job well done.

Now we will make the Fragment shader. We'll need to obtain those varying variables for drawing the fragment correctly, and then we can use those in the Main code for drawing the texture.

varying vec2 v_vTexcoord;
varying vec4 v_vColour;

void main()
{
    gl_FragColor = v_vColour * texture2D( gm_BaseTexture, v_vTexcoord );
}

The fragment's color is equal to the vertex color (white) times the texture, and that's what gets drawn to the screen. Oh boy! We made our fragment shader.

Part 6: Using shaders in GM Studio

This section will be fairly short. This is just how we draw things with shaders and how we set "uniform" variables.

Setting a shader and drawing in Studio is simple enough. You use shader_set(shader name), draw what you want to draw, and then go back to the regular shader with shader_reset().

shader_set(shadertest);
draw_self();
shader_reset();

Setting a uniform in a shader is done with 2 lines of code: Getting the uniform and setting it.

uniform = shader_get_uniform(shader name, "uniform name);
shader_set_uniform(uniform, value);

Like that, you can set a uniform variable to be used in a shader. Now the main overview is done.

Part 7: Messing around in the Fragment Shader

There are multiple ways you can change how the fragments get drawn, including both position and color. For now, we'll go over how texture coordinates work since it is an important thing to know when drawing something. Texture coordinates are the horizontal and vertical positions on a texture. When a texture is drawn to a primitive, it will get "fitted" onto the primitive for drawing. The horizontal and vertical values will range from 0 (the top or left edge) to 1 (the bottom or right edge). You can get a texture coordinate's y and use that to change the texture coordinate's x like a sine wave, and that will greatly effect the outcome result when you are creating the 2D texture for the fragment color.

Honestly, grab the texture coordinates and do whatever you want to them, try experimenting a bit. Now that you know the basics, you should be able to use the correct variable types, what each GL variable means, how to set shaders and uniforms in a shader, and what each function means. I hope you liked this tutorial.

Be sure to tell me how useful this tutorial was, what you learned, what you did and didn't understand. Thanks again for reading.
« Last Edit: January 03, 2016, 09:08:51 AM by lawatson »
(click to show/hide)

smoke weed everyday