Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Math primitives cleanup #13020

Merged
merged 12 commits into from
Apr 19, 2024
55 changes: 54 additions & 1 deletion crates/bevy_math/src/bounding/bounded3d/primitive_impls.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};
Expand Down Expand Up @@ -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};
Expand Down
24 changes: 22 additions & 2 deletions crates/bevy_math/src/primitives/dim2.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
lynn-lumen marked this conversation as resolved.
Show resolved Hide resolved
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()
}

Expand Down Expand Up @@ -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]
Expand Down
88 changes: 30 additions & 58 deletions crates/bevy_math/src/primitives/dim3.rs
Original file line number Diff line number Diff line change
@@ -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)]
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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))]
Expand Down Expand Up @@ -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]
Expand Down Expand Up @@ -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");
}
}