Posted August 15, 2019 by Papaya Games
#tutorial #shader #fire #godot #light
I know what you're thinking: another fire shader tutorial? While it is true that there are tons of examples online, I think most of them fail at explaning what does the code actually do. So, let's see how this toon, stylized, whatever-you-wanna-call-it fire shader works in Duck in Town!
First, let's see the final product, in a torch inside the amazing Duck Guild of Town:
And some experimenting:
(I know, that gif quality sucks, but you get the idea)
So, let's start!
Create a new MeshInstance, set its mesh to a QuadMesh (if you're not using Godot Engine, just create a 3D plane however it is done on your engine) and let's assign it a new shader!
First, let's set the shader type and render modes:
shader_type spatial; render_mode unshaded, cull_disabled;
The unshaded mode is to make sure our fire isn't affected by other lighting (in some cases you might want that, but we're aiming to that toon style so lighting is usually simple and plain). The culling is also disabled to make sure your plane looks fine from every point in your game.
After setting this, we need to define the uniforms:
uniform sampler2D fire_tex_1 : hint_albedo; uniform sampler2D fire_tex_2 : hint_albedo; uniform sampler2D fire_mask : hint_albedo; uniform vec4 inner_color : hint_color; uniform vec4 outer_color : hint_color;
void vertex() {
MODELVIEW_MATRIX = INV_CAMERA_MATRIX * mat4(CAMERA_MATRIX[0],WORLD_MATRIX[1],vec4(normalize(cross(CAMERA_MATRIX[0].xyz,WORLD_MATRIX[1].xyz)), 0.0),WORLD_MATRIX[3]);
MODELVIEW_MATRIX = MODELVIEW_MATRIX * mat4(vec4(1.0, 0.0, 0.0, 0.0),vec4(0.0, 1.0/length(WORLD_MATRIX[1].xyz), 0.0, 0.0), vec4(0.0, 0.0, 1.0, 0.0),vec4(0.0, 0.0, 0.0 ,1.0));
}
I could explain the meaning of these lines, but some math is required, so let's just copy and paste for now. (You can learn the basics about billboards here).
Now, let's get onto the important part: the fragment shader. This is where magic will happen!
First, let's sample our textures:
vec3 noise_1 = texture(fire_tex_1, vec2(UV.x, UV.y + TIME * 0.7)).rgb;
vec3 noise_2 = texture(fire_tex_2, vec2(UV.x, UV.y + TIME * 1.2)).rgb;
vec3 mask = texture(fire_mask, UV).rgb;
While we are sampling the mask the classic way (just using the mesh''s UVs), for the noise textures we are adding TIME to the UVs' Y coordinate, meaning the texture will pan along this axis. Feel free to change the multiplier for TIME, in case you want it to be slower or faster. If we set the addition of these two noise textures to the ALBEDO (essentially, the final color), (noise_1 + noise_2) you should see something like this:
(Again, sorry for the gif quality)
If you then multiply the mask's value to this addition (so you have (noise_1+noise_2)*mask)) you get something like this:
Looks familiar, right? We are almost there! Now we need to use our colors and to get rid of that weird black. Let's see the code before explaining it (and take your time to understand it before reading the explanation, that's how one learns!)
vec3 col = (noise_1 + noise_2) * mask;
ALBEDO = vec3(step(0.015, col.r));
ALBEDO *= mix(outer_color.rgb, inner_color.rgb, step(0.15, col.r));
ALPHA = step(0.015, col.r);
The col variable just holds the noise and mask, just how we've seen earlier. Next, we have the first ALBEDO value: an step function. What this line does is to return 0 if the col.r value is lesser than 0.015, and 1 otherwise. This hard step is needed for that characteristic stylized look: notice how there are only hard edges. As always, you can tweak these values until you get something cool! Notice how it is inside a vec3 -- that's just because that's the type that ALBEDO expects. Using only one argument means the three components in this vector will have the same value (that's ok for us, we are going to add colors after). Now you have your flame only in white and with an awful black background: let's add some color.
We multiply the previous ALBEDO value by a mix function. What this function does is to mix two vectors (in this case, two colors) depending on a value (the third argument). In this case, we are using an step as a mix-value, meaning it will only return 0 or 1, so the mix will only return one color or the another (so, there isn't any mixing actually happening here). This is a pattern commonly used in shaders, to avoid using if-else structures (GPUs don't like those). The 0.15 in the step is how big the inner color (the more intense part in our fire) will be. Again, tweak this value as you wish.
There's only one thing left: to get rid of that awful black background! This is actually quite easy. Just using the same step value as we used before, we set the alpha to 1 (max opacity) to any pixel whose greyscale value is greater than 0.015, meaning all the black or almost-black parts will be transparent.
Hopefully that explanation was clearer enough? Most shaders use similar patterns, so it is important that you fully understand this. If you have any doubt leave a comment and I'll try to help you!
After writing the shader, you should have your mesh looking like an amazing stylized toon fire, like this one:
Now there's only one thing left: you may have noticed in the gif at the start of the tutorial that there's actual flickering light coming from the fire. To do this, just add an SpotLight node as a child to your mesh! For the flickering, add an AnimationPlayer and make a simple, long anim (10-15 secs) with random flickering changing the light's intensity, and set the animation to autoplay. This is where Godot Engine's node-scene system really shines: you can save this tree as an independent scene and instance it as an individual object wherever you want some cool fire!
Remember to experiment with the shader parameters - try different textures, colors, or step/mix values, you might be surprised how things change just randomly changing params!
Duck in Town - A Rising Knight releases in 2019, we'd love to be on your collections in itch.io and in your wishlists on Steam! For any question, feel free to leave a comment or ask me on Twitter (@_atorresm).