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!