Sci fi engine thruster VFX shader for Godot 4. Jet engine exhaust flame/propulsion.
Breakdown
- Initially I made a gradient on object, making one end of object transparent and other end opaque.
- Then I applied noise texture, thus making the transition a bit rough.
- Then I animated the UVs and modified the intensity of colors at different levels.
- Finally, I applied a Fresnel effect on edges so they don’t look sharp.
We will start with the simple effect, and will add details to it with time.
Start with Simple Gradient
The first step is to make a gradient spanning across the mesh. One end of the mesh should be transparent (its ALPHA = 0.0) and other end must have ALPHA value 1.0.
This is how it looks:
Code:
uniform float model_height = 3.0;
uniform float dissolve_start : hint_range(0.0, 1.0) = 0.001;
uniform float dissolve_length : hint_range(0.0, 1.0) = 1.0;
uniform float gradient_bias : hint_range(0.1, 5.0) = 1.0;
varying float vert_height;
varying vec2 v_uv;
void vertex() {
vert_height = (VERTEX.y + (model_height / 2.0)) / model_height;
v_uv = UV;
}
void fragment(){
float gradient_height = vert_height - dissolve_start;
gradient_height *= 1.0 / dissolve_length;
gradient_height = clamp(pow(gradient_height, gradient_bias), 0.0, 1.0);
}
ALPHA = mix(1.0, 0.0, gradient_height);
ALBEDO = vec3(1.0 - gradient_height);
}
The vertex shader calculates vert_height
, which is the vertical position of each vertex normalized between 0 and 1. The calculation vert_height = (VERTEX.y + (model_height / 2.0)) / model_height;
ensures that the bottom vertex is at 0 and the top vertex is at 1.
In fragment shader, gradient_height
is computed by taking the vert_height
, subtracting dissolve_start
(to begin the effect partway up the model), and then dividing by dissolve_length
to scale the effect over the desired portion of the model.
gradient_height
is then modified using pow(gradient_height, gradient_bias)
to apply a bias to the gradient, which adjusts how sharply or smoothly the dissolve happens. Finally, these values are assigned to visualize the effect.
Adding noise to make it interesting
The above jet engine like effect is sufficient for some games, but I needed something more interesting, so I added the contribution of noise to make it more detailed. It is simple, just add a noise sampler2D and add its value to gradient to distort the gradient.
// ADD THESE UNIFORMS:
uniform float noise_speed = 1.0; // Speed of noise movement
uniform float noise_strength = 0.1; // Strength of the noise effect
uniform float stretch_factor = 0.6; // Factor to stretch the noise
uniform sampler2D noise_texture; // Noise texture
// THEN IN fragment():
// ADD (on top):
float time = TIME * noise_speed;
vec2 moving_uv = v_uv + noise_direction * time;
vec2 stretched_uv = vec2(moving_uv.x, moving_uv.y * stretch_factor);
float noise_value = texture(noise_texture, stretched_uv).r * noise_strength;
// AND THEN:
// REPLACE:
gradient_height = clamp(pow(gradient_height, gradient_bias), 0.0, 1.0);
// WITH THIS:
gradient_height = clamp(pow(gradient_height, gradient_bias) + noise_value, 0.0, 1.0);
The noise was static, so it was animated from one end to the other by moving the UVs with time. Also, UVs were stretched in one direction by multiplying it with a uniform variable stretch_factor
.
This is how it looks so far:
Adding Colors
Now we replace the boring gradient color with our custon color that we assign as uniform. For this, create a new uniform variable of vec3
type and call it _color
. Next assign it to ALBEDO
like this:
ALBEDO = neon(pow(ALPHA, power_factor), _color);
But wait, note that I have used a neon()
function instead of directly assigning the color. This is because neon
function intensifies the color and makes it look more like flame. Directly assigning will make it look dull.
The neon function is simple:
vec3 neon(float value, vec3 color) {
float ramp = clamp(value, 0.0, 1.0);
vec3 output_color = vec3(0.0);
ramp = ramp * ramp;
output_color += pow(color, vec3(4.0)) * ramp;
ramp = ramp * ramp;
output_color += color * ramp;
ramp = ramp * ramp;
output_color += vec3(1.0) * ramp;
return output_color;
}
Making edges smooth
Notice the sharp edges of cone above. We can easily remove them with a simple Fresnel
function.
float fresnel(float amount, vec3 normal, vec3 view) {
return pow((1.0 - clamp(dot(normalize(normal), normalize(view)), 0.0, 1.0)), amount);
}
This is how Fresnel function looks like (the darker regions have lower value (approaching 0.0) and brighter region have values approaching 1.0.
Now if we multiply the fresnel function with the ALPHA, we will get ends of the model as transparent, so edges will not be sharp. So lets calculate fresnel & multiply them:
// AT END OF fragment():
float fresnel_effect = 1.0 - fresnel(fresnel_factor, NORMAL, VIEW);
fresnel_effect = pow(fresnel_effect * fresnel_amplification, fresnel_power);
ALPHA *= fresnel_effect;
I have just amplified the fresnel to make it look right using the pow
function with some variables that are uniforms. fresnel_effect
, fresnel_amplification
and fresnel_power
are simple float
type uniforms.
More
There are tons of controls to make jet thruster look better. You can play with the values to get what is desirable. For example:
Full Godot Shader Code
shader_type spatial;
render_mode blend_mix, unshaded, cull_back;
group_uniforms Basic_Effects;
uniform float model_height = 3.0;
uniform float dissolve_start : hint_range(0.0, 1.0) = 0.001;
uniform float dissolve_length : hint_range(0.0, 1.0) = 1.0;
uniform float gradient_bias : hint_range(0.1, 5.0) = 1.0;
uniform vec2 noise_direction = vec2(0.0, 1.0); // Direction of noise movement
group_uniforms Noise_Effects;
uniform float noise_speed = 1.0; // Speed of noise movement
uniform float noise_strength = 0.1; // Strength of the noise effect
uniform float stretch_factor = 0.6; // Factor to stretch the noise
uniform sampler2D noise_texture; // Noise texture
varying float vert_height;
varying vec2 v_uv;
group_uniforms Misc_Effects;
uniform vec3 _color : source_color;
uniform float power_factor = 1.0;
uniform float alpha_intensity_factor = 2.0;
group_uniforms Fresnel_Effects;
uniform float fresnel_factor = 1.0;
uniform float fresnel_amplification = 2.0; // Corrected typo
uniform float fresnel_power = 2.0;
uniform bool enable_fresnel = true;
vec3 neon(float value, vec3 color) {
float ramp = clamp(value, 0.0, 1.0);
vec3 output_color = vec3(0.0);
ramp = ramp * ramp;
output_color += pow(color, vec3(4.0)) * ramp;
ramp = ramp * ramp;
output_color += color * ramp;
ramp = ramp * ramp;
output_color += vec3(1.0) * ramp;
return output_color;
}
float fresnel(float amount, vec3 normal, vec3 view) {
return pow((1.0 - clamp(dot(normalize(normal), normalize(view)), 0.0, 1.0)), amount);
}
void vertex() {
vert_height = (VERTEX.y + (model_height / 2.0)) / model_height;
v_uv = UV;
}
void fragment() {
float time = TIME * noise_speed;
vec2 moving_uv = v_uv + noise_direction * time;
vec2 stretched_uv = vec2(moving_uv.x, moving_uv.y * stretch_factor);
float noise_value = texture(noise_texture, stretched_uv).r * noise_strength;
float gradient_height = vert_height - dissolve_start;
gradient_height *= 1.0 / dissolve_length;
gradient_height = clamp(pow(gradient_height, gradient_bias) + noise_value, 0.0, 1.0);
ALPHA = mix(1.0, 0.0, gradient_height);
ALPHA = pow(ALPHA, power_factor);
ALBEDO = neon(pow(ALPHA, power_factor), _color);
ALPHA = pow(ALPHA, alpha_intensity_factor);
if (enable_fresnel) {
float fresnel_effect = 1.0 - fresnel(fresnel_factor, NORMAL, VIEW);
fresnel_effect = pow(fresnel_effect * fresnel_amplification, fresnel_power);
ALPHA *= fresnel_effect;
}
}
Thank you for reading. Have a great day <3