This commit is contained in:
Jérôme Bousquié
2026-09-24 11:21:35 +02:00
parent 004761252b
commit 805babe53d
18 changed files with 733 additions and 400 deletions
+26 -6
View File
@@ -91,7 +91,7 @@ struct FrameUniforms {
num_spot: u32,
shadow_light_index: u32, // packed index of the shadow light ; MAX_LIGHTS = off
light_view_proj: mat4x4<f32>, // world → shadow light clip space (Étape 14, D3)
shadow_params: vec4<f32>, // .x = shadow map size, .y = depth bias
shadow_params: vec4<f32>, // .x = map size, .y = constant bias, .z = slope bias
options: vec4<u32>, // .x = unlit flag ; .y = shadows on
};
@@ -202,16 +202,20 @@ fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
diffuse += frame.lights[i].color.rgb * frame.lights[i].color.a * ndotl * falloff * spot_factor;
}
let lit = base * (ambient + diffuse) * compute_shadow(in.world_pos);
let lit = base * (ambient + diffuse) * compute_shadow(in.world_pos, n);
return vec4<f32>(lit, in.color.a);
}
// Étape 14 (DRAFT 3.2, D5) : PCF shadow factor for this fragment. Reprojects the world position
// into the shadow light's clip space, converts to depth-map UVs + normalized depth, then averages
// a 3×3 `textureSampleCompare` neighborhood using the comparison sampler (LessEqual). Returns
// 1.0 when fully lit (or shadows disabled), 0.0 when fully in shadow. The reference depth is
// pulled toward the viewer by `frame.shadow_params.y` (bias) to suppress acne.
fn compute_shadow(world_pos: vec3<f32>) -> f32 {
// 1.0 when fully lit (or shadows disabled), 0.0 when fully in shadow.
//
// Bias strategy : **slope-scaled** — the reference depth is pulled toward the viewer by
// `max(constant_bias, slope_bias * (1.0 - abs(dot(n, light_dir))))`. The slope term grows as the
// surface becomes perpendicular to the light (grazing angle), where acne is worst. This prevents
// the large black patches that a constant bias alone cannot suppress on large flat surfaces.
fn compute_shadow(world_pos: vec3<f32>, normal: vec3<f32>) -> f32 {
// Shadows off (options.y == 0) or no valid caster (sentinel = MAX_LIGHTS) → fully lit.
if (frame.options.y == 0u || frame.shadow_light_index == MAX_LIGHTS) {
return 1.0;
@@ -224,9 +228,25 @@ fn compute_shadow(world_pos: vec3<f32>) -> f32 {
// The light projection is built with the WebGPU `[0,1]` clip-depth convention (glam
// directx/WebGPU module), so NDC z is already in [0,1]: no extra remap is needed.
let current_depth = shadow_ndc.z;
let bias = frame.shadow_params.y;
let texel = 1.0 / max(frame.shadow_params.x, 1.0);
// Slope-scaled bias (fixes the large acne patches on surfaces at grazing angles to the light).
// Direction from surface toward the shadow-casting light:
// directional → position_dir.xyz (already the surface→light direction)
// spot → normalize(light_position - world_pos)
let sl_idx = frame.shadow_light_index;
let sl = frame.lights[sl_idx];
let is_dir = (sl_idx < frame.num_directional);
var light_dir: vec3<f32>;
if (is_dir) {
light_dir = normalize(sl.position_dir.xyz);
} else {
light_dir = normalize(sl.position_dir.xyz - world_pos);
}
// The slope factor: 0 when the normal faces the light (no bias needed), 1 when perpendicular.
let slope = 1.0 - abs(dot(normalize(normal), light_dir));
let bias = max(frame.shadow_params.y, frame.shadow_params.z * slope);
// 3×3 PCF : average of the comparison results around the fragment's texel.
var lit_count = 0.0;
for (var ox = -1i; ox <= 1; ox++) {