Dynamic Shadow Shader

Godot Version

4.6.2

Question

A couple of weeks ago, I began working on a dynamic shadow system based on this video:

Which uses height maps and ray stepping to create dynamic lighting in a fully 2d environment. In my version, since I have many more assets than the game in the video, I’m using LSB stenography at a 5-bit resolution to encode the height maps into the textures themselves and pull the data from the pixels in the shader, so I don’t have to have duplicate sprites; one for the height map and one for the normal sprite. Unfortunately, this means that the entire process is really expensive and causes my MacBook Pro to constantly run at 180 degrees.

Is there any way to make this process less expensive or reduce how often it runs?

shader_type canvas_item;

uniform vec4 color_over : source_color;
uniform int check_range : hint_range(1, 300) = 125;
uniform float margin : hint_range(0.0, 0.1) = 0.014;
uniform float bias : hint_range(0.0, 1) = 0.018;
uniform vec2 SunDir = vec2(1.0, 1.0);
uniform float shadow_length : hint_range(0.1, 10.0) = 1.0;
uniform sampler2D encoded_map : filter_nearest;
uniform int bits = 5;
uniform vec2 snap_resolution = vec2(320.0, 180.0); 

float decode_height(vec2 uv) {
    float b = texture(encoded_map, uv).b;
    int b_byte = int(b * 255.0);
    int mask = (1 << bits) - 1;
    return float(b_byte & mask) / float(mask);
}

vec2 snap(vec2 uv) {
    return floor(uv * snap_resolution) / snap_resolution;
}

void fragment() {
    vec2 snapped_uv = snap(UV);
    vec4 sprite_color = texture(TEXTURE, snapped_uv);

    if (sprite_color.a == 0.0) {
        COLOR = vec4(0.0);
    }
	else {
    float start_height = decode_height(snapped_uv);
    vec2 step_dir = normalize(SunDir) * shadow_length;

    bool in_shadow = false;

    for (int i = 1; i <= check_range; i++) {
        vec2 location = step_dir * float(i);
        vec2 diag_uv = snap(snapped_uv + location * TEXTURE_PIXEL_SIZE);

        if (diag_uv.x < 0.0 || diag_uv.x > 1.0 ||
            diag_uv.y < 0.0 || diag_uv.y > 1.0) {
            break;
        }

        float diag_height = decode_height(diag_uv);
        float required_height = start_height + length(location) / 255.0 + bias;

        if (required_height + margin + bias >= diag_height &&
            required_height - margin + bias <= diag_height) {
            in_shadow = true;
            break;
        }
    }

    COLOR = in_shadow ? sprite_color * color_over : sprite_color;
}
}

This code pulls the current screen from a subviewport with the main game nodes as children and updates the texture of the sprite 2d the shader is attached to:

extends Sprite2D

var is_capturing = false

func _ready():
	await RenderingServer.frame_post_draw
	capture()

func _process(_delta):
	material.set_shader_parameter("SunDir", get_global_mouse_position().normalized())
	capture()

func capture():
	if is_capturing:
		return
	is_capturing = true
	await RenderingServer.frame_post_draw
	var img
	if $"../" is not Control:
		img = $"../../SubViewportContainer/SubViewport".get_texture().get_image()
	else:
		img = $"../SubViewportContainer/SubViewport".get_texture().get_image()
	var tex = ImageTexture.create_from_image(img)

	
	texture = tex
	material.set_shader_parameter('encoded_map', tex)
	is_capturing = false

Thank you!

Raymarching will always be expensive. Imo this whole approach is not worth the effort. Much easier (and performant) to render 3d world and pixelate it in a post processing shader to get the desired look. You get dynamic shadows from any number of light sources “for free”.

Is there any way I could get a similar effect while still staying 2D without the use of ray marching?

You can always just draw it. Do you really need dynamic shadows?

I’d like them to move around somewhat. My original system involved duplicating, changing the colors of, and rotating the original sprite to look like a shadow. That worked pretty well for objects that were taller than they were wide but not for the others.