2D shadows shader (v2.1)

Features:

  • Any X/Y offset
  • Any color
  • debug helper
  • no need to prepare an image
  • 🆕 uses modulate from parent, including alpha for shadow
  • 🆕 shadow blur. Blur goes out of shadow bounds
  • 🆕 new option for handling parents’ rotation

Github: https://github.com/TABmk/godot-2d-shadow-shader

YouTube preview: https://www.youtube.com/watch?v=9rneonV7nQ0

Shader code
// version: 2.1
// repository: https://github.com/TABmk/godot-2d-shadow-shader

shader_type canvas_item;

uniform bool debug = false;
uniform float border_scale = 2.0;
uniform vec2 shadow_offset = vec2(-10.0, -10.0);
uniform vec4 color : source_color;
uniform float blur_amount : hint_range(0.0, 5.0) = 0.0;
uniform float shadow_scale = 1.5;
uniform bool disable_rotating = false;

varying flat vec4 modulate;
varying flat float sprite_rotation;

void vertex() {
    float final_scale = max(border_scale, border_scale * shadow_scale);
    VERTEX.xy *= vec2(final_scale);
    modulate = COLOR;
    sprite_rotation = atan(MODEL_MATRIX[0][1], MODEL_MATRIX[0][0]);
}

vec4 sample_texture_safe(sampler2D tex, vec2 uv) {
    return (uv.x < 0.0 || uv.x > 1.0 || uv.y < 0.0 || uv.y > 1.0) 
        ? vec4(0.0) 
        : texture(tex, uv);
}

vec4 apply_gaussian_blur(sampler2D tex, vec2 uv, vec2 pixel_size) {
    if (blur_amount <= 0.0) return sample_texture_safe(tex, uv);
    
    vec4 color_blur = vec4(0.0);
    float total_weight = 0.0;
    int kernel_size = int(blur_amount * 3.0);
    
    for (int x = -kernel_size; x <= kernel_size; x++) {
        for (int y = -kernel_size; y <= kernel_size; y++) {
            vec2 blur_offset = vec2(float(x), float(y)) * pixel_size;
            float weight = exp(-0.5 * (float(x * x + y * y)) / (blur_amount * blur_amount));
            color_blur += sample_texture_safe(tex, uv + blur_offset) * weight;
            total_weight += weight;
        }
    }
    
    return total_weight > 0.0 ? color_blur / total_weight : vec4(0.0);
}

vec2 rotate_point(vec2 point, float angle) {
    float s = sin(angle);
    float c = cos(angle);
    return vec2(
        point.x * c - point.y * s,
        point.x * s + point.y * c
    );
}

float calculate_fade(float coord, float scale) {
    if (coord < 0.0) {
        return 1.0 + (coord / (scale - 1.0));
    } else if (coord > 1.0) {
        return 1.0 - ((coord - 1.0) / (scale - 1.0));
    }
    return 1.0;
}

vec4 process_texture(vec2 uv, sampler2D tex, bool is_main, vec2 pixel_size) {
    if (is_main) {
        return sample_texture_safe(tex, uv) * modulate;
    }
    
    vec4 blurred = apply_gaussian_blur(tex, uv, pixel_size);
    float fade_x = calculate_fade(uv.x, shadow_scale);
    float fade_y = calculate_fade(uv.y, shadow_scale);
    float fade = smoothstep(0.0, 1.0, min(fade_x, fade_y));
    
    return vec4(color.rgb, (blurred.a * color.a) * modulate.a * fade);
}

void fragment() {
    float final_scale = max(border_scale, border_scale * shadow_scale);
    vec2 scaled_uv = UV * final_scale - (0.5 * (final_scale - 1.0));
    
    vec4 main_texture = process_texture(scaled_uv, TEXTURE, true, TEXTURE_PIXEL_SIZE);
    
    vec2 adjusted_offset = disable_rotating ? shadow_offset : rotate_point(shadow_offset, -sprite_rotation);
    vec2 shadow_uv = scaled_uv + adjusted_offset * TEXTURE_PIXEL_SIZE;
    
    vec4 shadow = process_texture(shadow_uv, TEXTURE, false, TEXTURE_PIXEL_SIZE);
    
    vec4 main_pm = vec4(main_texture.rgb * main_texture.a, main_texture.a);
    vec4 shadow_pm = vec4(shadow.rgb * shadow.a, shadow.a);

    vec4 out_pm = shadow_pm * (1.0 - main_pm.a) + main_pm;
    vec4 res = vec4(
        out_pm.rgb / max(out_pm.a, 0.0001),
        out_pm.a
    );

    vec4 debug_layer = vec4(1.0, 0.0, 0.0, 0.3);
    COLOR = mix(debug ? debug_layer : res, res, res.a);
}
Tags
2d, shadow
The shader code and all code snippets in this post are under CC0 license and can be used freely without the author's permission. Images and videos, and assets depicted in those, do not fall under this license. For more info, see our License terms.

Related shaders

Stars in Shadows

Dynamic 2D Lights and Soft Shadows

Unshaded with Shadows

guest

10 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
zdrmlp
zdrmlp
1 year ago

is there a way to also make the shadow looks soft on the edge or have fade near edges? so it looks smooth?

lukepass86
lukepass86
1 year ago

This is the shader that worked best, thanks! I just had to replace “source_color” with “hint_color” to make it work for Godot 3.5

It would be perfect to have some sort of blur effect for the shadow.

Chris
Chris
1 year ago

Thank you so much for taking the time to make a V2 with blur! =)

SuperYellows
SuperYellows
4 months ago

I was using this with some sprites with semi-transparent pixels around the edges, and noticed they were getting dropped, leading to janky looking edges on the original texture. So I dug into the code and produced a fix. You can see it here: https://www.reddit.com/user/superyellows/comments/1ldc1fk/2d_shadow_shader_fixed_for_alpha_1/

(Thanks for this cool shader!)

DonProtz
DonProtz
3 months ago

nice work here, thanks for the update. Using this shader besides shadow for glow effect with blur settings high

Adam
Adam
2 months ago

I’m using your shader on a large image, and the blur range of 0 to 5 wasn’t great. So I set the range from 0-50, and that caused huge performance issues. Any ideas?

I ended up reducing the size of my image.

Leonardo Cafissi
2 months ago
Reply to  Adam

if the blur part of the shaders there are 2 nested for loops so I guess the higher blur the more loops are runned, I have no idea how to solve this sorry, I keep the blur value low