// Shadow sampling prelude for receiver shaders.
//
// Included in fill-extrusion and model fragment shaders to sample the
// cascaded shadow map and compute shadow occlusion.
//
// Expects the following to be defined in the including shader:
// - `shadow_params` uniform with light matrices and parameters
// - `shadow_map_0`, `shadow_map_1` depth textures
// - `shadow_sampler` comparison sampler
struct ShadowParams {
light_matrix_0: mat4x4<f32>,
light_matrix_1: mat4x4<f32>,
// [intensity, texel_size, normal_offset, cascade_split]
shadow_config: vec4<f32>,
// [dir.x, dir.y, dir.z, 0]
shadow_dir: vec4<f32>,
};
/// Transform a world-space position into shadow-map UV + depth for a cascade.
fn shadow_coords(light_matrix: mat4x4<f32>, world_pos: vec3<f32>) -> vec3<f32> {
let light_clip = light_matrix * vec4<f32>(world_pos, 1.0);
// Perspective divide (orthographic, but still needed for mat4 correctness).
let ndc = light_clip.xyz / light_clip.w;
// Map from NDC [-1,1] XY → [0,1] UV. Z is already [0,1] in WGPU.
return vec3<f32>(ndc.x * 0.5 + 0.5, -ndc.y * 0.5 + 0.5, ndc.z);
}
/// Sample shadow occlusion for a world-space position.
///
/// Returns a value in [0, 1] where 0 = fully lit and 1 = fully shadowed.
fn shadow_occlusion(
world_pos: vec3<f32>,
normal: vec3<f32>,
params: ShadowParams,
map_0: texture_depth_2d,
map_1: texture_depth_2d,
samp: sampler_comparison,
) -> f32 {
let intensity = params.shadow_config.x;
if intensity < 0.001 {
return 0.0;
}
let texel_size = params.shadow_config.y;
let normal_offset_scale = params.shadow_config.z;
let cascade_split = params.shadow_config.w;
// Normal offset to reduce shadow acne.
let sun_dir = normalize(params.shadow_dir.xyz);
let ndotl = max(dot(normal, sun_dir), 0.0);
let offset_scale = normal_offset_scale * (1.0 - ndotl);
let offset_pos = world_pos + normal * offset_scale;
// Try cascade 0 first.
let coords0 = shadow_coords(params.light_matrix_0, offset_pos);
if coords0.x >= 0.0 && coords0.x <= 1.0 && coords0.y >= 0.0 && coords0.y <= 1.0
&& coords0.z >= 0.0 && coords0.z <= 1.0 {
let shadow = textureSampleCompare(map_0, samp, coords0.xy, coords0.z);
return (1.0 - shadow) * intensity;
}
// Fallback to cascade 1.
let coords1 = shadow_coords(params.light_matrix_1, offset_pos);
if coords1.x >= 0.0 && coords1.x <= 1.0 && coords1.y >= 0.0 && coords1.y <= 1.0
&& coords1.z >= 0.0 && coords1.z <= 1.0 {
// Fade out at the far edge.
let fade_start = 0.75;
let edge = max(
max(abs(coords1.x * 2.0 - 1.0), abs(coords1.y * 2.0 - 1.0)),
0.0
);
let fade = smoothstep(fade_start, 1.0, edge);
let shadow = textureSampleCompare(map_1, samp, coords1.xy, coords1.z);
return (1.0 - shadow) * intensity * (1.0 - fade);
}
// Outside all cascades — no shadow.
return 0.0;
}