2D top down oblique shadow

This shader is a 2D top-down height-map shadow shader.

It computes per-pixel shadowing by ray-marching backward along the sun direction to test whether light is blocked by higher surface values.

Using carrier_scale together with source_uv_scale, it allows shadows to extend beyond the original sprite bounds while keeping the source content centered.

It also uses smoothstep-based soft thresholding and optional 4-tap anti-aliasing to reduce hard edges and jagged artifacts.

Additionally, it can render shadow tint in transparent regions, which is especially useful for projecting building shadows onto surrounding ground.

Ref:

1. https://editor.p5js.org/BarneyCodes/sketches/brfZ0NNpZ

2. https://www.youtube.com/watch?v=bMTeCqNkId8

Shader code
shader_type canvas_item;

render_mode blend_mix;

// 承载面放大倍数:>1.0 时可让阴影绘制超出原 Sprite 矩形
uniform float carrier_scale = 1.0;

// 灰度高度图:0=地面,1=最高处(由美术约定)
uniform sampler2D height_map : filter_nearest, repeat_disable;
// 原图采样(用于 alpha 掩码;不要依赖内建 TEXTURE,避免在辅助函数中报未定义)
uniform sampler2D albedo_map : filter_nearest, repeat_disable;

// 高度图 UV 缩放(以贴图中心为基准)。
// < 1.0:把高度图“缩小”(更集中在中间);> 1.0:把高度图“放大”。
uniform float height_uv_scale = 1.0;

// 源贴图(颜色/高度)的采样区域缩放(以中心为基准)。
// 用途:当你把“阴影承载 Sprite”缩放得更大时,用这个参数让采样仍聚焦在中间的建筑区域,
// 从而让阴影可以画到原建筑贴图之外(但仍在承载 Sprite 的矩形内)。
// - > 1.0:源贴图在承载面中更“集中”(承载面更大,建筑占更小区域)
// - = 1.0:不改变
uniform vec2 source_uv_scale = vec2(1.0, 1.0);

// 太阳方向(在“贴图空间”里使用):xy 控制影子方向,z 控制太阳高度(z 越大影子越短)。
// 这不是世界坐标的真实太阳向量,而是为了 2D 高度场投影而做的近似参数。
uniform vec3 sun_dir = vec3(0.6, 0.4, 1.0);

// 高度缩放:把 height_map 的 0..1 映射到“可投影的高度”。
uniform float height_scale = 1.0;

// 每步在 UV 上走多少像素(越大越快但更容易漏光/锯齿)
uniform float step_px = 1.0;

// 步进次数上限(越大越远的阴影越完整,但更耗)
uniform int steps = 32;

// 射线起点偏移(像素):正值 = 向太阳方向偏移,让射线从 sprite 边缘外侧发出,
// 避免阴影在建筑内部出现偏移伪影;推荐从 step_px 的 0.5~1 倍开始调。
uniform float shadow_ray_offset_px = 0.0;

// 阴影边缘软化阈值(高度单位):越大越柔和,越小越锐利
uniform float shadow_softness = 0.08;

// 是否启用 4tap 抗锯齿(沿光线垂直方向多重采样)
uniform bool shadow_aa_enabled = true;
// 抗锯齿采样半径(像素)
uniform float shadow_aa_radius_px = 0.75;

// 阴影强度与颜色(颜色通常用偏冷的深灰/蓝灰)
uniform float shadow_strength = 0.65;
uniform vec4 shadow_tint : source_color = vec4(0.0, 0.0, 0.0, 1.0);

// 允许在原图透明区域也绘制阴影(需要你的 Sprite2D 纹理区域足够大,才能“画到建筑外的地面”)
uniform bool draw_shadow_on_transparent = true;
uniform float alpha_cutoff = 0.01;

// 用原图 alpha 作为“实体掩码”:透明像素不参与高度遮挡,避免圆角外的伪阴影。
uniform bool use_albedo_alpha_mask = true;

// 是否绘制原贴图(做“阴影专用 Sprite”时建议关掉)
uniform bool draw_albedo = true;

const int MAX_STEPS = 96;

void vertex() {
	VERTEX.xy *= max(carrier_scale, 1.0);
}

vec2 to_source_uv(vec2 uv) {
	// 把承载面的 UV 映射到源贴图的 UV(以中心为基准缩放)
	vec2 s = max(source_uv_scale, vec2(1e-4));
	return (uv - vec2(0.5)) * s + vec2(0.5);
}

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;
	}

	float s = max(height_uv_scale, 1e-4);
	huv = (huv - vec2(0.5)) * s + vec2(0.5);
	huv = clamp(huv, vec2(0.0), vec2(1.0));
	// 用 R 通道作为高度;如果你的高度图存在 A 通道,也可以改成 .a
	float h = texture(height_map, huv).r * height_scale;
	if (use_albedo_alpha_mask) {
		float mask_alpha = texture(albedo_map, huv).a;
		h *= step(alpha_cutoff, mask_alpha);
	}
	return h;
}

float trace_shadow_occlusion(vec2 start_uv, float start_h, vec2 step_uv, float ray_rise_per_step, float edge_softness) {
	float occlusion = 0.0;
	float ray_h = start_h;
	vec2 uv = start_uv;

	for (int i = 0; i < MAX_STEPS; i++) {
		if (i >= steps) {
			break;
		}

		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 h = sample_height(uv);
		// 把硬阈值(h > ray_h)改成软阈值,减轻锯齿
		float occ_step = smoothstep(0.0, edge_softness, 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 (draw_albedo && in_src) {
		base = texture(TEXTURE, src_uv) * COLOR;
	}

	// 当前像素高度(同一张 UV 对齐的高度图)
	float h0 = sample_height(UV);

	// 归一化后的太阳方向(贴图空间近似)
	vec3 L = normalize(sun_dir);
	float sun_xy_len = max(length(L.xy), 1e-5);
	float sun_z = max(L.z, 1e-4);

	// 沿“指向太阳”的反方向采样(从当前点往太阳方向追溯遮挡物)
	vec2 dir_uv = normalize(L.xy);
	float px_to_uv = length(TEXTURE_PIXEL_SIZE);
	vec2 step_uv = dir_uv * (step_px * px_to_uv);

	// 简化的“每走一步,射线高度上升多少”
	// 这里的单位是经验近似:想要更物理,需要把高度单位和像素/UV单位严格统一。
	float ray_rise_per_step = (sun_z / sun_xy_len) * (step_px * 0.02);

	// 射线起点:向太阳方向偏移 shadow_ray_offset_px 像素,让射线从建筑外侧发出
	vec2 ray_offset = dir_uv * (shadow_ray_offset_px * px_to_uv);
	vec2 start_uv = UV + ray_offset;
	float edge_softness = max(shadow_softness, 1e-4);

	float occlusion = trace_shadow_occlusion(start_uv, h0, step_uv, ray_rise_per_step, edge_softness);

	// 4tap 抗锯齿:沿光线垂直方向做多重采样,缓解边缘像素跳变
	if (shadow_aa_enabled && shadow_aa_radius_px > 0.0) {
		vec2 perp_uv = vec2(-dir_uv.y, dir_uv.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, edge_softness);
		float occ_n05 = trace_shadow_occlusion(start_uv - perp_uv * 0.5, h_n05, step_uv, ray_rise_per_step, edge_softness);
		float occ_p15 = trace_shadow_occlusion(start_uv + perp_uv * 1.5, h_p15, step_uv, ray_rise_per_step, edge_softness);
		float occ_n15 = trace_shadow_occlusion(start_uv - perp_uv * 1.5, h_n15, step_uv, ray_rise_per_step, edge_softness);

		occlusion = (occlusion + occ_p05 + occ_n05 + occ_p15 + occ_n15) / 5.0;
	}

	float shadow = shadow_strength * occlusion;

	// 只在可见像素上压暗,或在透明区域“补画地面阴影”
	vec4 out_color = vec4(0.0);
	if (base.a > alpha_cutoff) {
		base.rgb = mix(base.rgb, base.rgb * (1.0 - shadow), shadow);
		out_color = base;
	} else if (draw_shadow_on_transparent && shadow > 0.0) {
		out_color = shadow_tint;
		out_color.a *= shadow;
	}

	COLOR = out_color;
}
Live Preview
Tags
oblique, shadow, topdown
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.

Related shaders

guest

1 Comment
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
Torguen
Torguen
26 days ago

Gracias por compartir esto es estupendo este shader.