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 <vitorfhc@protonmail.com>
Co-authored-by: Ben Harper <ben@tukom.org>
This commit is contained in:
Ben Harper 2024-05-24 01:03:00 +10:00 committed by GitHub
parent 1d950e6195
commit bd5148e0f5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 301 additions and 40 deletions

View File

@ -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"
);
}
}

View File

@ -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]

View File

@ -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");
}
// 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"
);
#[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 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 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");
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"
);
}
}