VFX Series: Lesson 0. Your First Godot Shader
This is an introductory lesson for those who never worked with Godot shaders before. It is aimed to demonstrate what shaders are capable of with very little code and why any game developer or visual artist needs them.
Shaders are easy (!). They have a reputation of something complicated only because most people who use them actively are very mathematically inclined, and they tend to explain shaders as mathematical computations. Which is technically true, thought you can say the same about absolutely everything that computer does, because all it does is … well — computation :) We are going to look at shaders as simple scripting tool that allows you to create stunning visual effects limited only by your imagination. I hope this approach lets more and more people, even without programming or math background to use shaders for their creative work, or even their job.
Shaders are small (relatively speaking, they can be very complex, but only if you need very complex effects) programs that get executed on GPU as opposed to regular program code that executes using central processor. Everything that a game draws on your screen is done through shaders. They also can be used for physics calculations, world simulations, or anything, really. Game engines, 3d editing software like Blender, and even applications like Photoshop and Krita all use them, but hide it from the users behind different features usually called “Materials”, “Filters” or “Effects”. Under the hood those are just fancy ways to create shaders without realizing you are doing it.
We are going to be writing shader code from scratch, and unleash their full potential. You can get some inspiration on what shaders can be used for by visiting ShaderToy — an amazing tool to test ideas and share them with others.
There are several different languages to write shader code in (just like with regular programming languages). Godot uses its own language which is compatible with another popular language GLSL. Which is the one ShaderToy uses, so you can take cool things you find online and plug them into your own Godot-based creations. This language is very simple, and feels very familiar to anyone who programmed before in any language.
Let’s create a simple sphere and assign new blank shader material to it. Of course you have to start Godot and create a new project or scene first, but if you are reading this your editor is probably constantly running and you might already have a folder full of started pet projects :). Even if it’s not, it surely soon will be — Godot and Blender are very addictive in their simplicity and power.
After you click on the newly-created Shader an editor will pop in the bottom, you can get your first shader written with a single line added inside your fragment() function:
Let’s break down what is going on here:
shader_type spatial; // This is a shader for an object in 3D space
void fragment() {
// "fragment" means "pixel" in this case
// This is special function name, it gets called
// for EVERY PIXEL on the screen occupied by the object
// This is why higher game resolutions require more powerful video cards
// more pixels -> more times the function is called
ALBEDO = vec3(1.0, 0.0, 0.0);
// ALBEDO is a special variable that means <objects base color>
// vec3 is a built-in data structure to hold 3 numbers.
// It can be used for anything you want, usual usages are:
// Coordinates in 3D space: X, Y, Z
// Colors defined by components: R, G, B
// Our case is color, so it reads:
// "Make base color equal to a color which has 100% red in it"
}
In other words we just said “Make every pixel occupied by this object red”. How come our sphere is partly dark and partly very bright, wouldn’t it make sphere just uniformly red? The answer is simple: our shader did exactly that, but then another shader kicked in and added shading on top of our uniformly red sphere. That’s why shaders are called shaders — their original main purpose is shading objects based on light sources and environment. We can disable automatic shading using special directive:
render_mode unshaded;
Let’s try it out:
Now it’s exactly what one would expect: we see only what we asked for — red color in every pixel.
As mentioned before this program runs for every pixel, and is called “pixel shader”. However there is way more than just color for every pixel, let’s add another important property — transparency.
Now we can see through the sphere. Floor is drawn in the background, then sphere on top of it. If you wonder how system knows to draw the floor behind our sphere, where it wasn’t visible before — it doesn’t. It ALWAYS draws the floor and all the visible objects, and just paints them over each other. This is called overdraw and it means that when you run a huge AAA video game and it makes your GPU almost melt — a lot of this power is wasted to draw pixels you never see. The process of indicating which objects should be drawn and which ones should be scipped is called culling. We will discuss it in future posts
You also might have noticed that sphere shadow disappeared, it’s a side effect of having our sphere transparent — Godot rendering pipeline is optimized in such a way that it draws solid objects first, then transparent ones on top of drawn picture (or behind it like the bottom part of the sphere under the floor which we never gonna see, but GPU still draws it). Only solid objects drop shadows by default.
Now another interesting concept of shaders becomes apparent: for each pixel on the screen multiple shader programs get executed, at least one per object, and some objects have many shaders running for them, and each one produces pixel color information. These multiple pixel colors get combined to produce final pixel we see on the screen, the process that is called blending. And as you probably guessed there is more than one way of blending pixels together. Default one is “mixing” — adding pixel colors proportional to their alpha (transparency) component, so in our case, it’s 50% background, 50% red from the sphere itself. Other modes include “add”, “subtract” and “multiply”. You can see why they are the way they are — those are simple arithmetic operation done on pixel colors. To signify which blending mode we prefer we need to add one of the 4 keywords to render_mode:
render_mode blend_mix;
render_mode blend_add;
render_mode blend_sub;
render_mode blend_mul;
To illustrate how they look and interact let’s put them on 4 different spheres (hanging in the air this time).
They all have their usages, e.g. subtracting image from the white background can be used to create X-Ray effect, multiplying image by a single color creates “lens filter” effect for that color.
Look closely how those modes interact in areas where spheres intersect: spheres 2 and 3 add and then subtract exactly the same amount of red to the background, effectively canceling each other out and revealing the floor behind it completely unchanged. Spheres 3 and 4 give us black area, why? Let’s consider what those two shaders do, first we subtract all the red from the background (floor), then we pass the result through “red filter” multiplication. We say “Show me only red component of the image”. But there is no red component, we just removed it with subtraction. If you apply operations in reverse order — you get similar result, “Show me only red, then remove all of it”.
Blend “mix” and “add” look similar, but the difference is more apparent if we turn shading back on:
Keep in mind that colors are coded by amount of certain component added together, so Black = 0 red, 0 green, 0 blue (vec3(0.0, 0.0, 0.0)). While White is 100% of everything at once: vec3(1.0, 1.0, 1.0). Now we can see what to expect from these two: Adding black to background is essentially X + 0, meaning nothing changes, and we see exactly that on the right — dark shaded area of the sphere doesn’t affect image at all and only red lit part of the sphere get blended in. Left sphere gives darkening of the background, which is what you would expect from making image 50% original and 50% completely black — halfway between black and the floor color.
Now, let’s think about another implication of blending (or using shaders in general). If colors are just arbitrary numbers that we add, multiply and subtract, what will happen if they go beyond what’s expected? What would negative numbers do?
Seems like nothing special happening: -10 or 0 — same result. 1000% red or 100% red — it’s just red. …Or is it?
If you seen a new shiny OLED TV with HDR features and seen some cool HDR content on it, you probably wondered what makes HDR light so awesome? The answer is simple: it can show colors above 100% of brightness. Which means — it’s not enough that your image is red, it can emit red light with various strength, like a red flashlight. Consider red hot peace of metal, it is so hot it starts emitting light, and when it’s very bright it looks white to our eye. There is a way to simulate this effect in Godot (Blender too) with so-called “Tone mapping”. The algorithm we need is “ACES”. Let’s turn it on in WorldEnvironment node:
Whoa! So now we know what happens to those extra-bright colors — they get “hot” and start look like they are glowing. This effect makes light look way more realistic and overall makes image more contrasted adding the eye candy to almost any scene. I use it all the time, just enabling it from the very beginning, making sure everything I create has this already factored in.
We can go one step further and enable WorldEnvironment “Glow”, just to see what happens:
Reminding you — this is just red half a sphere, we haven’t added any extra code, this is how extreme color gets translated into visible range by postprocessing, which by the way is another built-in shader hidden behind UI checkboxes and sliders.
To sum this up we just learned what shaders are — simple programs that run for every pixel on the screen. We tried very simple basic shader, with just one feature of it — pixel color. Soon we are going to discover all kinds of shaders — those that create geometry, particles, fog, work with textures and much more!
I hope I was able to demonstrate how easy it is to write shaders and unleash their power with very little code. We will dive straight into writing more shaders in the following posts.