From bd5148e0f5eb322c86c8a06a97c50d40385ecdd0 Mon Sep 17 00:00:00 2001 From: Ben Harper Date: Fri, 24 May 2024 01:03:00 +1000 Subject: [PATCH] Add triangle_math tests and fix Triangle3d::bounding_sphere bug (#13467) # Objective Adopted #12659. Resolved the merge conflicts on #12659; * I merged the `triangle_tests` added by this PR and by #13020. * I moved the [commented out code](https://github.com/bevyengine/bevy/pull/12659#discussion_r1536640427) from the original PR into a separate test with `#[should_panic]`. --------- Co-authored-by: Vitor Falcao Co-authored-by: Ben Harper --- .../src/bounding/bounded3d/primitive_impls.rs | 94 +++++++-- crates/bevy_math/src/primitives/dim2.rs | 62 ++++++ crates/bevy_math/src/primitives/dim3.rs | 185 ++++++++++++++++-- 3 files changed, 301 insertions(+), 40 deletions(-) diff --git a/crates/bevy_math/src/bounding/bounded3d/primitive_impls.rs b/crates/bevy_math/src/bounding/bounded3d/primitive_impls.rs index 4c82b0f387321..d11f8b9bbe5f8 100644 --- a/crates/bevy_math/src/bounding/bounded3d/primitive_impls.rs +++ b/crates/bevy_math/src/bounding/bounded3d/primitive_impls.rs @@ -323,29 +323,15 @@ impl Bounded3d for Triangle3d { /// The [`Triangle3d`] implements the minimal bounding sphere calculation. For acute triangles, the circumcenter is used as /// the center of the sphere. For the others, the bounding sphere is the minimal sphere /// that contains the largest side of the triangle. - fn bounding_sphere(&self, translation: Vec3, rotation: Quat) -> BoundingSphere { - if self.is_degenerate() { + fn bounding_sphere(&self, translation: Vec3, _rotation: Quat) -> BoundingSphere { + if self.is_degenerate() || self.is_obtuse() { let (p1, p2) = self.largest_side(); - let (segment, _) = Segment3d::from_points(p1, p2); - return segment.bounding_sphere(translation, rotation); - } - - let [a, b, c] = self.vertices; - - let side_opposite_to_non_acute = if (b - a).dot(c - a) <= 0.0 { - Some((b, c)) - } else if (c - b).dot(a - b) <= 0.0 { - Some((c, a)) - } else if (a - c).dot(b - c) <= 0.0 { - Some((a, b)) + let mid_point = (p1 + p2) / 2.0; + let radius = mid_point.distance(p1); + BoundingSphere::new(mid_point + translation, radius) } else { - None - }; + let [a, _, _] = self.vertices; - if let Some((p1, p2)) = side_opposite_to_non_acute { - let (segment, _) = Segment3d::from_points(p1, p2); - segment.bounding_sphere(translation, rotation) - } else { let circumcenter = self.circumcenter(); let radius = circumcenter.distance(a); BoundingSphere::new(circumcenter + translation, radius) @@ -355,13 +341,14 @@ impl Bounded3d for Triangle3d { #[cfg(test)] mod tests { + use crate::bounding::BoundingVolume; use glam::{Quat, Vec3, Vec3A}; use crate::{ bounding::Bounded3d, primitives::{ Capsule3d, Cone, ConicalFrustum, Cuboid, Cylinder, InfinitePlane3d, Line3d, Polyline3d, - Segment3d, Sphere, Torus, + Segment3d, Sphere, Torus, Triangle3d, }, Dir3, }; @@ -607,4 +594,69 @@ mod tests { assert_eq!(bounding_sphere.center, translation.into()); assert_eq!(bounding_sphere.radius(), 1.5); } + + #[test] + fn triangle3d() { + let zero_degenerate_triangle = Triangle3d::new(Vec3::ZERO, Vec3::ZERO, Vec3::ZERO); + + let br = zero_degenerate_triangle.aabb_3d(Vec3::ZERO, Quat::IDENTITY); + assert_eq!( + br.center(), + Vec3::ZERO.into(), + "incorrect bounding box center" + ); + assert_eq!( + br.half_size(), + Vec3::ZERO.into(), + "incorrect bounding box half extents" + ); + + let bs = zero_degenerate_triangle.bounding_sphere(Vec3::ZERO, Quat::IDENTITY); + assert_eq!( + bs.center, + Vec3::ZERO.into(), + "incorrect bounding sphere center" + ); + assert_eq!(bs.sphere.radius, 0.0, "incorrect bounding sphere radius"); + + let dup_degenerate_triangle = Triangle3d::new(Vec3::ZERO, Vec3::X, Vec3::X); + let bs = dup_degenerate_triangle.bounding_sphere(Vec3::ZERO, Quat::IDENTITY); + assert_eq!( + bs.center, + Vec3::new(0.5, 0.0, 0.0).into(), + "incorrect bounding sphere center" + ); + assert_eq!(bs.sphere.radius, 0.5, "incorrect bounding sphere radius"); + let br = dup_degenerate_triangle.aabb_3d(Vec3::ZERO, Quat::IDENTITY); + assert_eq!( + br.center(), + Vec3::new(0.5, 0.0, 0.0).into(), + "incorrect bounding box center" + ); + assert_eq!( + br.half_size(), + Vec3::new(0.5, 0.0, 0.0).into(), + "incorrect bounding box half extents" + ); + + let collinear_degenerate_triangle = Triangle3d::new(Vec3::NEG_X, Vec3::ZERO, Vec3::X); + let bs = collinear_degenerate_triangle.bounding_sphere(Vec3::ZERO, Quat::IDENTITY); + assert_eq!( + bs.center, + Vec3::ZERO.into(), + "incorrect bounding sphere center" + ); + assert_eq!(bs.sphere.radius, 1.0, "incorrect bounding sphere radius"); + let br = collinear_degenerate_triangle.aabb_3d(Vec3::ZERO, Quat::IDENTITY); + assert_eq!( + br.center(), + Vec3::ZERO.into(), + "incorrect bounding box center" + ); + assert_eq!( + br.half_size(), + Vec3::new(1.0, 0.0, 0.0).into(), + "incorrect bounding box half extents" + ); + } } diff --git a/crates/bevy_math/src/primitives/dim2.rs b/crates/bevy_math/src/primitives/dim2.rs index d7c47e65dc98f..61725ab1b94e3 100644 --- a/crates/bevy_math/src/primitives/dim2.rs +++ b/crates/bevy_math/src/primitives/dim2.rs @@ -527,6 +527,54 @@ impl Triangle2d { (Circle { radius }, center) } + /// Checks if the triangle is degenerate, meaning it has zero area. + /// + /// A triangle is degenerate if the cross product of the vectors `ab` and `ac` has a length less than `10e-7`. + /// This indicates that the three vertices are collinear or nearly collinear. + #[inline(always)] + pub fn is_degenerate(&self) -> bool { + let [a, b, c] = self.vertices; + let ab = (b - a).extend(0.); + let ac = (c - a).extend(0.); + ab.cross(ac).length() < 10e-7 + } + + /// Checks if the triangle is acute, meaning all angles are less than 90 degrees + #[inline(always)] + pub fn is_acute(&self) -> bool { + let [a, b, c] = self.vertices; + let ab = b - a; + let bc = c - b; + let ca = a - c; + + // a^2 + b^2 < c^2 for an acute triangle + let mut side_lengths = [ + ab.length_squared(), + bc.length_squared(), + ca.length_squared(), + ]; + side_lengths.sort_by(|a, b| a.partial_cmp(b).unwrap()); + side_lengths[0] + side_lengths[1] > side_lengths[2] + } + + /// Checks if the triangle is obtuse, meaning one angle is greater than 90 degrees + #[inline(always)] + pub fn is_obtuse(&self) -> bool { + let [a, b, c] = self.vertices; + let ab = b - a; + let bc = c - b; + let ca = a - c; + + // a^2 + b^2 > c^2 for an obtuse triangle + let mut side_lengths = [ + ab.length_squared(), + bc.length_squared(), + ca.length_squared(), + ]; + side_lengths.sort_by(|a, b| a.partial_cmp(b).unwrap()); + side_lengths[0] + side_lengths[1] < side_lengths[2] + } + /// Reverse the [`WindingOrder`] of the triangle /// by swapping the first and last vertices. #[inline(always)] @@ -975,6 +1023,20 @@ mod tests { ); assert_eq!(triangle.area(), 21.0, "incorrect area"); assert_eq!(triangle.perimeter(), 22.097439, "incorrect perimeter"); + + let degenerate_triangle = + Triangle2d::new(Vec2::new(-1., 0.), Vec2::new(0., 0.), Vec2::new(1., 0.)); + assert!(degenerate_triangle.is_degenerate()); + + let acute_triangle = + Triangle2d::new(Vec2::new(-1., 0.), Vec2::new(1., 0.), Vec2::new(0., 5.)); + let obtuse_triangle = + Triangle2d::new(Vec2::new(-1., 0.), Vec2::new(1., 0.), Vec2::new(0., 0.5)); + + assert!(acute_triangle.is_acute()); + assert!(!acute_triangle.is_obtuse()); + assert!(!obtuse_triangle.is_acute()); + assert!(obtuse_triangle.is_obtuse()); } #[test] diff --git a/crates/bevy_math/src/primitives/dim3.rs b/crates/bevy_math/src/primitives/dim3.rs index 1c2e217ede3f3..2ad71c689d011 100644 --- a/crates/bevy_math/src/primitives/dim3.rs +++ b/crates/bevy_math/src/primitives/dim3.rs @@ -780,6 +780,42 @@ impl Triangle3d { ab.cross(ac).length() < 10e-7 } + /// Checks if the triangle is acute, meaning all angles are less than 90 degrees + #[inline(always)] + pub fn is_acute(&self) -> bool { + let [a, b, c] = self.vertices; + let ab = b - a; + let bc = c - b; + let ca = a - c; + + // a^2 + b^2 < c^2 for an acute triangle + let mut side_lengths = [ + ab.length_squared(), + bc.length_squared(), + ca.length_squared(), + ]; + side_lengths.sort_by(|a, b| a.partial_cmp(b).unwrap()); + side_lengths[0] + side_lengths[1] > side_lengths[2] + } + + /// Checks if the triangle is obtuse, meaning one angle is greater than 90 degrees + #[inline(always)] + pub fn is_obtuse(&self) -> bool { + let [a, b, c] = self.vertices; + let ab = b - a; + let bc = c - b; + let ca = a - c; + + // a^2 + b^2 > c^2 for an obtuse triangle + let mut side_lengths = [ + ab.length_squared(), + bc.length_squared(), + ca.length_squared(), + ]; + side_lengths.sort_by(|a, b| a.partial_cmp(b).unwrap()); + side_lengths[0] + side_lengths[1] < side_lengths[2] + } + /// Reverse the triangle by swapping the first and last vertices. #[inline(always)] pub fn reverse(&mut self) { @@ -1010,7 +1046,7 @@ mod tests { // Reference values were computed by hand and/or with external tools use super::*; - use crate::Quat; + use crate::{InvalidDirectionError, Quat}; use approx::assert_relative_eq; #[test] @@ -1210,8 +1246,91 @@ mod tests { assert_relative_eq!(Tetrahedron::default().centroid(), Vec3::ZERO); } + #[test] + fn extrusion_math() { + let circle = Circle::new(0.75); + let cylinder = Extrusion::new(circle, 2.5); + assert_eq!(cylinder.area(), 15.315264, "incorrect surface area"); + assert_eq!(cylinder.volume(), 4.417865, "incorrect volume"); + + let annulus = crate::primitives::Annulus::new(0.25, 1.375); + let tube = Extrusion::new(annulus, 0.333); + assert_eq!(tube.area(), 14.886437, "incorrect surface area"); + assert_eq!(tube.volume(), 1.9124937, "incorrect volume"); + + let polygon = crate::primitives::RegularPolygon::new(3.8, 7); + let regular_prism = Extrusion::new(polygon, 1.25); + assert_eq!(regular_prism.area(), 107.8808, "incorrect surface area"); + assert_eq!(regular_prism.volume(), 49.392204, "incorrect volume"); + } + #[test] fn triangle_math() { + // Default triangle tests + let mut default_triangle = Triangle3d::default(); + let reverse_default_triangle = Triangle3d::new( + Vec3::new(0.5, -0.5, 0.0), + Vec3::new(-0.5, -0.5, 0.0), + Vec3::new(0.0, 0.5, 0.0), + ); + assert_eq!(default_triangle.area(), 0.5, "incorrect area"); + assert_relative_eq!( + default_triangle.perimeter(), + 1.0 + 2.0 * 1.25_f32.sqrt(), + epsilon = 10e-9 + ); + assert_eq!(default_triangle.normal(), Ok(Dir3::Z), "incorrect normal"); + assert!( + !default_triangle.is_degenerate(), + "incorrect degenerate check" + ); + assert_eq!( + default_triangle.centroid(), + Vec3::new(0.0, -0.16666667, 0.0), + "incorrect centroid" + ); + assert_eq!( + default_triangle.largest_side(), + (Vec3::new(0.0, 0.5, 0.0), Vec3::new(-0.5, -0.5, 0.0)) + ); + default_triangle.reverse(); + assert_eq!( + default_triangle, reverse_default_triangle, + "incorrect reverse" + ); + assert_eq!( + default_triangle.circumcenter(), + Vec3::new(0.0, -0.125, 0.0), + "incorrect circumcenter" + ); + + // Custom triangle tests + let right_triangle = Triangle3d::new(Vec3::ZERO, Vec3::X, Vec3::Y); + let obtuse_triangle = Triangle3d::new(Vec3::NEG_X, Vec3::X, Vec3::new(0.0, 0.1, 0.0)); + let acute_triangle = Triangle3d::new(Vec3::ZERO, Vec3::X, Vec3::new(0.5, 5.0, 0.0)); + + assert_eq!( + right_triangle.circumcenter(), + Vec3::new(0.5, 0.5, 0.0), + "incorrect circumcenter" + ); + assert_eq!( + obtuse_triangle.circumcenter(), + Vec3::new(0.0, -4.95, 0.0), + "incorrect circumcenter" + ); + assert_eq!( + acute_triangle.circumcenter(), + Vec3::new(0.5, 2.475, 0.0), + "incorrect circumcenter" + ); + + assert!(acute_triangle.is_acute()); + assert!(!acute_triangle.is_obtuse()); + assert!(!obtuse_triangle.is_acute()); + assert!(obtuse_triangle.is_obtuse()); + + // Arbitrary triangle tests let [a, b, c] = [Vec3::ZERO, Vec3::new(1., 1., 0.5), Vec3::new(-3., 2.5, 1.)]; let triangle = Triangle3d::new(a, b, c); @@ -1233,25 +1352,53 @@ mod tests { "incorrect normal" ); - let degenerate = Triangle3d::new(Vec3::NEG_ONE, Vec3::ZERO, Vec3::ONE); - assert!(degenerate.is_degenerate(), "did not find degenerate"); - } - - #[test] - fn extrusion_math() { - let circle = Circle::new(0.75); - let cylinder = Extrusion::new(circle, 2.5); - assert_eq!(cylinder.area(), 15.315264, "incorrect surface area"); - assert_eq!(cylinder.volume(), 4.417865, "incorrect volume"); + // Degenerate triangle tests + let zero_degenerate_triangle = Triangle3d::new(Vec3::ZERO, Vec3::ZERO, Vec3::ZERO); + assert!( + zero_degenerate_triangle.is_degenerate(), + "incorrect degenerate check" + ); + assert_eq!( + zero_degenerate_triangle.normal(), + Err(InvalidDirectionError::Zero), + "incorrect normal" + ); + assert_eq!( + zero_degenerate_triangle.largest_side(), + (Vec3::ZERO, Vec3::ZERO), + "incorrect largest side" + ); - let annulus = crate::primitives::Annulus::new(0.25, 1.375); - let tube = Extrusion::new(annulus, 0.333); - assert_eq!(tube.area(), 14.886437, "incorrect surface area"); - assert_eq!(tube.volume(), 1.9124937, "incorrect volume"); + let dup_degenerate_triangle = Triangle3d::new(Vec3::ZERO, Vec3::X, Vec3::X); + assert!( + dup_degenerate_triangle.is_degenerate(), + "incorrect degenerate check" + ); + assert_eq!( + dup_degenerate_triangle.normal(), + Err(InvalidDirectionError::Zero), + "incorrect normal" + ); + assert_eq!( + dup_degenerate_triangle.largest_side(), + (Vec3::ZERO, Vec3::X), + "incorrect largest side" + ); - let polygon = crate::primitives::RegularPolygon::new(3.8, 7); - let regular_prism = Extrusion::new(polygon, 1.25); - assert_eq!(regular_prism.area(), 107.8808, "incorrect surface area"); - assert_eq!(regular_prism.volume(), 49.392204, "incorrect volume"); + let collinear_degenerate_triangle = Triangle3d::new(Vec3::NEG_X, Vec3::ZERO, Vec3::X); + assert!( + collinear_degenerate_triangle.is_degenerate(), + "incorrect degenerate check" + ); + assert_eq!( + collinear_degenerate_triangle.normal(), + Err(InvalidDirectionError::Zero), + "incorrect normal" + ); + assert_eq!( + collinear_degenerate_triangle.largest_side(), + (Vec3::NEG_X, Vec3::X), + "incorrect largest side" + ); } }