Fast Travel Shader (Speed Lines)

3d speed lines shader godot

3D Fast travel shader (speed lines shader) for Godot 4. Sci-fi effects.

Breakdown

  1. Initially I a gradient on object, making both ends transparent and rest of the capsule opaque.
  2. Then I applied the noise texture stretched on the opaque part.
  3. Then I made the transparency of opaque part to 0.0 if noise value is below certain threshold, thus making speed lines.
  4. Speed lines are also made more bright by applying neon effect on them which brightens the color.
  5. Finally, UVs are moved with time to achieve motion.
  6. Finally, I applied my refraction shader to simulate space-wrap effect in background.
speed lines 3D shader godot

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
 // 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);
    

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 9background) 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);

Full Shader

shader_type spatial;

render_mode
    cull_disabled,
    unshaded,
    blend_mix;

group_uniforms Speed_Lines;
uniform vec4 color1: source_color;
uniform vec4 color2: source_color;
uniform sampler2D noise_texture; // Our noise texture
uniform vec2 direction; // Direction of UV movement
uniform float speed : hint_range(0.0, 10.0); // Speed of UV movement

group_uniforms Refraction_Efects;
uniform float refraction_strength : hint_range(0.0, 8.0, 0.001) = 0.05;
uniform sampler2D screen_texture : hint_screen_texture;
uniform sampler2D depth_texture : hint_depth_texture;
uniform sampler2D normal_map;

group_uniforms Fading_Ends;
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;

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;
}

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;
}

void vertex() {
    // For fading effect
    vertex_height = (VERTEX.y + (model_height / 2.0)) / model_height;
}

void 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);
    vec4 noise_color2 = texture(noise_texture, uv_movement + vec2(0.50));
    
    vec3 normal_map_rgb = texture(normal_map, uv_movement).rgb;

    // Apply noise color to the fragment
    ALBEDO = calculate_neon_effect(0.50 + noise_color1.a, color1.rgb);
    ALBEDO += calculate_neon_effect(0.50 + noise_color2.a, color2.rgb);
    
    float alpha_blend = noise_color1.a + noise_color2.a;
    
    ALBEDO = mix(ALBEDO, texture(screen_texture, refract_uv(SCREEN_UV, refraction_strength, normal_map_rgb)).rgb, 1.0 - alpha_blend);

    // Fading effects fragment part
    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);
    
    ALPHA = mix(1.0, 0.0, gradient_height);
}

Thank you for reading <3

Leave a Reply

Your email address will not be published. Required fields are marked *