diff --git a/lib/src/resources/lights.rs b/lib/src/resources/lights.rs index b0c1799..717e3cb 100644 --- a/lib/src/resources/lights.rs +++ b/lib/src/resources/lights.rs @@ -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); + } } diff --git a/lib/src/shaders/standard_shader.wgsl b/lib/src/shaders/standard_shader.wgsl index 395e781..e7a12c5 100644 --- a/lib/src/shaders/standard_shader.wgsl +++ b/lib/src/shaders/standard_shader.wgsl @@ -169,15 +169,19 @@ fn fs_main(in: VertexOutput) -> @location(0) vec4 { } // 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);