Add Capsule2d
primitive (#11585)
# Objective Currently, the `Capsule` primitive is technically dimension-agnostic in that it implements both `Primitive2d` and `Primitive3d`. This seems good on paper, but it can often be useful to have separate 2D and 3D versions of primitives. For example, one might want a two-dimensional capsule mesh. We can't really implement both 2D and 3D meshing for the same type using the upcoming `Meshable` trait (see #11431). We also currently don't implement `Bounded2d` for `Capsule`, see https://github.com/bevyengine/bevy/pull/11336#issuecomment-1890797788. Having 2D and 3D separate at a type level is more explicit, and also more consistent with the existing primitives, as there are no other types that implement both `Primitive2d` and `Primitive3d` at the same time. ## Solution Rename `Capsule` to `Capsule3d` and add `Capsule2d`. `Capsule2d` implements `Bounded2d`. For now, I went for `Capsule2d` for the sake of consistency and clarity. Mathematically the more accurate term would be `Stadium` or `Pill` (see [Wikipedia](https://en.wikipedia.org/wiki/Stadium_(geometry))), but those might be less obvious to game devs. For reference, Godot has [`CapsuleShape2D`](https://docs.godotengine.org/en/stable/classes/class_capsuleshape2d.html). I can rename it if others think the geometrically correct name is better though. --- ## Changelog - Renamed `Capsule` to `Capsule3d` - Added `Capsule2d` with `Bounded2d` implemented --------- Co-authored-by: Alice Cecile <alice.i.cecile@gmail.com>
This commit is contained in:
parent
1bc293f33a
commit
a9f061e909
@ -3,8 +3,8 @@
|
|||||||
use glam::{Mat2, Vec2};
|
use glam::{Mat2, Vec2};
|
||||||
|
|
||||||
use crate::primitives::{
|
use crate::primitives::{
|
||||||
BoxedPolygon, BoxedPolyline2d, Circle, Ellipse, Line2d, Plane2d, Polygon, Polyline2d,
|
BoxedPolygon, BoxedPolyline2d, Capsule2d, Circle, Direction2d, Ellipse, Line2d, Plane2d,
|
||||||
Rectangle, RegularPolygon, Segment2d, Triangle2d,
|
Polygon, Polyline2d, Rectangle, RegularPolygon, Segment2d, Triangle2d,
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::{Aabb2d, Bounded2d, BoundingCircle};
|
use super::{Aabb2d, Bounded2d, BoundingCircle};
|
||||||
@ -230,6 +230,31 @@ impl Bounded2d for RegularPolygon {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Bounded2d for Capsule2d {
|
||||||
|
fn aabb_2d(&self, translation: Vec2, rotation: f32) -> Aabb2d {
|
||||||
|
// Get the line segment between the hemicircles of the rotated capsule
|
||||||
|
let segment = Segment2d {
|
||||||
|
// Multiplying a normalized vector (Vec2::Y) with a rotation returns a normalized vector.
|
||||||
|
direction: Direction2d::new_unchecked(Mat2::from_angle(rotation) * Vec2::Y),
|
||||||
|
half_length: self.half_length,
|
||||||
|
};
|
||||||
|
let (a, b) = (segment.point1(), segment.point2());
|
||||||
|
|
||||||
|
// Expand the line segment by the capsule radius to get the capsule half-extents
|
||||||
|
let min = a.min(b) - Vec2::splat(self.radius);
|
||||||
|
let max = a.max(b) + Vec2::splat(self.radius);
|
||||||
|
|
||||||
|
Aabb2d {
|
||||||
|
min: min + translation,
|
||||||
|
max: max + translation,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn bounding_circle(&self, translation: Vec2, _rotation: f32) -> BoundingCircle {
|
||||||
|
BoundingCircle::new(translation, self.radius + self.half_length)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use glam::Vec2;
|
use glam::Vec2;
|
||||||
@ -237,8 +262,8 @@ mod tests {
|
|||||||
use crate::{
|
use crate::{
|
||||||
bounding::Bounded2d,
|
bounding::Bounded2d,
|
||||||
primitives::{
|
primitives::{
|
||||||
Circle, Direction2d, Ellipse, Line2d, Plane2d, Polygon, Polyline2d, Rectangle,
|
Capsule2d, Circle, Direction2d, Ellipse, Line2d, Plane2d, Polygon, Polyline2d,
|
||||||
RegularPolygon, Segment2d, Triangle2d,
|
Rectangle, RegularPolygon, Segment2d, Triangle2d,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -440,4 +465,18 @@ mod tests {
|
|||||||
assert_eq!(bounding_circle.center, translation);
|
assert_eq!(bounding_circle.center, translation);
|
||||||
assert_eq!(bounding_circle.radius(), 1.0);
|
assert_eq!(bounding_circle.radius(), 1.0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn capsule() {
|
||||||
|
let capsule = Capsule2d::new(0.5, 2.0);
|
||||||
|
let translation = Vec2::new(2.0, 1.0);
|
||||||
|
|
||||||
|
let aabb = capsule.aabb_2d(translation, 0.0);
|
||||||
|
assert_eq!(aabb.min, translation - Vec2::new(0.5, 1.5));
|
||||||
|
assert_eq!(aabb.max, translation + Vec2::new(0.5, 1.5));
|
||||||
|
|
||||||
|
let bounding_circle = capsule.bounding_circle(translation, 0.0);
|
||||||
|
assert_eq!(bounding_circle.center, translation);
|
||||||
|
assert_eq!(bounding_circle.radius(), 1.5);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -5,7 +5,7 @@ use glam::{Mat3, Quat, Vec2, Vec3};
|
|||||||
use crate::{
|
use crate::{
|
||||||
bounding::{Bounded2d, BoundingCircle},
|
bounding::{Bounded2d, BoundingCircle},
|
||||||
primitives::{
|
primitives::{
|
||||||
BoxedPolyline3d, Capsule, Cone, ConicalFrustum, Cuboid, Cylinder, Direction3d, Line3d,
|
BoxedPolyline3d, Capsule3d, Cone, ConicalFrustum, Cuboid, Cylinder, Direction3d, Line3d,
|
||||||
Plane3d, Polyline3d, Segment3d, Sphere, Torus, Triangle2d,
|
Plane3d, Polyline3d, Segment3d, Sphere, Torus, Triangle2d,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@ -146,7 +146,7 @@ impl Bounded3d for Cylinder {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Bounded3d for Capsule {
|
impl Bounded3d for Capsule3d {
|
||||||
fn aabb_3d(&self, translation: Vec3, rotation: Quat) -> Aabb3d {
|
fn aabb_3d(&self, translation: Vec3, rotation: Quat) -> Aabb3d {
|
||||||
// Get the line segment between the hemispheres of the rotated capsule
|
// Get the line segment between the hemispheres of the rotated capsule
|
||||||
let segment = Segment3d {
|
let segment = Segment3d {
|
||||||
@ -311,7 +311,7 @@ mod tests {
|
|||||||
use crate::{
|
use crate::{
|
||||||
bounding::Bounded3d,
|
bounding::Bounded3d,
|
||||||
primitives::{
|
primitives::{
|
||||||
Capsule, Cone, ConicalFrustum, Cuboid, Cylinder, Direction3d, Line3d, Plane3d,
|
Capsule3d, Cone, ConicalFrustum, Cuboid, Cylinder, Direction3d, Line3d, Plane3d,
|
||||||
Polyline3d, Segment3d, Sphere, Torus,
|
Polyline3d, Segment3d, Sphere, Torus,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@ -463,7 +463,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn capsule() {
|
fn capsule() {
|
||||||
let capsule = Capsule::new(0.5, 2.0);
|
let capsule = Capsule3d::new(0.5, 2.0);
|
||||||
let translation = Vec3::new(2.0, 1.0, 0.0);
|
let translation = Vec3::new(2.0, 1.0, 0.0);
|
||||||
|
|
||||||
let aabb = capsule.aabb_3d(translation, Quat::IDENTITY);
|
let aabb = capsule.aabb_3d(translation, Quat::IDENTITY);
|
||||||
|
@ -471,6 +471,7 @@ pub struct Rectangle {
|
|||||||
/// Half of the width and height of the rectangle
|
/// Half of the width and height of the rectangle
|
||||||
pub half_size: Vec2,
|
pub half_size: Vec2,
|
||||||
}
|
}
|
||||||
|
impl Primitive2d for Rectangle {}
|
||||||
|
|
||||||
impl Default for Rectangle {
|
impl Default for Rectangle {
|
||||||
/// Returns the default [`Rectangle`] with a half-width and half-height of `0.5`.
|
/// Returns the default [`Rectangle`] with a half-width and half-height of `0.5`.
|
||||||
@ -721,6 +722,30 @@ impl RegularPolygon {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A 2D capsule primitive, also known as a stadium or pill shape.
|
||||||
|
///
|
||||||
|
/// A two-dimensional capsule is defined as a neighborhood of points at a distance (radius) from a line
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||||
|
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
|
||||||
|
#[doc(alias = "stadium", alias = "pill")]
|
||||||
|
pub struct Capsule2d {
|
||||||
|
/// The radius of the capsule
|
||||||
|
pub radius: f32,
|
||||||
|
/// Half the height of the capsule, excluding the hemicircles
|
||||||
|
pub half_length: f32,
|
||||||
|
}
|
||||||
|
impl Primitive2d for Capsule2d {}
|
||||||
|
|
||||||
|
impl Capsule2d {
|
||||||
|
/// Create a new `Capsule2d` from a radius and length
|
||||||
|
pub fn new(radius: f32, length: f32) -> Self {
|
||||||
|
Self {
|
||||||
|
radius,
|
||||||
|
half_length: length / 2.0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[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
|
||||||
|
@ -423,22 +423,20 @@ impl Cylinder {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A capsule primitive.
|
/// A 3D capsule primitive.
|
||||||
/// A capsule is defined as a surface at a distance (radius) from a line
|
/// A three-dimensional capsule is defined as a surface at a distance (radius) from a line
|
||||||
#[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))]
|
||||||
pub struct Capsule {
|
pub struct Capsule3d {
|
||||||
/// The radius of the capsule
|
/// The radius of the capsule
|
||||||
pub radius: f32,
|
pub radius: f32,
|
||||||
/// Half the height of the capsule, excluding the hemispheres
|
/// Half the height of the capsule, excluding the hemispheres
|
||||||
pub half_length: f32,
|
pub half_length: f32,
|
||||||
}
|
}
|
||||||
impl super::Primitive2d for Capsule {}
|
impl Primitive3d for Capsule3d {}
|
||||||
impl Primitive3d for Capsule {}
|
|
||||||
|
|
||||||
impl Capsule {
|
impl Capsule3d {
|
||||||
/// Create a new `Capsule` from a radius and length
|
/// Create a new `Capsule3d` from a radius and length
|
||||||
#[inline(always)]
|
|
||||||
pub fn new(radius: f32, length: f32) -> Self {
|
pub fn new(radius: f32, length: f32) -> Self {
|
||||||
Self {
|
Self {
|
||||||
radius,
|
radius,
|
||||||
@ -704,7 +702,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn capsule_math() {
|
fn capsule_math() {
|
||||||
let capsule = Capsule::new(2.0, 9.0);
|
let capsule = Capsule3d::new(2.0, 9.0);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
capsule.to_cylinder(),
|
capsule.to_cylinder(),
|
||||||
Cylinder::new(2.0, 9.0),
|
Cylinder::new(2.0, 9.0),
|
||||||
|
@ -91,3 +91,12 @@ impl_reflect_struct!(
|
|||||||
sides: usize,
|
sides: usize,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
impl_reflect_struct!(
|
||||||
|
#[reflect(Debug, PartialEq, Serialize, Deserialize)]
|
||||||
|
#[type_path = "bevy_math::primitives"]
|
||||||
|
struct Capsule2d {
|
||||||
|
radius: f32,
|
||||||
|
half_length: f32,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
@ -71,7 +71,7 @@ impl_reflect_struct!(
|
|||||||
impl_reflect_struct!(
|
impl_reflect_struct!(
|
||||||
#[reflect(Debug, PartialEq, Serialize, Deserialize)]
|
#[reflect(Debug, PartialEq, Serialize, Deserialize)]
|
||||||
#[type_path = "bevy_math::primitives"]
|
#[type_path = "bevy_math::primitives"]
|
||||||
struct Capsule {
|
struct Capsule3d {
|
||||||
radius: f32,
|
radius: f32,
|
||||||
half_length: f32,
|
half_length: f32,
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user