aboutsummaryrefslogtreecommitdiff
path: root/crates/fparkan-fx/src/shadow.rs
diff options
context:
space:
mode:
authorValentin Popov <valentin@popov.link>2026-10-16 00:07:56 +0300
committerValentin Popov <valentin@popov.link>2026-10-16 00:07:56 +0300
commitc92beb1b389fb1b35fd8eac871fb5372c5093b88 (patch)
treeb690909f79488979d58875f8e30fa255f382334b /crates/fparkan-fx/src/shadow.rs
parent1b6617aaa8df2749c341545a9102f419637c8c7f (diff)
downloadfparkan-c92beb1b389fb1b35fd8eac871fb5372c5093b88.tar.xz
fparkan-c92beb1b389fb1b35fd8eac871fb5372c5093b88.zip
Reject degenerate shadow receiversdevel
Diffstat (limited to 'crates/fparkan-fx/src/shadow.rs')
-rw-r--r--crates/fparkan-fx/src/shadow.rs71
1 files changed, 64 insertions, 7 deletions
diff --git a/crates/fparkan-fx/src/shadow.rs b/crates/fparkan-fx/src/shadow.rs
index 64e2b8e..19473ca 100644
--- a/crates/fparkan-fx/src/shadow.rs
+++ b/crates/fparkan-fx/src/shadow.rs
@@ -893,13 +893,11 @@ fn project_receivers(
if !use_rays && !use_general {
continue;
}
- let normal = normalize(receiver.normal).unwrap_or_else(|| {
- normalize(cross(
- sub(receiver.positions[1], receiver.positions[0]),
- sub(receiver.positions[2], receiver.positions[0]),
- ))
- .unwrap_or([0.0, 0.0, 0.0])
- });
+ // Reject collapsed geometry even when the caller supplied a valid fallback normal.
+ let Some(geometric_normal) = geometric_triangle_normal(receiver.positions) else {
+ continue;
+ };
+ let normal = normalize(receiver.normal).unwrap_or(geometric_normal);
let normal_term = dot(page.projection.shadow_axis, normal);
if normal_term <= 0.0 || !normal_term.is_finite() {
continue;
@@ -1136,6 +1134,25 @@ fn validate_triangle(triangle: &ShadowTriangle, field: &'static str) -> Result<(
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> {
match *light {
ShadowLight::Directional { direction, rgb, .. }
@@ -1753,6 +1770,46 @@ mod tests {
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 {
ShadowTriangle {
positions,