fix: spot — corrige le signe du test de cône (cube noir)

Le test du cône comparait l (surface->lumière) à dir_angle.xyz (lumière->scène),
deux directions opposées, donc cone ≈ -1 et facteur spot = 0 : les spots
n'éclairaient rien (cube noir). On teste désormais -l (lumière->point), aligné
avec l'axe du cône. Ajout d'un test Rust qui verrouille l'invariant (alignement
+1 sur l'axe). Build/test/WGSL/fmt OK.
This commit is contained in:
Jérôme Bousquié
2026-09-18 20:48:20 +02:00
parent 73085de537
commit 2582b6f571
2 changed files with 33 additions and 4 deletions
+25
View File
@@ -177,4 +177,29 @@ mod tests {
fn capacity_bounded_by_max_lights() {
assert!(MAX_LIGHTS >= 1);
}
/// Locks the spot sign convention used by the shader: for a surface point that lies on the
/// cone axis, the alignment between the "light -> point" direction (`-l`, where `l` points
/// from the surface toward the light) and the stored cone axis (`dir_angle.xyz`, from the
/// light toward the scene) must be **+1** (full cone), not −1. A regression to the wrong sign
/// would make every spot light contribute zero (black cube). Mirrors the WGSL spot loop.
#[test]
fn spot_cone_axis_alignment_is_positive() {
// Spot at (0,0,3), cone axis pointing toward the origin (light -> scene).
let light_pos = Vec3::new(0.0, 0.0, 3.0);
let surface_point = Vec3::ZERO;
let cone_axis = (surface_point - light_pos).normalize(); // (0,0,-1)
// Shader math: l points surface -> light; the cone test uses -l (light -> point).
let l = (light_pos - surface_point).normalize(); // (0,0,1)
let to_point = -l; // (0,0,-1)
let cone = to_point.dot(cone_axis);
assert!(
(cone - 1.0).abs() < 1e-6,
"on-axis point must align with the cone axis (got {cone}); if it is ~-1 the spot sign is wrong"
);
// Sanity: the buggy expression (dot of l with the axis) would be ~ -1.
assert!((l.dot(cone_axis) + 1.0).abs() < 1e-6);
}
}
+8 -4
View File
@@ -169,15 +169,19 @@ fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
}
// Lumières spot (indices num_directional + num_point..num_directional + num_point +
// num_spot). Cône orienté : pénombre lissée entre le demi-angle intérieur (dir_angle.w) et
// un liseré extérieur (demi-angle − 0.1 rad), plus atténuation linéaire par rayon.
// num_spot). Cône orienté : on teste l'alignement de la direction **de la lumière vers le
// point** de la surface (-l, car l pointe de la surface vers la lumière) avec l'axe du cône
// (dir_angle.xyz, de la lumière vers la scène). Pénombre lissée entre le demi-angle intérieur
// (dir_angle.w) et un liseré extérieur (demi-angle − 0.1 rad), plus atténuation linéaire.
let spot_base = frame.num_directional + frame.num_point;
for (var i = spot_base; i < spot_base + frame.num_spot; i++) {
let to_light = frame.lights[i].position_dir.xyz - in.world_pos;
let dist = length(to_light);
let l = to_light / max(dist, 1e-4);
let l = to_light / max(dist, 1e-4); // surface -> lumière
let ndotl = max(dot(n, l), 0.0);
let cone = dot(l, normalize(frame.lights[i].dir_angle.xyz));
// direction lumière -> point de la surface = -l ; alignée avec l'axe du cône (dir_angle.xyz).
let to_point = -l;
let cone = dot(to_point, normalize(frame.lights[i].dir_angle.xyz));
let cos_inner = frame.lights[i].dir_angle.w;
let cos_outer = cos_inner - 0.1;
let spot_factor = clamp((cone - cos_outer) / max(cos_inner - cos_outer, 1e-4), 0.0, 1.0);