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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqmHYx8FiWM5PcsoiErgp9
This commit is contained in:
KenM76andClaude Opus 5.5 committed 2026-11-03 23:06:54 -05:00
1 parent 4e2e77a986
commit ca21d4fb7b
3 files changed
+29 -2

No files matched your search

+2 -2
View File
@@ -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);
+8
View File
@@ -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]);
}
+19
View File
@@ -40,6 +40,17 @@ pub(crate) fn normalize(a: Vec3) -> Option<Vec3> {
(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<Vec3> {
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);
}
}