3D Fast travel shader (speed lines shader) for Godot 4. Sci-fi effects.
Breakdown
- Initially I a gradient on object, making both ends transparent and rest of the capsule opaque.
- Then I applied the noise texture stretched on the opaque part.
- Then I made the transparency of opaque part to 0.0 if noise value is below certain threshold, thus making speed lines.
- Speed lines are also made more bright by applying neon effect on them which brightens the color.
- Finally, UVs are moved with time to achieve motion.
- Finally, I applied my refraction shader to simulate space-wrap effect in background.
![speed lines 3D shader godot](https://gameidea.org/wp-content/uploads/2024/08/Recording-2024-08-01-at-07.10.59.gif)
Start with gradient
Similar to how I did in jet engine exhaust shader tutorial; initially, we just make our mesh transparency gradient, thus one end becomes transparent (alpha=0) and other end alpha=1.
For this, we calculate vertex height in the vertex shader and normalize it between 0.0 and 1.0. Thus, one end vertex will be represented as 0.0 and opposite end vertex will be represented as 1.0.
Then in fragment shader, we subtract the (normalized) vertex height from threshold height (threshold level). We also make the effect a bit stretched by division with dissolve length and finally clamp the results between 0.0 and 1.0. When we plug this value with alpha, we get nice gradient. For 2-sided gradient, we take abs
of the values and instead subtract vertex_height
by (dissolve_start + dissolve_length / 2)
to make it symmetrical gradient.
![3d gradient shader godot](https://gameidea.org/wp-content/uploads/2024/08/image_2024-08-28_014650806.png)
// UNIFORMS:
uniform float model_height = 2.0;
uniform float dissolve_start : hint_range(0.0, 1.0) = 0.0;
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 vertex_height;
// CALCULATE VERTEX HEIGHT IN vertex() SHADER:
vertex_height = (VERTEX.y + (model_height / 2.0)) / model_height;
// IN fragment() SHADER:
float gradient_height = abs(vertex_height - (dissolve_start + dissolve_length * 0.5));
gradient_height *= 1.0 / dissolve_length;
gradient_height = clamp(pow(gradient_height, gradient_bias), 0.0, 1.0);
// Add transparency based on gradient.
ALPHA = mix(1.0, 0.0, gradient_height);
Adding lines
Lines are basically stretched noise texture. Also, the lines are moving so we have to move the UV towards the given direction. So lets make uniform variables for direction and animate the UVs.
// IN fragment():
vec2 uv_movement = UV + direction.xy * TIME * speed;
// Sample the noise texture with the moving UVs
vec4 noise_color1 = texture(noise_texture, uv_movement);
We then apply noise on the ALBEDO
, witch the effect. Neon effect make the intensity of color increased thus it looks ‘bleeding’ as neon lights do:
// OUTSIDE OF fragment():
vec3 calculate_neon_effect(float value, vec3 base_color) {
float ramp = clamp(value, 0.0, 1.0);
vec3 neon_color = vec3(0.0);
ramp = ramp * ramp;
neon_color += pow(base_color, vec3(4.0)) * ramp;
ramp = ramp * ramp;
neon_color += base_color * ramp;
ramp = ramp * ramp;
neon_color += vec3(1.0) * ramp;
return neon_color;
}
// INSIDE OF fragment():
ALBEDO += calculate_neon_effect(0.50 + noise_color2.a, color2.rgb)
We will have to make all the parts of noise texture below certain threshold as transparent, and show (background) screen texture at that place.
float alpha_blend = noise_color1.a + noise_color2.a;
ALBEDO = mix(ALBEDO, texture(screen_texture, SCREEN_UV).rgb, 1.0 - alpha_blend);
Adding refraction
We can optionally add refraction on the background space, to simulate space-wrap effect in shader. I used my old refraction shader function to distort UVs. And passed those distorted UVs to the screen_texture
instead of directly passing screen texture with default UVs.
vec2 refract_uv(vec2 uv_coords, float _refraction_strength, vec3 surface_normal) {
float refraction_intensity = _refraction_strength * 1.0;
uv_coords += refraction_intensity * length(surface_normal) - refraction_intensity * 1.2;
return uv_coords;
}
// IN fragment():
// REPLACE THIS:
ALBEDO = mix(ALBEDO, texture(screen_texture, SCREEN_UV).rgb, 1.0 - alpha_blend);
// WITH THIS:
ALBEDO = mix(ALBEDO, texture(screen_texture, refract_uv(SCREEN_UV, refraction_strength, normal_map_rgb)).rgb, 1.0 - alpha_blend);
Thank you for reading <3