Math primitives cleanup (#13020)

# Objective

- General clenup of the primitives in `bevy_math`
- Add `eccentricity()` to `Ellipse`

## Solution

- Moved `Bounded3d` implementation for `Triangle3d` to the `bounded`
module
- Added `eccentricity()` to `Ellipse`
- `Ellipse::semi_major()` and `::semi_minor()` now accept `&self`
instead of `self`
- `Triangle3d::is_degenerate()` actually uses `f32::EPSILON` as
documented
- Added tests for `Triangle3d`-maths

---------

Co-authored-by: Joona Aalto <jondolf.dev@gmail.com>
Co-authored-by: Miles Silberling-Cook <nth.tensor@gmail.com>
This commit is contained in:
Lynn 2024-04-19 01:45:51 +02:00 committed by GitHub
parent f68bc01544
commit cd80b10d43
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 106 additions and 61 deletions

View File

@ -4,7 +4,7 @@ use crate::{
bounding::{Bounded2d, BoundingCircle},
primitives::{
BoxedPolyline3d, Capsule3d, Cone, ConicalFrustum, Cuboid, Cylinder, InfinitePlane3d,
Line3d, Polyline3d, Segment3d, Sphere, Torus, Triangle2d,
Line3d, Polyline3d, Segment3d, Sphere, Torus, Triangle2d, Triangle3d,
},
Dir3, Mat3, Quat, Vec2, Vec3,
};
@ -303,6 +303,59 @@ impl Bounded3d for Torus {
}
}
impl Bounded3d for Triangle3d {
/// Get the bounding box of the triangle.
fn aabb_3d(&self, translation: Vec3, rotation: Quat) -> Aabb3d {
let [a, b, c] = self.vertices;
let a = rotation * a;
let b = rotation * b;
let c = rotation * c;
let min = a.min(b).min(c);
let max = a.max(b).max(c);
let bounding_center = (max + min) / 2.0 + translation;
let half_extents = (max - min) / 2.0;
Aabb3d::new(bounding_center, half_extents)
}
/// Get the bounding sphere of the triangle.
///
/// 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() {
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))
} else {
None
};
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)
}
}
}
#[cfg(test)]
mod tests {
use glam::{Quat, Vec3};

View File

@ -106,15 +106,27 @@ impl Ellipse {
}
}
#[inline(always)]
/// Returns the [eccentricity](https://en.wikipedia.org/wiki/Eccentricity_(mathematics)) of the ellipse.
/// It can be thought of as a measure of how "stretched" or elongated the ellipse is.
///
/// The value should be in the range [0, 1), where 0 represents a circle, and 1 represents a parabola.
pub fn eccentricity(&self) -> f32 {
let a = self.semi_major();
let b = self.semi_minor();
(a * a - b * b).sqrt() / a
}
/// Returns the length of the semi-major axis. This corresponds to the longest radius of the ellipse.
#[inline(always)]
pub fn semi_major(self) -> f32 {
pub fn semi_major(&self) -> f32 {
self.half_size.max_element()
}
/// Returns the length of the semi-minor axis. This corresponds to the shortest radius of the ellipse.
#[inline(always)]
pub fn semi_minor(self) -> f32 {
pub fn semi_minor(&self) -> f32 {
self.half_size.min_element()
}
@ -839,6 +851,14 @@ mod tests {
fn ellipse_math() {
let ellipse = Ellipse::new(3.0, 1.0);
assert_eq!(ellipse.area(), 9.424778, "incorrect area");
assert_eq!(ellipse.eccentricity(), 0.94280905, "incorrect eccentricity");
let line = Ellipse::new(1., 0.);
assert_eq!(line.eccentricity(), 1., "incorrect line eccentricity");
let circle = Ellipse::new(2., 2.);
assert_eq!(circle.eccentricity(), 0., "incorrect circle eccentricity");
}
#[test]

View File

@ -1,10 +1,7 @@
use std::f32::consts::{FRAC_PI_3, PI};
use super::{Circle, Primitive3d};
use crate::{
bounding::{Aabb3d, Bounded3d, BoundingSphere},
Dir3, InvalidDirectionError, Mat3, Quat, Vec2, Vec3,
};
use crate::{Dir3, InvalidDirectionError, Mat3, Vec2, Vec3};
/// A sphere primitive
#[derive(Clone, Copy, Debug, PartialEq)]
@ -767,7 +764,7 @@ impl Triangle3d {
/// 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 `f32::EPSILON`.
/// 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 {
@ -838,59 +835,6 @@ impl Triangle3d {
}
}
impl Bounded3d for Triangle3d {
/// Get the bounding box of the triangle.
fn aabb_3d(&self, translation: Vec3, rotation: Quat) -> Aabb3d {
let [a, b, c] = self.vertices;
let a = rotation * a;
let b = rotation * b;
let c = rotation * c;
let min = a.min(b).min(c);
let max = a.max(b).max(c);
let bounding_center = (max + min) / 2.0 + translation;
let half_extents = (max - min) / 2.0;
Aabb3d::new(bounding_center, half_extents)
}
/// Get the bounding sphere of the triangle.
///
/// 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() {
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))
} else {
None
};
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)
}
}
}
/// A tetrahedron primitive.
#[derive(Clone, Copy, Debug, PartialEq)]
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
@ -976,6 +920,7 @@ mod tests {
// Reference values were computed by hand and/or with external tools
use super::*;
use crate::Quat;
use approx::assert_relative_eq;
#[test]
@ -1174,4 +1119,31 @@ mod tests {
);
assert_relative_eq!(Tetrahedron::default().centroid(), Vec3::ZERO);
}
#[test]
fn triangle_math() {
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);
assert!(!triangle.is_degenerate(), "incorrectly found degenerate");
assert_eq!(triangle.area(), 3.0233467, "incorrect area");
assert_eq!(triangle.perimeter(), 9.832292, "incorrect perimeter");
assert_eq!(
triangle.circumcenter(),
Vec3::new(-1., 1.75, 0.75),
"incorrect circumcenter"
);
assert_eq!(
triangle.normal(),
Ok(Dir3::new_unchecked(Vec3::new(
-0.04134491,
-0.4134491,
0.90958804
))),
"incorrect normal"
);
let degenerate = Triangle3d::new(Vec3::NEG_ONE, Vec3::ZERO, Vec3::ONE);
assert!(degenerate.is_degenerate(), "did not find degenerate");
}
}