Fun with Shaders: An Introductory Look at Godot Engine’s Shading Language

In the field of Game Development, shaders are something that I’ve tended to shy away from. Usually I just get by with good old amateur programmer art and never bothered to learn how to use them, mostly due to lack of knowledge. What can they do? When should I use them? Why am I stuck using OpenGL2.1 on ancient hardware when learning the new OpenGL pipelines would be a lot less cruel? Well, times change and so does the computer hardware I use, so there’s no better time to get off my ass and get a good handle on shaders.

Getting Started

So, the first thing we need to understand is that a shader is a program that takes an input (vertices, images, time, ect) and produces an output. This output can be a 2D image, or even 3D geometry. Simple right?

To give a quick example, open up Godot (preferably 3.0) and create a Sprite node and give it a Texture. Find “CanvasItem” in the Sprite’s properties and add a new material shader. Once a material shader is added you can then assign a shader file.

The bottom pane will now display the Shader tab. Much like writing scripts for nodes, you can write shaders and apply them to the Shader property under CanvasItem in the Inspector. Unlike Godot Script, shaders are written in Godot Engine’s in-house shading language. With that in mind, it’s a good idea to take a look at the documentation, or at least have a tab open for reference. Good? Let’s get started.

The first line of any shader script is for specifying the shader type, of which there are three:

  • spatial- A shader type for 3D rendering.
  • canvas_item – A shader type for 2D rendering.
  • particles – A shader for particle effects.

There can be only one shader type for a shader, and the shader type determines what built-in functions you can use, among other things. In this example we’re working on a sprite, so we’ll use the canvas_item type. After specifying the shader type, we can then choose to specify several render modes with the render_mode keyword. You can find a very short list of available render modes for canvas shaders here. blend_mix is the default render mode, but we’ll put it in just to be complete.

Next we’ll take a look at processor functions. Processor functions are functions where the bulk of our shader work takes place. There are three processor functions, and they each apply shaders differently:

  • vertex() – As the name implies, the vertex processor functions runs on the vertices of a 2D or 3D object.
  • fragment() – Works on the pixels in between vertices. For 2D shaders, we’ll be focusing on this one the most.
  • light() – Similar to fragment as it affects every pixel, but it also takes into account every light source that hits an object.

All the processor functions are similar in that they all require a void return type. 

Fragment Shader

Let’s work with the fragment processor first. The simplest shading we can do is to take a color, a vector with four fields (red, blue, green, and alpha), and assign it to COLOR. Now COLOR is a built-in variable that represents our output. When this shader is used, our cyan-colored vec4 is written to every pixel of our sprite. Since shaders run on a GPU which have many cores, every pixel of our sprite is written to in parallel, as opposed to a serial manner like CPUs. This means that shaders can preform large-scale operations on graphics way faster than a typical CPU. Anyway, what if we want to keep the texture that was on our sprite?

Thankfully, keeping the texture of our sprite only takes an extra line of work. In the code snippet below we use the built-in texture method to write the texture data of our sprite to COLOR. Then in the second line we change COLOR by multiplying a vector with itself. Easy, right? The texture method requires only a texture, and the on-screen coordinates of that texture (UV).

COLOR = texture(TEXTURE,UV);
COLOR.rgba = COLOR.rgba * vec4(1,0,0,1);

Vertex Shader

Now let’s take a look at a simple vertex shader.

The vertex function is a pretty simple one-liner. We increment the coordinates of our texture, VERTEX.xy, with a built-in sine function and TIME. And like a good ol’ sine wave should, it goes back and forth.

We can also write our own functions. Here we have a gradient function that colors our texture based on it’s coordinates in the viewport.

shader_type canvas_item;
render_mode blend_mix;

vec4 gradient(vec2 fragcoord) {
	vec2 grad = fragcoord.xy/vec2(1024.0,600.0);
	return vec4(grad.x,grad.y,0.0,1.0);
}

void fragment() {
	COLOR = gradient(FRAGCOORD.xy);
}

We start by taking the coordinates of our fragment and normalizing it by the size of the viewport, which for the default Godot 2D scene is 1024×600 (Surprisingly, there’s no built-in variable available for getting the viewport size in the canvas_item shader type. We can pass variables that aren’t built-in to our shader scripts, we’ll get to that). This operation gives us a vec2 which we can use to create a gradient.

Try putting grad.x or grad.y into a different order in the vec4 to get different colors. I manually resized our sprite to match the size of the viewport. If you don’t resize it you’ll get an unimpressive image.

Creating Interactive Shaders

A nice feature of the Godot Engine is that we can pass in variables from Godot Scripts into our Shader Scripts. In the example below, the gdscript sets two values in our shader script with the set_shader_param() method. We can also use get_shader_param(), which we can use to retrieve uniform values.

Or, like in our gradient example above, we can get the size of the viewport without hard-coding it in our script, like so:

Make sure to take advantage of Godot’s built in help.

I’ve also made it so that the sprite’s texture scales with the viewport for a little bit of interactivity.

Yours should look better. I just limited the colors because I wanted a smaller gif. If I ever get webm encoding/ffmpeg set up on Windows I’m sure I can get this down to <1MB.

The set_shader_param() function is an incredible tool when it comes to getting outside information to shader scripts, and can add a good deal of functionality for games.