2D Drop Shadow for Canvas Groups (now updated with scaling parameter)

This drop shadow shader can be applied to CanvasGroup nodes. It has offset, scaling, blur and rotation adjustments.

Limitation: If the shader’s rotation parameter is being used, the shadow does not match the node movement, so a separate script is needed to compensate for that. You can use the code below and attach it to the CanvasGroup:

var base_window_size: Vector2i


func _ready() -> void:
	var base_width: int = ProjectSettings.get_setting("display/window/size/viewport_width")
	var base_height: int = ProjectSettings.get_setting("display/window/size/viewport_height")
	base_window_size = Vector2i(base_width, base_height)


func _process(_delta: float) -> void:
	var normalized_pos = global_position / Vector2(base_window_size) # Map global_position to 0-1 range
	self.material.set_shader_parameter("rotation_pivot", normalized_pos)
Shader code
shader_type canvas_item;
render_mode unshaded;

uniform float original_alpha : hint_range(0.0, 1.0) = 1.0;
uniform vec4 drop_shadow_color : source_color = vec4(0.0, 0.0, 0.0, 0.5);
uniform float shadow_offset_x : hint_range(-0.3, 0.3) = 0.05;
uniform float shadow_offset_y : hint_range(-0.3, 0.3) = 0.05;
uniform vec2 shadow_scale = vec2(1.0, 1.0);
uniform float shadow_rotation : hint_range(0.0, 360.0) = 0.0;
uniform vec2 rotation_pivot = vec2(0.5, 0.5);

uniform float shadow_blur_strength : hint_range(0.0, 20.0) = 2.0;
uniform int shadow_blur_samples : hint_range(3, 8, 1) = 4;
uniform float _blur_scale_factor = 1.0;

uniform sampler2D SCREEN_TEXTURE : hint_screen_texture, repeat_disable, filter_linear_mipmap;

vec2 scale_uv(vec2 uv, vec2 scale) {
    return (uv - rotation_pivot) / scale + rotation_pivot;
}

vec2 rotate_uv(vec2 uv, float angle, vec2 aspect_correction) {
    uv -= rotation_pivot;
    uv *= aspect_correction; // Correct aspect ratio before rotation
    float s = sin(angle);
    float c = cos(angle);
    vec2 rotated = vec2(
        uv.x * c - uv.y * s,
        uv.x * s + uv.y * c
    );
    rotated /= aspect_correction; // Restore original proportions
    return rotated + rotation_pivot;
}

vec4 get_rotated_shadow(vec2 uv, float rotation, vec2 aspect) {
    vec2 scaled_uv = scale_uv(uv, shadow_scale);
    vec2 corrected_uv = rotate_uv(scaled_uv, rotation, aspect);
    return texture(SCREEN_TEXTURE, corrected_uv);
}

vec4 apply_shadow_blur(vec2 uv, vec2 pixel_size, vec2 offset, float rotation, vec2 aspect) {
    float samples = float(pow(2.0, float(shadow_blur_samples)));
	float scaled_blur_strength = shadow_blur_strength * _blur_scale_factor;
    float layer_increment = scaled_blur_strength / samples;
    float angle_increment = TAU / samples;

    // Base rotated shadow
    vec4 shadow_base = get_rotated_shadow(uv - offset, rotation, aspect);
    vec4 color = vec4(drop_shadow_color.rgb, drop_shadow_color.a * shadow_base.a);

    if (scaled_blur_strength <= 0.0) return color;

    // Blur with aspect-corrected rotation
    for (float d = layer_increment; d <= scaled_blur_strength; d += layer_increment) {
        vec4 layer_color = vec4(0.0);
        for (float t = 0.0; t < TAU; t += angle_increment) {
            vec2 sample_uv = uv - offset + vec2(cos(t), sin(t)) * d * pixel_size;
            vec4 sample = get_rotated_shadow(sample_uv, rotation, aspect);
            layer_color += vec4(drop_shadow_color.rgb, drop_shadow_color.a * sample.a);
        }
        layer_color /= samples;
        float weight = 1.0 - sqrt(d / scaled_blur_strength);
        color = mix(color, layer_color, weight);
    }

    return color;
}

void fragment() {
    vec2 pixel_size = SCREEN_PIXEL_SIZE;
    vec2 uv = SCREEN_UV;
    float rotation = radians(shadow_rotation);
    vec2 offset = vec2(shadow_offset_x, shadow_offset_y);
    
    // Calculate aspect ratio correction (width/height, height/width)
    vec2 aspect = vec2(1.0, SCREEN_PIXEL_SIZE.x/SCREEN_PIXEL_SIZE.y);

    vec4 original = texture(SCREEN_TEXTURE, uv);
    vec4 shadow = apply_shadow_blur(uv, pixel_size, offset, rotation, aspect);
	
	original.a *= original_alpha; // Alpha modulation affects both original and shadow
	shadow.a *= original_alpha;
	
	COLOR = mix(shadow, original, original.a);
}
Live Preview
Tags
2d, canvas group, drop shadow, 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

guest

2 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
kyle
kyle
8 months ago

Looks amazing! But not too fit to isometric games, wonder if it’s hard to make it project shadow like this : https://godotshaders.com/shader/shadow-2d ?

ThatOneGuyJesse
ThatOneGuyJesse
28 days ago

I had the problem where my shadow was shifting around when zooming in/out. Fixed it by adding a vec2 for zoom level and altering this one line:

uniform vec2 zoom = vec2(1.0, 1.0); // set through script
...
// vec2 offset = vec2(shadow_offset_x, shadow_offset_y); // <- replace this line
vec2 offset = vec2(shadow_offset_x, shadow_offset_y) * zoom; // scale offset by zoom to keep it consistent regardless of zoom level

This means we need to set the zoom level with a simple script on the CanvasGroup:

extends CanvasGroup


@onready var camera: Camera2D = get your camera or whatever


func _process(_delta: float) -> void:
    self.material.set_shader_parameter("zoom", camera.get_zoom())