From ca21d4fb7bb763e3dc49d6d9b8f05b5de7a8f8e7 Mon Sep 17 00:00:00 2001 From: KenM76 Date: Tue, 3 Nov 2026 23:06:54 -0500 Subject: [PATCH] M12: prc-render: scale a triangle's edges before their cross product normalize already scales by the largest component, but the face normal was the normalized cross product of two edges, and edges near 1e160 overflow that product to infinity first, so the triangle was dropped. unit_cross divides both edges by their largest component before the cross product. a_huge_square_is_drawn fails with the old line (white instead of [190,192,200]). Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01SqmHYx8FiWM5PcsoiErgp9 --- crates/prc-render/src/render.rs | 4 ++-- crates/prc-render/src/render/tests.rs | 8 ++++++++ crates/prc-render/src/vec3.rs | 19 +++++++++++++++++++ 3 files changed, 29 insertions(+), 2 deletions(-) diff --git a/crates/prc-render/src/render.rs b/crates/prc-render/src/render.rs index d9072a3055..3670878e17 100644 --- a/crates/prc-render/src/render.rs +++ b/crates/prc-render/src/render.rs @@ -5,7 +5,7 @@ use crate::WorldMesh; use crate::raster::{Corner, Target, View, ViewCorner, at}; -use crate::vec3::{cross, dot, length, normalize, scale, sub}; +use crate::vec3::{cross, dot, length, normalize, scale, sub, unit_cross}; use pdfcraft_prc::TriangleMesh; /// The largest image [`render`] draws, in pixels (width × height). @@ -318,7 +318,7 @@ impl Scene { let Some(world) = mesh.triangles.get(t).and_then(|tri| corners(mesh, tri)) else { return; }; - let Some(face) = normalize(cross(sub(at3(&world, 1), at3(&world, 0)), sub(at3(&world, 2), at3(&world, 0)))) else { + let Some(face) = unit_cross(sub(at3(&world, 1), at3(&world, 0)), sub(at3(&world, 2), at3(&world, 0))) else { return; }; let stored = stored_normals(mesh, t); diff --git a/crates/prc-render/src/render/tests.rs b/crates/prc-render/src/render/tests.rs index 8d0764315d..ec8104a260 100644 --- a/crates/prc-render/src/render/tests.rs +++ b/crates/prc-render/src/render/tests.rs @@ -268,3 +268,11 @@ fn unlit_draws_the_plain_colour_and_the_headlight_shades_by_angle() { assert_eq!(lit(Lighting::Headlight, 1.0), [151, 153, 159, 255]); assert_eq!(lit(Lighting::Unlit, 1.0), [190, 192, 200, 255]); } + +#[test] +fn a_huge_square_is_drawn() { + // Its edges' cross product (about 1e320) overflows unless they are scaled first. + let image = render(&[quad(0.0, 1e160, 0.0)], &ortho(4e160), &small()).unwrap(); + assert_eq!(pixel(&image, 20, 20), [190, 192, 200, 255]); + assert_eq!(pixel(&image, 1, 1), [255, 255, 255, 255]); +} diff --git a/crates/prc-render/src/vec3.rs b/crates/prc-render/src/vec3.rs index 8ebf844087..7a673e0055 100644 --- a/crates/prc-render/src/vec3.rs +++ b/crates/prc-render/src/vec3.rs @@ -40,6 +40,17 @@ pub(crate) fn normalize(a: Vec3) -> Option { (l.is_finite() && l > 0.0).then(|| scale(a, 1.0 / l)) } +/// The unit normal of the plane spanned by `a` and `b` (`a × b`, normalized); `None` when they are +/// parallel, zero or not finite. Both are first divided by their largest component, so the cross +/// product of two huge edges cannot overflow before it is normalized. +pub(crate) fn unit_cross(a: Vec3, b: Vec3) -> Option { + let big = a.iter().chain(b.iter()).fold(0.0_f64, |m, c| m.max(c.abs())); + if !(big.is_finite() && big > 0.0) { + return None; + } + normalize(cross(a.map(|c| c / big), b.map(|c| c / big))) +} + #[cfg(test)] mod tests { use super::*; @@ -63,4 +74,12 @@ mod tests { assert_eq!(normalize([1e200, 0.0, 0.0]), Some([1.0, 0.0, 0.0])); assert_eq!(normalize([0.0, -3e-320, 0.0]), Some([0.0, -1.0, 0.0])); } + + #[test] + fn unit_cross_survives_edges_whose_product_overflows() { + assert_eq!(unit_cross([1e160, 0.0, 0.0], [0.0, 1e160, 0.0]), Some([0.0, 0.0, 1.0])); + assert_eq!(unit_cross([1e-170, 0.0, 0.0], [0.0, 1e-170, 0.0]), Some([0.0, 0.0, 1.0])); + assert_eq!(unit_cross([2.0, 0.0, 0.0], [4.0, 0.0, 0.0]), None); + assert_eq!(unit_cross([f64::INFINITY, 0.0, 0.0], [0.0, 1.0, 0.0]), None); + } }