Reject degenerate shadow receivers
Docs Deploy / Build and Deploy MkDocs (push) Successful in 1m30s

This commit is contained in:
Valentin Popov committed 2026-10-16 01:07:56 +04:00
1 parent 1b6617aaa8
commit c92beb1b38
1 file changed
+64 -7
+64 -7
View File
@@ -893,13 +893,11 @@ fn project_receivers(
if !use_rays && !use_general { if !use_rays && !use_general {
continue; continue;
} }
let normal = normalize(receiver.normal).unwrap_or_else(|| { // Reject collapsed geometry even when the caller supplied a valid fallback normal.
normalize(cross( let Some(geometric_normal) = geometric_triangle_normal(receiver.positions) else {
sub(receiver.positions[1], receiver.positions[0]), continue;
sub(receiver.positions[2], receiver.positions[0]), };
)) let normal = normalize(receiver.normal).unwrap_or(geometric_normal);
.unwrap_or([0.0, 0.0, 0.0])
});
let normal_term = dot(page.projection.shadow_axis, normal); let normal_term = dot(page.projection.shadow_axis, normal);
if normal_term <= 0.0 || !normal_term.is_finite() { if normal_term <= 0.0 || !normal_term.is_finite() {
continue; continue;
@@ -1136,6 +1134,25 @@ fn validate_triangle(triangle: &ShadowTriangle, field: &'static str) -> Result<(
Ok(()) Ok(())
} }
fn geometric_triangle_normal(positions: [[f32; 3]; 3]) -> Option<[f32; 3]> {
let a = positions[0].map(f64::from);
let b = positions[1].map(f64::from);
let c = positions[2].map(f64::from);
let ab = std::array::from_fn::<_, 3, _>(|axis| b[axis] - a[axis]);
let ac = std::array::from_fn::<_, 3, _>(|axis| c[axis] - a[axis]);
let cross = [
ab[1] * ac[2] - ab[2] * ac[1],
ab[2] * ac[0] - ab[0] * ac[2],
ab[0] * ac[1] - ab[1] * ac[0],
];
let length_squared = cross.iter().map(|value| value * value).sum::<f64>();
if !length_squared.is_finite() || length_squared == 0.0 {
return None;
}
let length = length_squared.sqrt();
Some(cross.map(|value| (value / length) as f32))
}
fn validate_light(light: &ShadowLight) -> Result<(), ShadowError> { fn validate_light(light: &ShadowLight) -> Result<(), ShadowError> {
match *light { match *light {
ShadowLight::Directional { direction, rgb, .. } ShadowLight::Directional { direction, rgb, .. }
@@ -1753,6 +1770,46 @@ mod tests {
assert_eq!(filtered.indices.len(), accepted.indices.len()); assert_eq!(filtered.indices.len(), accepted.indices.len());
} }
#[test]
fn degenerate_receivers_with_supplied_normal_are_skipped_without_losing_neighbors() {
let caster = caster(1, 1.0, 3, Vec::new());
let light = ShadowLight::Directional {
direction: [0.0, 0.0, 1.0],
rgb: [1.0; 3],
active: true,
flags: SHADOW_PRIMARY_DIRECTIONAL_FLAG,
};
let valid = triangle([[-1.0, -1.0, 1.0], [0.0, 1.0, 1.0], [1.0, -1.0, 1.0]], 0);
let point = ShadowTriangle {
positions: [[0.0, 0.0, 1.0]; 3],
normal: [0.0, 0.0, 1.0],
mask: 0,
};
let collinear = ShadowTriangle {
positions: [[-1.0, 0.0, 1.0], [0.0, 0.0, 1.0], [1.0, 0.0, 1.0]],
normal: [0.0, 0.0, 1.0],
mask: 0,
};
let build = |receivers: &[ShadowTriangle]| {
build_shadow_frame(ShadowScene {
lights: &[light],
casters: &[caster.clone()],
receivers,
fade: 1.0,
smooth_passes: 0,
camera: None,
lod: ShadowLodSettings::default(),
})
.expect("shadow frame")
};
let expected = build(&[valid]);
let actual = build(&[point, collinear, valid]);
assert!(!expected.indices.is_empty(), "valid neighbor must render");
assert_eq!(actual.vertices, expected.vertices);
assert_eq!(actual.indices, expected.indices);
}
fn triangle(positions: [[f32; 3]; 3], mask: u32) -> ShadowTriangle { fn triangle(positions: [[f32; 3]; 3], mask: u32) -> ShadowTriangle {
ShadowTriangle { ShadowTriangle {
positions, positions,