Skip to content

Commit

Permalink
Add Capsule2d primitive (bevyengine#11585)
Browse files Browse the repository at this point in the history
# 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 bevyengine#11431). We also currently don't
implement `Bounded2d` for `Capsule`, see
bevyengine#11336 (comment).

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>
  • Loading branch information
2 people authored and tjamaan committed Feb 6, 2024
1 parent bcd6b35 commit 3d2ac60
Show file tree
Hide file tree
Showing 6 changed files with 89 additions and 18 deletions.
47 changes: 43 additions & 4 deletions crates/bevy_math/src/bounding/bounded2d/primitive_impls.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
use glam::{Mat2, Vec2};

use crate::primitives::{
BoxedPolygon, BoxedPolyline2d, Circle, Ellipse, Line2d, Plane2d, Polygon, Polyline2d,
Rectangle, RegularPolygon, Segment2d, Triangle2d,
BoxedPolygon, BoxedPolyline2d, Capsule2d, Circle, Direction2d, Ellipse, Line2d, Plane2d,
Polygon, Polyline2d, Rectangle, RegularPolygon, Segment2d, Triangle2d,
};

use super::{Aabb2d, Bounded2d, BoundingCircle};
Expand Down Expand Up @@ -230,15 +230,40 @@ 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)]
mod tests {
use glam::Vec2;

use crate::{
bounding::Bounded2d,
primitives::{
Circle, Direction2d, Ellipse, Line2d, Plane2d, Polygon, Polyline2d, Rectangle,
RegularPolygon, Segment2d, Triangle2d,
Capsule2d, Circle, Direction2d, Ellipse, Line2d, Plane2d, Polygon, Polyline2d,
Rectangle, RegularPolygon, Segment2d, Triangle2d,
},
};

Expand Down Expand Up @@ -440,4 +465,18 @@ mod tests {
assert_eq!(bounding_circle.center, translation);
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);
}
}
8 changes: 4 additions & 4 deletions crates/bevy_math/src/bounding/bounded3d/primitive_impls.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use glam::{Mat3, Quat, Vec2, Vec3};
use crate::{
bounding::{Bounded2d, BoundingCircle},
primitives::{
BoxedPolyline3d, Capsule, Cone, ConicalFrustum, Cuboid, Cylinder, Direction3d, Line3d,
BoxedPolyline3d, Capsule3d, Cone, ConicalFrustum, Cuboid, Cylinder, Direction3d, Line3d,
Plane3d, Polyline3d, Segment3d, Sphere, Torus, Triangle2d,
},
};
Expand Down Expand Up @@ -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 {
// Get the line segment between the hemispheres of the rotated capsule
let segment = Segment3d {
Expand Down Expand Up @@ -311,7 +311,7 @@ mod tests {
use crate::{
bounding::Bounded3d,
primitives::{
Capsule, Cone, ConicalFrustum, Cuboid, Cylinder, Direction3d, Line3d, Plane3d,
Capsule3d, Cone, ConicalFrustum, Cuboid, Cylinder, Direction3d, Line3d, Plane3d,
Polyline3d, Segment3d, Sphere, Torus,
},
};
Expand Down Expand Up @@ -463,7 +463,7 @@ mod tests {

#[test]
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 aabb = capsule.aabb_3d(translation, Quat::IDENTITY);
Expand Down
25 changes: 25 additions & 0 deletions crates/bevy_math/src/primitives/dim2.rs
Original file line number Diff line number Diff line change
Expand Up @@ -471,6 +471,7 @@ pub struct Rectangle {
/// Half of the width and height of the rectangle
pub half_size: Vec2,
}
impl Primitive2d for Rectangle {}

impl Default for Rectangle {
/// Returns the default [`Rectangle`] with a half-width and half-height of `0.5`.
Expand Down Expand Up @@ -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)]
mod tests {
// Reference values were computed by hand and/or with external tools
Expand Down
16 changes: 7 additions & 9 deletions crates/bevy_math/src/primitives/dim3.rs
Original file line number Diff line number Diff line change
Expand Up @@ -423,22 +423,20 @@ impl Cylinder {
}
}

/// A capsule primitive.
/// A capsule is defined as a surface at a distance (radius) from a line
/// A 3D capsule primitive.
/// A three-dimensional capsule is defined as a surface at a distance (radius) from a line
#[derive(Clone, Copy, Debug, PartialEq)]
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
pub struct Capsule {
pub struct Capsule3d {
/// The radius of the capsule
pub radius: f32,
/// Half the height of the capsule, excluding the hemispheres
pub half_length: f32,
}
impl super::Primitive2d for Capsule {}
impl Primitive3d for Capsule {}
impl Primitive3d for Capsule3d {}

impl Capsule {
/// Create a new `Capsule` from a radius and length
#[inline(always)]
impl Capsule3d {
/// Create a new `Capsule3d` from a radius and length
pub fn new(radius: f32, length: f32) -> Self {
Self {
radius,
Expand Down Expand Up @@ -704,7 +702,7 @@ mod tests {

#[test]
fn capsule_math() {
let capsule = Capsule::new(2.0, 9.0);
let capsule = Capsule3d::new(2.0, 9.0);
assert_eq!(
capsule.to_cylinder(),
Cylinder::new(2.0, 9.0),
Expand Down
9 changes: 9 additions & 0 deletions crates/bevy_reflect/src/impls/math/primitives2d.rs
Original file line number Diff line number Diff line change
Expand Up @@ -91,3 +91,12 @@ impl_reflect_struct!(
sides: usize,
}
);

impl_reflect_struct!(
#[reflect(Debug, PartialEq, Serialize, Deserialize)]
#[type_path = "bevy_math::primitives"]
struct Capsule2d {
radius: f32,
half_length: f32,
}
);
2 changes: 1 addition & 1 deletion crates/bevy_reflect/src/impls/math/primitives3d.rs
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ impl_reflect_struct!(
impl_reflect_struct!(
#[reflect(Debug, PartialEq, Serialize, Deserialize)]
#[type_path = "bevy_math::primitives"]
struct Capsule {
struct Capsule3d {
radius: f32,
half_length: f32,
}
Expand Down

0 comments on commit 3d2ac60

Please sign in to comment.