Functions
1 Oct, 2021

Now that we've covered the data types, it's time to learn how to use them! Let's go over operators and functions.

Operators

One of the first things you need to know about is the operators. Thankfully, they are quite similar to GML, so this should quick to cover. All operators must both terms to use the same data type. So for x + y, x and y should both be floats or both be ints, and not mixed data types.

  • Arithmetic operators: Addition (+), subtraction (-), multiplication (*) and division (/). One exception to the data type rule is that matrices and vectors can be multiplied together (as long as they have matching dimensions). Division with integers rounds down to the nearest integer as "div" does in GML.
  • Assignment operators: Increment (++), decrement (--), addition assignment (+=), subtraction assignment (-=), multiplication assignment (*=) and division assignment (/=). Incrementation simply means adding 1 to a variable and decrementation means subtracting 1, so i++ is equivalent to i = i+1.
  • Comparison operators: Equal-to (==), not-equal-to (!=), less-than (<), less-than or equal-to (<=), greater-than (>), greater-than or equal-to(>=). These operators always return a 1-dimensional bool being true if the condition is met, and false otherwise.
  • Condition operators: Logical AND (&&), logical OR (||), logical XOR (^^) and ternary selection (?:). These are all supported in GML as well, but you may know them by "and", "or", and "xor". Ternary selection is like a mini if-statement. In this format: (bool_condition) ? a: b, if bool condition is true, it will return 'a' and if not, it returns 'b'. Helpful with writing concise code.

Functions

Functions are a fundamental part of shaders, a bit like an extension of the operators. The built-in shader functions use inputs, called "parameters" to "return" an output from the function. This can be anything from simple math functions to getting a pixel's color from a texture. Everything beyond operators has to be done with functions.

Built-in functions

Here's a quick overview of all supported functions in GLSL ES (if want any more detail about specific functions, you can click them to view their page):

  • Texture functions:
    texture2D, texture2DProj, texture2DLod, texture2DProjLod
    These are the functions used to get the color and alpha of a pixel on a given sampler. The "Proj" variants are used for projecting a texture on a 3D surface (e.g. for shadow mapping) and the "Lod" variants are currently NOT supported in GM.
  • Common functions:
    floor, ceil, fract, mod, sign, abs, min, max, clamp, mix, step, smoothstep
    You're probably familiar with most of these functions. 'floor', 'ceil', 'sign', 'abs', 'clamp', and 'mix' (called lerp in GML) work exactly the same as they do in GML!
    'fract', called frac in GML, and 'mod' both handle negatives differently, 'min' and 'max' are limited to only two arguments each, leaving 'step' and 'smoothstep' as the only unfamiliar functions. I won't summarize them here so I encourage you to check out those pages.
  • Math functions:
    pow, sqrt, inversesqrt, exp, exp2, log, log2
    'sqrt' works the same as in GML, pow is the equivalent to the GML power, 'inversesqrt' is the reciprocal of the square root, the "exp" functions are exponential functions and the "log" are logarithms with different bases (The Natural Number and 2).
  • Vector functions:
    length, distance, normalize, dot, cross, reflect, refract, faceforward, matrixCompMult
    'length' and 'distance' find the length of a vector and distance between two vectors, 'normalize' divides a vector by its length, 'dot' and 'cross' are for dot and cross products which you may or may not be familiar with. The rest of them are more complicated and so I encourage reading later.
  • Trigonometry functions:
    radians, degrees, sin, cos, tan, asin, acos, atan
    'radians' (degtorad in GML) converts angles from degrees to radians and 'degrees' (radtodeg in GML) converts radians to degrees. 'cos', 'sin', 'tan' function just like in GML, 'acos', 'asin', 'atan' function just like the "arc" equivalents in GML.
  • Boolean functions:
    equal, notEqual, lessThan, lessThanEqual, greaterThan, greaterThanEqual, any, all, not
    'equal', 'notEqual', 'lessThan', 'lessThanEqual', 'greaterThan' and 'greaterThanEqual' all return bools (or bvecs) being true if the condition is met or false if not. `all` returns true if all components of a bvec are true, 'any' returns true if any components of a bvec are true and `not` simply negates all components of a bool or bvec.

Matrices

Now that you have an overview of the operations and functions, it's much easier to explain matrices. So we know from the last tutorial that mat2 contains 4 (2x2) components and that the order of multiplication matters, but now I can show you why. First, let's consider these two cases:

//Remember, a matrix is like an array of vectors rather than a larger vector.
mat2 matrix = mat2(2, 3, 4, 5); //First row is 2 and 3, second row: 4 and 5.

vec2 A = vec2(0, 1) * matrix; //Computed as vec2(3, 5)
vec2 B = matrix * vec2(0, 1); //Computed as vec2(4, 5)
To perform a vector-matrix multiplication, you can generalize with this forumla for each component 'i':
new_vector[i] = vector[0] * matrix[0][i] + ... + vector[n] * matrix[n][i];
So in the example above, we are essentially calculating: A = vec2(0*2 + 1*3, 0*4 + 1*5). If you swap the order putting the matrix first, then you swap the matrix rows for columns and columns for rows and you get this: B = vec2(0*2 + 1*4, 0*3 + 1*5).
To make this a little easier to remember, you can also think of this as a separate dot-product for each component: new_vector[i] = dot(vector, matrix[i]);
Alright, that's enough math for now! Feel free to revisit this if you ever need a refresher!

Logic and loop statements

Logic statements are a crucial part of programming languages and GLSL ES is no exception. You're probably already familiar with these so I'll be brief and just clarify the syntax differences. Here is a list of those statements:

  • if:
    Shaders syntax is more strict than in GML so your if statement must be in this format:
    if (bool_condition) //example: a == b
    {
        do_something();
    }
    Specifically, your condition must be a bool (unlike in GML) and MUST be placed in parentheses. I recommend using curly brackets at the start and end of the conditional code for ease of reading, but it's not required in one-liners.
  • for:
    For statements loop code inside the brackets (or the next line) until a condition is false. They must have at least a condition in this format:
    for(; bool_condition; ) //example: i > 10
    {
        do_something();
    }
    
    But more commonly you'll see something like this:
    for(int i = 0; i<10; i++)
    So you don't have to initialize a variable or increment inside the for-loop, but you must have two semicolons to separate the condition.
  • while:
    While statements loop through the code in the brackets (or the next line) until a condition is false. Make sure an end condition is met or it will loop until it crashes.
    while (bool_condition)
    {
        do_something();
    }
  • do-while:
    Do-while statements loops code inside the brackets just like while, except the condition is checked after the first iteration. This means it will always do at least one loop. This is the standard format:
    do
    {
        do_something();
    }
    while(bool_condition);

Loop control

There are two main ways to stop a loop. Break and continue:
  • break:
    Aborts the loop, skipping everything in the loop after it. Put this at the bottom of your loop if you want the code to execute before aborting!
  • continue:
    Skips the rest of the loop iteration and jumps to the next iteration. Putting this at the end of a loop will have no effect.
'break' is often used when additional iterations are unnecessary and in some cases can speed up performance significantly.

Custom functions

Custom functions are another useful feature for organizing your code. Here's a simple example function:

vec4 tex(vec2 coordinates)
{
    return texture2D(gm_BaseTexture, coordinates);
}
When called, this outputs a (vec4) pixel color from the base-texture at the given texture coordinates. This shows you set the output datatype before the function name and you can add parameters to your functions inside the parentheses.
Along with defining a parameter's datatype, you can also set parameter qualifiers:
  • in:
    This is the default qualifier for parameters when none is specified. It simply inputs a value from called function as expected. It can be thought of as a read-only variable.
  • out:
    This is for an output variable rather than an input. That means you need to set its value inside the function. This can be thought of as a write-only variable.
  • inout:
    This can be used as an input and an output. In other words, it's a read-and-write variable.
Here's an example:
vec4 vignette_tex(in vec2 coordinates, out float vignette)
{
    vec4 tex = texture2D(gm_BaseTexture, coordinates);
    vignette = (1.-length(coordinates-.5));
    return tex*vignette;
}
Here we have an input (texture coordinates) and an output (vignette brightness) along with a vec4 return. Sometimes you need to output multiple values and out is a way to do that!
It's also important to understand variable 'scope' to avoid naming conflicts. Basically, if you define a variable inside brackets (e.g. custom functions, if-statements, or loops), it will only be local to that scope. That means your functions can all use variables with the same name as long as it doesn't conflict with a global variable (outside of functions). It's a lot like global and local variables (being limited to specific instances) in GML.

The main function

The main function is a bit of unusual because it doesn't have any parameters, doesn't return anything, and is the one custom function required in every single shader! Instead of parameters, you just have to work off of the built-in "gl_" and "gm_" variables, attributes (vertex shader), and varyings (fragment shader).

  • Vertex shader:
    In the vertex shader, you must set gl_Position, which is the vertex position in "projection-space". More on that later, but for now, you just need to know that it's the vertex position, usually transformed by the gm_Matrices[MATRIX_WORLD_VIEW_PROJECTION]. Here you can set the vertex position however like, for example, if you want to flip the y-axis, you can do this:
    //Vertex position with an inverted y-axis.
    vec4 inverted_pos = vec4(in_Position.x, -in_Position.y, in_Position.z, 1.0);
    //Set the vertex position.
    gl_Position = gm_Matrices[MATRIX_WORLD_VIEW_PROJECTION] * inverted_pos;
    
    You may have noticed the "inverted_pos" is a vec4. The W component is used with the transformation matrices and should always be 1.0. This will make sense in the next tutorial so don't worry about it for now.
    The main function is also where you set the values for 'varying' variables. Most shaders pass the color and texture coordinate values from the attributes to the fragment shader via varyings. Some shaders also pass the vertex position or normals, but you can pass anything you want. The general rule is, if it can be computed in the vertex shader, do that because it's faster to do one calculation per-triangle than one per-pixel!
  • Fragment shader:
    In the fragment shader, you must set gl_FragColor, which determines the output fragment/pixel color. For example, the default shader simply outputs the texture color and alpha.
    gl_FragColor's channels are "normalized", meaning the acceptable range is 0 to 1 (unlike GML's 0-255). This is because the shader doesn't "know" how bits are needed for each channel. Currently, GM only supports 8-bit (0-255) RGBA textures, but perhaps someday we'll get other options.
    Another useful statement is called "discard". When called, it will completely skip rendering the fragment; used for alpha testing, and more. Here's an example:
    vec4 tex = texture2D(gm_BaseTexture, v_vTexcoord);
    //Discard all fragments that are less than 50% alpha.
    if (tex.a<.5) discard;
    
    Note: You should avoid using discard if it's possible to do so because it is very slow on some platforms.
    And that pretty much covers the fragment shader!

Summary

That was a lot to cover so don't worry if you don't understand some parts! The purpose of these tutorials is to help provide a solid foundation that you can refer to when you need to. If you have a decent grasp of operations and functions, you can begin experimenting. As you become more comfortable with shaders, you'll learn how to write code more efficiently and clearly.
At this point you have all you need to begin writing your own shaders, however the next tutorial will explain tips and tricks I've learned to help you launch into it! Congrats, you've almost completed!