Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
97 changes: 97 additions & 0 deletions crates/bevy_gizmos/src/circles.rs
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,46 @@ where
resolution: DEFAULT_CIRCLE_RESOLUTION,
}
}

/// Draw a wireframe ellipsoid in 3D made out of 3 ellipses around the axes with the given
/// `isometry` applied.
///
/// If `isometry == Isometry3d::IDENTITY` then
///
/// - the center is at `Vec3::ZERO`
/// - the 3 ellipses are in the XY, YZ and XZ planes.
///
/// # Example
/// ```
/// # use bevy_gizmos::prelude::*;
/// # use bevy_math::prelude::*;
/// # use bevy_color::Color;
/// fn system(mut gizmos: Gizmos) {
/// gizmos.ellipsoid(Isometry3d::IDENTITY, Vec3::new(1., 2., 3.), Color::BLACK);
///
/// // Each circle has 32 line-segments by default.
/// // You may want to increase this for larger spheres.
/// gizmos
/// .ellipsoid(Isometry3d::IDENTITY, Vec3::new(1., 2., 3.), Color::BLACK)
/// .resolution(64);
/// }
/// # bevy_ecs::system::assert_is_system(system);
/// ```
#[inline]
pub fn ellipsoid(
&mut self,
isometry: impl Into<Isometry3d>,
radii: Vec3,
color: impl Into<Color>,
) -> EllipsoidBuilder<'_, Config, Clear> {
EllipsoidBuilder {
gizmos: self,
radii,
isometry: isometry.into(),
color: color.into(),
resolution: DEFAULT_CIRCLE_RESOLUTION,
}
}
}

/// A builder returned by [`GizmoBuffer::ellipse`].
Expand Down Expand Up @@ -353,3 +393,60 @@ where
});
}
}

/// A builder returned by [`GizmoBuffer::ellipsoid`].
pub struct EllipsoidBuilder<'a, Config, Clear>
where
Config: GizmoConfigGroup,
Clear: 'static + Send + Sync,
{
gizmos: &'a mut GizmoBuffer<Config, Clear>,

// Radii of the ellipsoid
radii: Vec3,

isometry: Isometry3d,
// Color of the ellipsoid
color: Color,

// Number of line-segments used to approximate the ellipsoid geometry
resolution: u32,
}

impl<Config, Clear> EllipsoidBuilder<'_, Config, Clear>
where
Config: GizmoConfigGroup,
Clear: 'static + Send + Sync,
{
/// Set the number of line-segments used to approximate the ellipsoid geometry.
pub fn resolution(mut self, resolution: u32) -> Self {
self.resolution = resolution;
self
}
}

impl<Config, Clear> Drop for EllipsoidBuilder<'_, Config, Clear>
where
Config: GizmoConfigGroup,
Clear: 'static + Send + Sync,
{
fn drop(&mut self) {
if !self.gizmos.enabled {
return;
}

// draws one great circle around each of the local axes
[
(Vec3::X, Vec2::new(self.radii.z, self.radii.y)),
(Vec3::Y, Vec2::new(self.radii.x, self.radii.z)),
(Vec3::Z, Vec2::new(self.radii.x, self.radii.y)),
]
.into_iter()
.for_each(|(axis, half_size)| {
let axis_rotation = Isometry3d::from_rotation(Quat::from_rotation_arc(Vec3::Z, axis));
self.gizmos
.ellipse(self.isometry * axis_rotation, half_size, self.color)
.resolution(self.resolution);
});
}
}
30 changes: 28 additions & 2 deletions crates/bevy_gizmos/src/primitives/dim3.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,17 @@ use super::helpers::*;
use bevy_color::Color;
use bevy_math::{
primitives::{
Capsule3d, Cone, ConicalFrustum, Cuboid, Cylinder, Line3d, Plane3d, Polyline3d,
Capsule3d, Cone, ConicalFrustum, Cuboid, Cylinder, Ellipsoid, Line3d, Plane3d, Polyline3d,
Primitive3d, Segment3d, Sphere, Tetrahedron, Torus, Triangle3d,
},
Dir3, Isometry3d, Quat, UVec2, Vec2, Vec3,
};

use crate::{circles::SphereBuilder, gizmos::GizmoBuffer, prelude::GizmoConfigGroup};
use crate::{
circles::{EllipsoidBuilder, SphereBuilder},
gizmos::GizmoBuffer,
prelude::GizmoConfigGroup,
};

const DEFAULT_RESOLUTION: u32 = 5;
// length used to simulate infinite lines
Expand Down Expand Up @@ -80,6 +84,28 @@ where
}
}

// ellipsoid

impl<Config, Clear> GizmoPrimitive3d<Ellipsoid> for GizmoBuffer<Config, Clear>
where
Config: GizmoConfigGroup,
Clear: 'static + Send + Sync,
{
type Output<'a>
= EllipsoidBuilder<'a, Config, Clear>
where
Self: 'a;

fn primitive_3d(
&mut self,
primitive: &Ellipsoid,
isometry: impl Into<Isometry3d>,
color: impl Into<Color>,
) -> Self::Output<'_> {
self.ellipsoid(isometry, primitive.radii, color)
}
}

// plane 3d

/// Builder for configuring the drawing options of [`Plane3d`].
Expand Down
36 changes: 32 additions & 4 deletions crates/bevy_math/src/bounding/bounded3d/primitive_impls.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ use crate::{
bounding::{Bounded2d, BoundingCircle, BoundingVolume},
ops,
primitives::{
Capsule3d, Cone, ConicalFrustum, Cuboid, Cylinder, InfinitePlane3d, Line3d, Segment3d,
Sphere, Torus, Triangle2d, Triangle3d,
Capsule3d, Cone, ConicalFrustum, Cuboid, Cylinder, Ellipsoid, InfinitePlane3d, Line3d,
Segment3d, Sphere, Torus, Triangle2d, Triangle3d,
},
Isometry2d, Isometry3d, Mat3, Vec2, Vec3, Vec3A,
};
Expand All @@ -27,6 +27,18 @@ impl Bounded3d for Sphere {
}
}

impl Bounded3d for Ellipsoid {
fn aabb_3d(&self, isometry: impl Into<Isometry3d>) -> Aabb3d {
let isometry = isometry.into();
Aabb3d::new(isometry.translation, self.radii)
}

fn bounding_sphere(&self, isometry: impl Into<Isometry3d>) -> BoundingSphere {
let isometry = isometry.into();
BoundingSphere::new(isometry.translation, self.radii.max_element())
}
}

impl Bounded3d for InfinitePlane3d {
fn aabb_3d(&self, isometry: impl Into<Isometry3d>) -> Aabb3d {
let isometry = isometry.into();
Expand Down Expand Up @@ -372,8 +384,8 @@ mod tests {
use crate::{
bounding::Bounded3d,
primitives::{
Capsule3d, Cone, ConicalFrustum, Cuboid, Cylinder, InfinitePlane3d, Line3d, Polyline3d,
Segment3d, Sphere, Torus, Triangle3d,
Capsule3d, Cone, ConicalFrustum, Cuboid, Cylinder, Ellipsoid, InfinitePlane3d, Line3d,
Polyline3d, Segment3d, Sphere, Torus, Triangle3d,
},
Dir3,
};
Expand All @@ -392,6 +404,22 @@ mod tests {
assert_eq!(bounding_sphere.radius(), 1.0);
}

#[test]
fn ellipsoid() {
let ellipsoid = Ellipsoid {
radii: Vec3::new(1.0, 2.0, 3.0),
};
let translation = Vec3::new(2.0, 1.0, 0.0);

let aabb = ellipsoid.aabb_3d(translation);
assert_eq!(aabb.min, Vec3A::new(1.0, -1.0, -3.0));
assert_eq!(aabb.max, Vec3A::new(3.0, 3.0, 3.0));

let bounding_sphere = ellipsoid.bounding_sphere(translation);
assert_eq!(bounding_sphere.center, translation.into());
assert_eq!(bounding_sphere.radius(), 3.0);
}

#[test]
fn plane() {
let translation = Vec3::new(2.0, 1.0, 0.0);
Expand Down
82 changes: 82 additions & 0 deletions crates/bevy_math/src/primitives/dim3.rs
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,74 @@ impl Measured3d for Sphere {
}
}

/// An ellipsoid primitive representing all points whose distance from the origin is scaled
/// independently along each axis
#[derive(Clone, Copy, Debug, PartialEq)]
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(
feature = "bevy_reflect",
derive(Reflect),
reflect(Debug, PartialEq, Default, Clone)
)]
#[cfg_attr(
all(feature = "serialize", feature = "bevy_reflect"),
reflect(Serialize, Deserialize)
)]
pub struct Ellipsoid {
/// The radii of the ellipsoid in each dimension
pub radii: Vec3,
}

impl Primitive3d for Ellipsoid {}

impl Default for Ellipsoid {
/// Returns the default [`Ellipsoid`] with a radius of `0.5` in each dimension.
fn default() -> Self {
Self {
radii: Vec3::splat(0.5),
}
}
}

impl Ellipsoid {
/// Create a new [`Ellipsoid`] from a `radii`.
#[inline]
pub const fn new(radii: Vec3) -> Self {
Self { radii }
}

/// Get the diameter of the ellipsoid in each dimension.
///
/// This is a [`Vec3`] where the components refer to the diameter in each dimension.
#[inline]
pub fn diameter(&self) -> Vec3 {
2.0 * self.radii
}
}

impl Measured3d for Ellipsoid {
/// Get the surface area of the ellipsoid.
///
/// This is only an approximation with a relative error of about 1.061%.
///
/// See <https://en.wikipedia.org/wiki/Ellipsoid#Approximate_formula>
#[inline]
fn area(&self) -> f32 {
let p = 1.6075;
let pow_p = |x| ops::powf(x, p);
let inner_sqrt = pow_p(self.radii.x) * pow_p(self.radii.y)
+ pow_p(self.radii.y) * pow_p(self.radii.z)
+ pow_p(self.radii.z) * pow_p(self.radii.x);
4.0 * PI * ops::powf(inner_sqrt / 3.0, p.recip())
}

/// Get the volume of the ellipsoid.
#[inline]
fn volume(&self) -> f32 {
4.0 * FRAC_PI_3 * self.radii.x * self.radii.y * self.radii.z
}
}

/// A bounded plane in 3D space. It forms a surface starting from the origin with a defined height and width.
#[derive(Clone, Copy, Debug, PartialEq)]
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
Expand Down Expand Up @@ -1692,6 +1760,20 @@ mod tests {
assert_eq!(sphere.volume(), 268.08257, "incorrect volume");
}

#[test]
fn ellipsoid_math() {
let ellipsoid = Ellipsoid {
radii: Vec3::new(1.0, 2.0, 3.0),
};
assert_eq!(
ellipsoid.diameter(),
Vec3::new(2.0, 4.0, 6.0),
"incorrect diameter"
);
assert_eq!(ellipsoid.area(), 48.971935, "incorrect area");
assert_eq!(ellipsoid.volume(), 25.132742, "incorrect volume");
}

#[test]
fn plane_from_points() {
let (plane, translation) = Plane3d::from_points(Vec3::X, Vec3::Z, Vec3::NEG_X);
Expand Down
22 changes: 22 additions & 0 deletions crates/bevy_math/src/sampling/shape_sampling.rs
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,28 @@ impl ShapeSample for Sphere {
}
}

// NOTE: This implementation is *cheap* and *not perfectly accurate*. This was a deliberate choice
// since accurate sampling would require rejection based approaches. If this is re-evaluated to not
// matter in the future, the implementation may change to something else.
impl ShapeSample for Ellipsoid {
type Output = Vec3;

fn sample_interior<R: RngExt + ?Sized>(&self, rng: &mut R) -> Vec3 {
let r_cubed = rng.random_range(0.0..=1.0);
let r = ops::cbrt(r_cubed);

let p = r * sample_unit_sphere_boundary(rng);

Vec3::new(p.x * self.radii.x, p.y * self.radii.y, p.z * self.radii.z)
}

fn sample_boundary<R: RngExt + ?Sized>(&self, rng: &mut R) -> Vec3 {
let p = sample_unit_sphere_boundary(rng);

Vec3::new(p.x * self.radii.x, p.y * self.radii.y, p.z * self.radii.z)
}
}

impl ShapeSample for Annulus {
type Output = Vec2;

Expand Down
Loading
Loading