Better Top-Down Shadow With Height-Map
A better shader to make more realistic shadow.
Shader code
shader_type canvas_item;
uniform float carrier_scale = 1.5; // 放大 Sprite 矩形以超出范围渲染阴影
uniform sampler2D height_map : filter_nearest, repeat_disable; // 灰度图
uniform vec2 sun_dir = vec2(0.5, 0.5); //2D太阳所在方位
uniform float sun_elevation = 45.0; // 太阳仰角(度),0=水平,90=正头顶
uniform float height = 0.0; //定义离地高度
uniform float thickness : hint_range(0.0, 128.0, 0.1) = 20.0; //定义结构厚度
uniform float step_px = 1.0; // 每步在 UV 上走多少像素(越大越快但更容易漏光/锯齿)
uniform int steps = 32; // 步进次数上限(越大越远的阴影越完整,但更耗)
uniform float shadow_softness = 0.08; // 阴影边缘软化阈值(高度单位):越大越柔和,越小越锐利
uniform bool shadow_aa_enabled = true; // 是否启用 4tap 抗锯齿(沿光线垂直方向多重采样)
uniform float shadow_aa_radius_px = 0.75; // 抗锯齿采样半径(像素)
uniform vec4 shadow_color : source_color = vec4(0.0, 0.0, 0.0, 0.5);
uniform bool draw_shadow_on_transparent = true; // 允许在原图透明区域也绘制阴影(需要你的 Sprite2D 纹理区域足够大,才能“画到建筑外的地面”)
uniform float alpha_cutoff = 0.01;
uniform bool use_albedo_alpha_mask = true; // 用原图 alpha 作为“实体掩码”:透明像素不参与高度遮挡,避免圆角外的伪阴影。
varying vec2 local_dir;
vec2 to_source_uv(vec2 uv) { // 将放大的sprite UV变回原先的大小
return (uv - vec2(0.5)) * vec2(carrier_scale) + vec2(0.5);
}
void vertex() {
VERTEX.xy *= max(carrier_scale, 1.0);
// 归一化后的太阳方向(贴图空间近似)
vec2 world_dir = normalize(sun_dir);
mat2 R = mat2(MODEL_MATRIX);
mat2 inv_rot = transpose(R);
local_dir = normalize(inv_rot * world_dir);
}
float sample_height(vec2 uv) {
vec2 huv = to_source_uv(uv);
// 超出源贴图区域的地方视作“地面高度 0”,避免 clamp 到边缘造成阴影消失/异常。
if (huv.x < 0.0 || huv.x > 1.0 || huv.y < 0.0 || huv.y > 1.0) {
return 0.0;
}
huv = clamp(huv, vec2(0.0), vec2(1.0));
// 用 R 通道作为高度;
float h = texture(height_map, huv).r;
if (use_albedo_alpha_mask) {
float mask_alpha = texture(height_map, huv).a;
h *= step(alpha_cutoff, mask_alpha);
}
return h * thickness;
}
float trace_shadow_occlusion(vec2 start_uv, float start_h, vec2 step_uv, float ray_rise_per_step) { //获得相对阴影高度
float occlusion = 0.0;
float ray_h = start_h;
vec2 uv = start_uv;
for (int i = 0; i < steps; i++) {
uv -= step_uv;
ray_h += ray_rise_per_step;
// 越界直接停止
if (uv.x < 0.0 || uv.x > 1.0 || uv.y < 0.0 || uv.y > 1.0) {
break;
}
float current_h = sample_height(uv);
float occ_step = smoothstep(0.0, shadow_softness * thickness, current_h - ray_h);
occlusion = max(occlusion, occ_step);
if (occlusion > 0.999) {
break;
}
}
return occlusion;
}
void fragment() {
vec2 src_uv = to_source_uv(UV);
bool in_src = (src_uv.x >= 0.0 && src_uv.x <= 1.0 && src_uv.y >= 0.0 && src_uv.y <= 1.0);
vec4 base = vec4(0.0);
if (in_src) {
base = texture(TEXTURE, src_uv);
}
// 沿“指向太阳”的反方向采样(从当前点往太阳方向追溯遮挡物)
vec2 px_to_uv = TEXTURE_PIXEL_SIZE / carrier_scale; // 二维像素尺寸
vec2 step_uv = local_dir * step_px * px_to_uv; // 逐分量相乘
// 简化的“每走一步,射线高度上升多少”
float ray_rise_per_step = (step_px * tan(radians(sun_elevation)));
// 射线起点:向太阳方向偏移 height.x 像素,让结构看起来有高度
vec2 ray_offset = local_dir * (-height * px_to_uv);
vec2 start_uv = UV + ray_offset;
float start_h = sample_height(start_uv);
float occlusion = trace_shadow_occlusion(start_uv, start_h, step_uv, ray_rise_per_step);
// 4tap 抗锯齿:沿光线垂直方向做多重采样,缓解边缘像素跳变
if (shadow_aa_enabled && shadow_aa_radius_px > 0.0) {
vec2 perp_uv = vec2(-local_dir.y, local_dir.x) * (shadow_aa_radius_px * px_to_uv);
float h_p05 = sample_height(UV + perp_uv * 0.5);
float h_n05 = sample_height(UV - perp_uv * 0.5);
float h_p15 = sample_height(UV + perp_uv * 1.5);
float h_n15 = sample_height(UV - perp_uv * 1.5);
float occ_p05 = trace_shadow_occlusion(start_uv + perp_uv * 0.5, h_p05, step_uv, ray_rise_per_step);
float occ_n05 = trace_shadow_occlusion(start_uv - perp_uv * 0.5, h_n05, step_uv, ray_rise_per_step);
float occ_p15 = trace_shadow_occlusion(start_uv + perp_uv * 1.5, h_p15, step_uv, ray_rise_per_step);
float occ_n15 = trace_shadow_occlusion(start_uv - perp_uv * 1.5, h_n15, step_uv, ray_rise_per_step);
occlusion = (occlusion + occ_p05 + occ_n05 + occ_p15 + occ_n15) / 5.0;
}
// 只在可见像素上压暗,或在透明区域“补画地面阴影”
vec4 out_color = vec4(0.0);
if (base.a > alpha_cutoff) {
out_color.rgb = mix(base.rgb, base.rgb * shadow_color.rgb, occlusion);
out_color.a = base.a;
} else if (draw_shadow_on_transparent && occlusion > 0.0) {
out_color = shadow_color;
out_color.a *= occlusion;
}
COLOR = out_color;
}

