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;
}
Live Preview
Tags
2d, shadow, top down
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 Fish_233

Related shaders

guest

0 Comments
Oldest
Newest Most Voted