Add Triangle3d primitive to bevy_math::primitives (#12508)
# Context [GitHub Discussion Link](https://github.com/bevyengine/bevy/discussions/12506) # Objective - **Clarity:** More explicit representation of a common geometric primitive. - **Convenience:** Provide methods tailored to 3D triangles (area, perimeters, etc.). ## Solution - Adding the `Triangle3d` primitive into the `bevy_math` crate. --- ## Changelog ### Added - `Triangle3d` primitive to the `bevy_math` crate ### Changed - `Triangle2d::reverse`: the first and last vertices are swapped instead of the second and third. --------- Co-authored-by: Miles Silberling-Cook <NthTensor@users.noreply.github.com> Co-authored-by: Joona Aalto <jondolf.dev@gmail.com>
This commit is contained in:
parent
e33b93e312
commit
c9ec95d782
@ -376,10 +376,10 @@ impl Triangle2d {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Reverse the [`WindingOrder`] of the triangle
|
/// Reverse the [`WindingOrder`] of the triangle
|
||||||
/// by swapping the second and third vertices
|
/// by swapping the first and last vertices
|
||||||
#[inline(always)]
|
#[inline(always)]
|
||||||
pub fn reverse(&mut self) {
|
pub fn reverse(&mut self) {
|
||||||
self.vertices.swap(1, 2);
|
self.vertices.swap(0, 2);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -753,9 +753,9 @@ mod tests {
|
|||||||
assert_eq!(cw_triangle.winding_order(), WindingOrder::Clockwise);
|
assert_eq!(cw_triangle.winding_order(), WindingOrder::Clockwise);
|
||||||
|
|
||||||
let ccw_triangle = Triangle2d::new(
|
let ccw_triangle = Triangle2d::new(
|
||||||
Vec2::new(0.0, 2.0),
|
|
||||||
Vec2::new(-1.0, -1.0),
|
Vec2::new(-1.0, -1.0),
|
||||||
Vec2::new(-0.5, -1.2),
|
Vec2::new(-0.5, -1.2),
|
||||||
|
Vec2::new(0.0, 2.0),
|
||||||
);
|
);
|
||||||
assert_eq!(ccw_triangle.winding_order(), WindingOrder::CounterClockwise);
|
assert_eq!(ccw_triangle.winding_order(), WindingOrder::CounterClockwise);
|
||||||
|
|
||||||
|
|||||||
@ -1,7 +1,10 @@
|
|||||||
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::{Dir3, Vec3};
|
use crate::{
|
||||||
|
bounding::{Aabb3d, Bounded3d, BoundingSphere},
|
||||||
|
Dir3, InvalidDirectionError, Quat, Vec3,
|
||||||
|
};
|
||||||
|
|
||||||
/// A sphere primitive
|
/// A sphere primitive
|
||||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||||
@ -629,6 +632,197 @@ impl Torus {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A 3D triangle primitive.
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||||
|
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
|
||||||
|
pub struct Triangle3d {
|
||||||
|
/// The vertices of the triangle.
|
||||||
|
pub vertices: [Vec3; 3],
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Primitive3d for Triangle3d {}
|
||||||
|
|
||||||
|
impl Default for Triangle3d {
|
||||||
|
/// Returns the default [`Triangle3d`] with the vertices `[0.0, 0.5, 0.0]`, `[-0.5, -0.5, 0.0]`, and `[0.5, -0.5, 0.0]`.
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
vertices: [
|
||||||
|
Vec3::new(0.0, 0.5, 0.0),
|
||||||
|
Vec3::new(-0.5, -0.5, 0.0),
|
||||||
|
Vec3::new(0.5, -0.5, 0.0),
|
||||||
|
],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Triangle3d {
|
||||||
|
/// Create a new [`Triangle3d`] from points `a`, `b`, and `c`.
|
||||||
|
#[inline(always)]
|
||||||
|
pub fn new(a: Vec3, b: Vec3, c: Vec3) -> Self {
|
||||||
|
Self {
|
||||||
|
vertices: [a, b, c],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the area of the triangle.
|
||||||
|
#[inline(always)]
|
||||||
|
pub fn area(&self) -> f32 {
|
||||||
|
let [a, b, c] = self.vertices;
|
||||||
|
let ab = b - a;
|
||||||
|
let ac = c - a;
|
||||||
|
ab.cross(ac).length() / 2.0
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the perimeter of the triangle.
|
||||||
|
#[inline(always)]
|
||||||
|
pub fn perimeter(&self) -> f32 {
|
||||||
|
let [a, b, c] = self.vertices;
|
||||||
|
a.distance(b) + b.distance(c) + c.distance(a)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the normal of the triangle in the direction of the right-hand rule, assuming
|
||||||
|
/// the vertices are ordered in a counter-clockwise direction.
|
||||||
|
///
|
||||||
|
/// The normal is computed as the cross product of the vectors `ab` and `ac`.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Returns [`Err(InvalidDirectionError)`](InvalidDirectionError) if the length
|
||||||
|
/// of the given vector is zero (or very close to zero), infinite, or `NaN`.
|
||||||
|
#[inline(always)]
|
||||||
|
pub fn normal(&self) -> Result<Dir3, InvalidDirectionError> {
|
||||||
|
let [a, b, c] = self.vertices;
|
||||||
|
let ab = b - a;
|
||||||
|
let ac = c - a;
|
||||||
|
Dir3::new(ab.cross(ac))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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`.
|
||||||
|
/// 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;
|
||||||
|
let ac = c - a;
|
||||||
|
ab.cross(ac).length() < 10e-7
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Reverse the triangle by swapping the first and last vertices.
|
||||||
|
#[inline(always)]
|
||||||
|
pub fn reverse(&mut self) {
|
||||||
|
self.vertices.swap(0, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the centroid of the triangle.
|
||||||
|
///
|
||||||
|
/// This function finds the geometric center of the triangle by averaging the vertices:
|
||||||
|
/// `centroid = (a + b + c) / 3`.
|
||||||
|
#[doc(alias("center", "barycenter", "baricenter"))]
|
||||||
|
#[inline(always)]
|
||||||
|
pub fn centroid(&self) -> Vec3 {
|
||||||
|
(self.vertices[0] + self.vertices[1] + self.vertices[2]) / 3.0
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the largest side of the triangle.
|
||||||
|
///
|
||||||
|
/// Returns the two points that form the largest side of the triangle.
|
||||||
|
#[inline(always)]
|
||||||
|
pub fn largest_side(&self) -> (Vec3, Vec3) {
|
||||||
|
let [a, b, c] = self.vertices;
|
||||||
|
let ab = b - a;
|
||||||
|
let bc = c - b;
|
||||||
|
let ca = a - c;
|
||||||
|
|
||||||
|
let mut largest_side_points = (a, b);
|
||||||
|
let mut largest_side_length = ab.length();
|
||||||
|
|
||||||
|
if bc.length() > largest_side_length {
|
||||||
|
largest_side_points = (b, c);
|
||||||
|
largest_side_length = bc.length();
|
||||||
|
}
|
||||||
|
|
||||||
|
if ca.length() > largest_side_length {
|
||||||
|
largest_side_points = (a, c);
|
||||||
|
}
|
||||||
|
|
||||||
|
largest_side_points
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the circumcenter of the triangle.
|
||||||
|
#[inline(always)]
|
||||||
|
pub fn circumcenter(&self) -> Vec3 {
|
||||||
|
if self.is_degenerate() {
|
||||||
|
// If the triangle is degenerate, the circumcenter is the midpoint of the largest side.
|
||||||
|
let (p1, p2) = self.largest_side();
|
||||||
|
return (p1 + p2) / 2.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
let [a, b, c] = self.vertices;
|
||||||
|
let ab = b - a;
|
||||||
|
let ac = c - a;
|
||||||
|
let n = ab.cross(ac);
|
||||||
|
|
||||||
|
// Reference: https://gamedev.stackexchange.com/questions/60630/how-do-i-find-the-circumcenter-of-a-triangle-in-3d
|
||||||
|
a + ((ac.length_squared() * n.cross(ab) + ab.length_squared() * ac.cross(ab).cross(ac))
|
||||||
|
/ (2.0 * n.length_squared()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
// Reference values were computed by hand and/or with external tools
|
// Reference values were computed by hand and/or with external tools
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user