2D shadow with bottom offset, image ratio independent shadow direction, and minimal vertex increase

This is the shader I wrote to get 2D shadows for Gobs and Gods.

It’s “just” a simple casted shadow, but with a few caveats which made is not so trivial to get right:

  • shadow direction does not depend on the image size ratio
  • it increases the sprite box by just the required amount
  • it has a Y-offset parameter to define where is the “bottom” of the shadow
  • it avoids artefacts which may appear when there is a non transparent pixel on the border of the image

However, it only works as-is with shadows casted “to the right”. 

Shader code
shader_type canvas_item;
render_mode blend_mix;

uniform float _vmax = 0.95; // Bottom of the sprite (in UV coordinates.)
uniform vec4 modulate : hint_color;
uniform vec2 shadowDirection = vec2(0.5, 0.5);  // direction the point casting the shadow to the shaded point.
//This direction is in PIXEL space, not UV, to avoid shadow direction depending from the image size ratio.


void vertex()
{
    // how far to the right is the shadow of the top right corner?	
    float pixelsAdded = _vmax * shadowDirection.x / TEXTURE_PIXEL_SIZE.y / max( 1.0, 1.0 + shadowDirection.y) ;	

	// is this a right side pixel?
	int vertexid = VERTEX_ID % 4; // not sure 'WHY' exactly %4 is required, but it's from there: https://www.reddit.com/r/godot/comments/17l9eqn/understanding_vertex_and_sprite_offset_for_simple/
	bool is_right_vertex = vertexid == 1 || vertexid == 2;    	
	
	// moving the right side pixels to the right to cover potential shadow position
	VERTEX += vec2( pixelsAdded * (is_right_vertex ? 1. : 0.), 0.); 
}

void fragment() {

	// original image size in pixels
	vec2 size = 1.0 / TEXTURE_PIXEL_SIZE; 
	// pixels added in vertex
    float pixelsAdded = _vmax * shadowDirection.x * size.y / max( 1.0, 1.0 + shadowDirection.y) ;	
	// size in pixels of the area covered by shader
	vec2 sizeTotal = vec2(size.x + pixelsAdded, size.y );		
	// position in pixels. (note that UV is normalised with sizeTotal)
	vec2 xy = UV * sizeTotal; 

	// height of the pixel
	float dy =  clamp(_vmax - UV.y, 0.0 , 1.0) ;
	// point casting the shadow
	vec2 xyShadow = xy - dy * size.y * shadowDirection;
		
	// UV in texture coordinates
	vec2 correctedUV = xy / size;
	// point casting shadow in same coordinates
	vec2 shadowUV = xyShadow / size;

	// read shadow-casting pixel color
	vec4 shadow = vec4(modulate.rgb, texture(TEXTURE, shadowUV).a * modulate.a);

	// if this pixel is on the border, it may cause artefacts => (smoothly) hide border pixels.
    vec2 shadowUV_OnBorder = smoothstep(vec2(0.),vec2(1.) , shadowUV *100.  )  ;
     shadowUV_OnBorder *= smoothstep(vec2(0.),vec2(1.) , (1.-shadowUV) *100.  )  ;
	shadow.a *= shadowUV_OnBorder.x * shadowUV_OnBorder.y;

	// read color of current pixel
	vec4 col = texture(TEXTURE, correctedUV);
	
	// mix current pixel with shadow behind.
	COLOR = mix(shadow, col, col.a);
}
Tags
2d, shadows
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.

More from alexandre.gilotte

Hexagonal tilemap, simple version

Hexagonal tilemap with blending

Related shaders

2D Spherical Rotations in any Direction From Image

Tilt-Shift Shader (minimal)

Chromatic Abberation (With Offset)

Subscribe
Notify of
guest

1 Comment
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
simon
simon
3 months ago

Awesome stuff, thanks for posting!
Just a heads up on godot 4.3 I needed to change the following

hint_color -> source_color
and

vertexids to check from 1 + 2 to 2 + 3:

bool is_right_vertex = vertexid == 2 || vertexid == 3;

Hopefully this is helpful for anyone wanting to give this a shot

Last edited 3 months ago by simon