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:
parent
f68bc01544
commit
cd80b10d43
@ -4,7 +4,7 @@ use crate::{
|
|||||||
bounding::{Bounded2d, BoundingCircle},
|
bounding::{Bounded2d, BoundingCircle},
|
||||||
primitives::{
|
primitives::{
|
||||||
BoxedPolyline3d, Capsule3d, Cone, ConicalFrustum, Cuboid, Cylinder, InfinitePlane3d,
|
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,
|
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)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use glam::{Quat, Vec3};
|
use glam::{Quat, Vec3};
|
||||||
|
@ -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.
|
/// Returns the length of the semi-major axis. This corresponds to the longest radius of the ellipse.
|
||||||
#[inline(always)]
|
#[inline(always)]
|
||||||
pub fn semi_major(self) -> f32 {
|
pub fn semi_major(&self) -> f32 {
|
||||||
self.half_size.max_element()
|
self.half_size.max_element()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the length of the semi-minor axis. This corresponds to the shortest radius of the ellipse.
|
/// Returns the length of the semi-minor axis. This corresponds to the shortest radius of the ellipse.
|
||||||
#[inline(always)]
|
#[inline(always)]
|
||||||
pub fn semi_minor(self) -> f32 {
|
pub fn semi_minor(&self) -> f32 {
|
||||||
self.half_size.min_element()
|
self.half_size.min_element()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -839,6 +851,14 @@ mod tests {
|
|||||||
fn ellipse_math() {
|
fn ellipse_math() {
|
||||||
let ellipse = Ellipse::new(3.0, 1.0);
|
let ellipse = Ellipse::new(3.0, 1.0);
|
||||||
assert_eq!(ellipse.area(), 9.424778, "incorrect area");
|
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]
|
#[test]
|
||||||
|
@ -1,10 +1,7 @@
|
|||||||
use std::f32::consts::{FRAC_PI_3, PI};
|
use std::f32::consts::{FRAC_PI_3, PI};
|
||||||
|
|
||||||
use super::{Circle, Primitive3d};
|
use super::{Circle, Primitive3d};
|
||||||
use crate::{
|
use crate::{Dir3, InvalidDirectionError, Mat3, Vec2, Vec3};
|
||||||
bounding::{Aabb3d, Bounded3d, BoundingSphere},
|
|
||||||
Dir3, InvalidDirectionError, Mat3, Quat, Vec2, Vec3,
|
|
||||||
};
|
|
||||||
|
|
||||||
/// A sphere primitive
|
/// A sphere primitive
|
||||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||||
@ -767,7 +764,7 @@ impl Triangle3d {
|
|||||||
|
|
||||||
/// Checks if the triangle is degenerate, meaning it has zero area.
|
/// 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.
|
/// This indicates that the three vertices are collinear or nearly collinear.
|
||||||
#[inline(always)]
|
#[inline(always)]
|
||||||
pub fn is_degenerate(&self) -> bool {
|
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.
|
/// A tetrahedron primitive.
|
||||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||||
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
|
#[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
|
// Reference values were computed by hand and/or with external tools
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
use crate::Quat;
|
||||||
use approx::assert_relative_eq;
|
use approx::assert_relative_eq;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@ -1174,4 +1119,31 @@ mod tests {
|
|||||||
);
|
);
|
||||||
assert_relative_eq!(Tetrahedron::default().centroid(), Vec3::ZERO);
|
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");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user