avian2d/collision/collider/parry/
mod.rs

1#![allow(clippy::unnecessary_cast)]
2
3pub mod contact_query;
4
5#[cfg(feature = "2d")]
6mod primitives2d;
7#[cfg(feature = "3d")]
8mod primitives3d;
9
10#[cfg(feature = "2d")]
11pub use primitives2d::{EllipseColliderShape, RegularPolygonColliderShape};
12
13use super::EnlargedAabb;
14use crate::{make_pose, prelude::*};
15#[cfg(feature = "collider-from-mesh")]
16use bevy::mesh::{Indices, VertexAttributeValues};
17use bevy::{log, prelude::*};
18use contact_query::UnsupportedShape;
19use itertools::Either;
20use parry::shape::{RoundShape, SharedShape, TypedShape, Voxels};
21
22impl<T: IntoCollider<Collider>> From<T> for Collider {
23    fn from(value: T) -> Self {
24        value.collider()
25    }
26}
27
28/// Parameters controlling the VHACD convex decomposition.
29///
30/// See <https://github.com/Unity-Technologies/VHACD#parameters> for details.
31#[derive(Clone, PartialEq, Debug, Reflect)]
32#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
33#[cfg_attr(feature = "serialize", reflect(Serialize, Deserialize))]
34#[reflect(PartialEq, Debug)]
35pub struct VhacdParameters {
36    /// Maximum concavity.
37    ///
38    /// Default: 0.1 (in 2D), 0.01 (in 3D).
39    /// Valid range `[0.0, 1.0]`.
40    pub concavity: Scalar,
41    /// Controls the bias toward clipping along symmetry planes.
42    ///
43    /// Default: 0.05.
44    /// Valid Range: `[0.0, 1.0]`.
45    pub alpha: Scalar,
46    /// Controls the bias toward clipping along revolution planes.
47    ///
48    /// Default: 0.05.
49    /// Valid Range: `[0.0, 1.0]`.
50    pub beta: Scalar,
51    /// Resolution used during the voxelization stage.
52    ///
53    /// Default: 256 (in 2D), 64 (in 3D).
54    pub resolution: u32,
55    /// Controls the granularity of the search for the best
56    /// clipping plane during the decomposition.
57    ///
58    /// Default: 4
59    pub plane_downsampling: u32,
60    /// Controls the precision of the convex-hull generation
61    /// process during the clipping plane selection stage.
62    ///
63    /// Default: 4
64    pub convex_hull_downsampling: u32,
65    /// Controls the way the input mesh or polyline is being
66    /// voxelized.
67    ///
68    /// Default: `FillMode::FloodFill { detect_cavities: false, detect_self_intersections: false }`
69    pub fill_mode: FillMode,
70    /// Controls whether the convex-hull should be approximated during the decomposition stage.
71    /// Setting this to `true` increases performances with a slight degradation of the decomposition
72    /// quality.
73    ///
74    /// Default: true
75    pub convex_hull_approximation: bool,
76    /// Controls the max number of convex-hull generated by the convex decomposition.
77    ///
78    /// Default: 1024
79    pub max_convex_hulls: u32,
80}
81
82impl Default for VhacdParameters {
83    fn default() -> Self {
84        Self {
85            #[cfg(feature = "3d")]
86            resolution: 64,
87            #[cfg(feature = "3d")]
88            concavity: 0.01,
89            #[cfg(feature = "2d")]
90            resolution: 256,
91            #[cfg(feature = "2d")]
92            concavity: 0.1,
93            plane_downsampling: 4,
94            convex_hull_downsampling: 4,
95            alpha: 0.05,
96            beta: 0.05,
97            convex_hull_approximation: true,
98            max_convex_hulls: 1024,
99            fill_mode: FillMode::FloodFill {
100                detect_cavities: false,
101                #[cfg(feature = "2d")]
102                detect_self_intersections: false,
103            },
104        }
105    }
106}
107
108impl From<VhacdParameters> for parry::transformation::vhacd::VHACDParameters {
109    fn from(value: VhacdParameters) -> Self {
110        Self {
111            concavity: value.concavity,
112            alpha: value.alpha,
113            beta: value.beta,
114            resolution: value.resolution,
115            plane_downsampling: value.plane_downsampling,
116            convex_hull_downsampling: value.convex_hull_downsampling,
117            fill_mode: value.fill_mode.into(),
118            convex_hull_approximation: value.convex_hull_approximation,
119            max_convex_hulls: value.max_convex_hulls,
120        }
121    }
122}
123
124/// Controls how the voxelization determines which voxel needs
125/// to be considered empty, and which ones will be considered full.
126#[derive(Hash, Clone, Copy, PartialEq, Eq, Debug, Reflect)]
127#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
128#[cfg_attr(feature = "serialize", reflect(Serialize, Deserialize))]
129#[reflect(Hash, PartialEq, Debug)]
130pub enum FillMode {
131    /// Only consider full the voxels intersecting the surface of the
132    /// shape being voxelized.
133    SurfaceOnly,
134    /// Use a flood-fill technique to consider fill the voxels intersecting
135    /// the surface of the shape being voxelized, as well as all the voxels
136    /// bounded of them.
137    FloodFill {
138        /// Detects holes inside of a solid contour.
139        detect_cavities: bool,
140        /// Attempts to properly handle self-intersections.
141        #[cfg(feature = "2d")]
142        detect_self_intersections: bool,
143    },
144}
145
146impl From<FillMode> for parry::transformation::voxelization::FillMode {
147    fn from(value: FillMode) -> Self {
148        match value {
149            FillMode::SurfaceOnly => Self::SurfaceOnly,
150            FillMode::FloodFill {
151                detect_cavities,
152                #[cfg(feature = "2d")]
153                detect_self_intersections,
154            } => Self::FloodFill {
155                detect_cavities,
156                #[cfg(feature = "2d")]
157                detect_self_intersections,
158            },
159        }
160    }
161}
162
163/// Flags used for the preprocessing of a triangle mesh collider.
164#[repr(transparent)]
165#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
166#[derive(Hash, Clone, Copy, PartialEq, Eq, Debug, Reflect)]
167#[reflect(opaque, Hash, PartialEq, Debug)]
168pub struct TrimeshFlags(u8);
169
170bitflags::bitflags! {
171    impl TrimeshFlags: u8 {
172        /// If set, the half-edge topology of the trimesh will be computed if possible.
173        const HALF_EDGE_TOPOLOGY = 0b0000_0001;
174        /// If set, the half-edge topology and connected components of the trimesh will be computed if possible.
175        ///
176        /// Because of the way it is currently implemented, connected components can only be computed on
177        /// a mesh where the half-edge topology computation succeeds. It will no longer be the case in the
178        /// future once we decouple the computations.
179        const CONNECTED_COMPONENTS = 0b0000_0010;
180        /// If set, any triangle that results in a failing half-hedge topology computation will be deleted.
181        const DELETE_BAD_TOPOLOGY_TRIANGLES = 0b0000_0100;
182        /// If set, the trimesh will be assumed to be oriented (with outward normals).
183        ///
184        /// The pseudo-normals of its vertices and edges will be computed.
185        const ORIENTED = 0b0000_1000;
186        /// If set, the duplicate vertices of the trimesh will be merged.
187        ///
188        /// Two vertices with the exact same coordinates will share the same entry on the
189        /// vertex buffer and the index buffer is adjusted accordingly.
190        const MERGE_DUPLICATE_VERTICES = 0b0001_0000;
191        /// If set, the triangles sharing two vertices with identical index values will be removed.
192        ///
193        /// Because of the way it is currently implemented, this methods implies that duplicate
194        /// vertices will be merged. It will no longer be the case in the future once we decouple
195        /// the computations.
196        const DELETE_DEGENERATE_TRIANGLES = 0b0010_0000;
197        /// If set, two triangles sharing three vertices with identical index values (in any order) will be removed.
198        ///
199        /// Because of the way it is currently implemented, this methods implies that duplicate
200        /// vertices will be merged. It will no longer be the case in the future once we decouple
201        /// the computations.
202        const DELETE_DUPLICATE_TRIANGLES = 0b0100_0000;
203        /// If set, a special treatment will be applied to contact manifold calculation to eliminate
204        /// or fix contacts normals that could lead to incorrect bumps in physics simulation
205        /// (especially on flat surfaces).
206        ///
207        /// This is achieved by taking into account adjacent triangle normals when computing contact
208        /// points for a given triangle.
209        const FIX_INTERNAL_EDGES = 0b1000_0000 | Self::ORIENTED.bits() | Self::MERGE_DUPLICATE_VERTICES.bits();
210    }
211}
212
213impl From<TrimeshFlags> for parry::shape::TriMeshFlags {
214    fn from(value: TrimeshFlags) -> Self {
215        Self::from_bits(value.bits().into()).unwrap()
216    }
217}
218
219/// An error indicating an inconsistency when building a triangle mesh collider.
220pub type TrimeshBuilderError = parry::shape::TriMeshBuilderError;
221
222/// A collider used for detecting collisions and generating contacts.
223///
224/// # Creation
225///
226/// `Collider` has tons of methods for creating colliders of various shapes:
227///
228/// ```
229#[cfg_attr(feature = "2d", doc = "# use avian2d::prelude::*;")]
230#[cfg_attr(feature = "3d", doc = "# use avian3d::prelude::*;")]
231/// # use bevy::prelude::*;
232/// #
233/// # fn setup(mut commands: Commands) {
234/// // Create a ball collider with a given radius
235#[cfg_attr(feature = "2d", doc = "commands.spawn(Collider::circle(0.5));")]
236#[cfg_attr(feature = "3d", doc = "commands.spawn(Collider::sphere(0.5));")]
237/// // Create a capsule collider with a given radius and height
238/// commands.spawn(Collider::capsule(0.5, 2.0));
239/// # }
240/// ```
241///
242/// Colliders on their own only detect contacts and generate
243/// [collision events](crate::collision#collision-events).
244/// To make colliders apply contact forces, they have to be attached
245/// to [rigid bodies](RigidBody):
246///
247/// ```
248#[cfg_attr(feature = "2d", doc = "use avian2d::prelude::*;")]
249#[cfg_attr(feature = "3d", doc = "use avian3d::prelude::*;")]
250/// use bevy::prelude::*;
251///
252/// // Spawn a dynamic body that falls onto a static platform
253/// fn setup(mut commands: Commands) {
254///     commands.spawn((
255///         RigidBody::Dynamic,
256#[cfg_attr(feature = "2d", doc = "        Collider::circle(0.5),")]
257#[cfg_attr(feature = "3d", doc = "        Collider::sphere(0.5),")]
258///         Transform::from_xyz(0.0, 2.0, 0.0),
259///     ));
260#[cfg_attr(
261    feature = "2d",
262    doc = "    commands.spawn((RigidBody::Static, Collider::rectangle(5.0, 0.5)));"
263)]
264#[cfg_attr(
265    feature = "3d",
266    doc = "    commands.spawn((RigidBody::Static, Collider::cuboid(5.0, 0.5, 5.0)));"
267)]
268/// }
269/// ```
270///
271/// Colliders can be further configured using various components like [`Friction`], [`Restitution`],
272/// [`Sensor`], [`CollisionLayers`], [`CollisionMargin`], and [`ColliderDensity`].
273///
274/// If you need to specify the shape of the collider statically, use [`ColliderConstructor`] and build your collider
275/// with the [`Collider::try_from_constructor`] method.
276/// This can also be done automatically by simply placing the [`ColliderConstructor`] on an entity.
277///
278#[cfg_attr(
279    feature = "3d",
280    doc = "Colliders can also be generated automatically for meshes and scenes. See [`ColliderConstructor`] and [`ColliderConstructorHierarchy`]."
281)]
282///
283/// ## Multiple Colliders
284///
285/// It can often be useful to attach multiple colliders to the same rigid body.
286///
287/// This can be done in two ways. Either use [`Collider::compound`] to have one collider that consists of many
288/// shapes, or for more control, spawn several collider entities as the children of a rigid body:
289///
290/// ```
291#[cfg_attr(feature = "2d", doc = "use avian2d::prelude::*;")]
292#[cfg_attr(feature = "3d", doc = "use avian3d::prelude::*;")]
293/// use bevy::prelude::*;
294///
295/// fn setup(mut commands: Commands) {
296///     // Spawn a rigid body with one collider on the same entity and two as children
297///     commands
298#[cfg_attr(
299    feature = "2d",
300    doc = "        .spawn((RigidBody::Dynamic, Collider::circle(0.5)))"
301)]
302#[cfg_attr(
303    feature = "3d",
304    doc = "        .spawn((RigidBody::Dynamic, Collider::sphere(0.5)))"
305)]
306///         .with_children(|children| {
307///             // Spawn the child colliders positioned relative to the rigid body
308#[cfg_attr(
309    feature = "2d",
310    doc = "            children.spawn((Collider::circle(0.5), Transform::from_xyz(2.0, 0.0, 0.0)));
311            children.spawn((Collider::circle(0.5), Transform::from_xyz(-2.0, 0.0, 0.0)));"
312)]
313#[cfg_attr(
314    feature = "3d",
315    doc = "            children.spawn((Collider::sphere(0.5), Transform::from_xyz(2.0, 0.0, 0.0)));
316            children.spawn((Collider::sphere(0.5), Transform::from_xyz(-2.0, 0.0, 0.0)));"
317)]
318///         });
319/// }
320/// ```
321///
322/// Colliders can be arbitrarily nested and transformed relative to the parent.
323/// The rigid body that a collider is attached to can be accessed using the [`ColliderOf`] component.
324///
325/// The benefit of using separate entities for the colliders is that each collider can have its own
326/// [friction](Friction), [restitution](Restitution), [collision layers](CollisionLayers),
327/// and other configuration options, and they send separate [collision events](crate::collision#collision-events).
328///
329/// # See More
330///
331/// - [Rigid bodies](RigidBody)
332/// - [Density](ColliderDensity)
333/// - [Friction] and [restitution](Restitution) (bounciness)
334/// - [Collision layers](CollisionLayers)
335/// - [Sensors](Sensor)
336/// - [Collision margins for adding extra thickness to colliders](CollisionMargin)
337#[cfg_attr(
338    feature = "3d",
339    doc = "- Generating colliders for meshes and scenes with [`ColliderConstructor`] and [`ColliderConstructorHierarchy`]"
340)]
341/// - [Get colliding entities](CollidingEntities)
342/// - [Collision events](crate::collision#collision-events)
343/// - [Accessing collision data](Collisions)
344/// - [Filtering and modifying contacts with hooks](CollisionHooks)
345/// - [Manual contact queries](contact_query)
346///
347/// # Advanced Usage
348///
349/// Internally, `Collider` uses the shapes provided by `parry`. If you want to create a collider
350/// using these shapes, you can simply use `Collider::from(SharedShape::some_method())`.
351///
352/// To get a reference to the internal [`SharedShape`], you can use the [`Collider::shape()`]
353/// or [`Collider::shape_scaled()`] methods.
354///
355/// `Collider` is currently not `Reflect`. If you need to reflect it, you can use [`ColliderConstructor`] as a workaround.
356#[derive(Clone, Component, Debug)]
357#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
358#[require(
359    ColliderMarker,
360    ColliderAabb,
361    CollisionLayers,
362    EnlargedAabb,
363    ColliderDensity,
364    ColliderMassProperties
365)]
366pub struct Collider {
367    /// The raw unscaled collider shape.
368    shape: SharedShape,
369    /// The scaled version of the collider shape.
370    ///
371    /// If the scale is `Vector::ONE`, this will be `None` and `unscaled_shape`
372    /// will be used instead.
373    scaled_shape: SharedShape,
374    /// The global scale used for the collider shape.
375    scale: Vector,
376}
377
378impl From<SharedShape> for Collider {
379    fn from(value: SharedShape) -> Self {
380        Self {
381            shape: value.clone(),
382            scaled_shape: value,
383            scale: Vector::ONE,
384        }
385    }
386}
387
388impl Default for Collider {
389    fn default() -> Self {
390        #[cfg(feature = "2d")]
391        {
392            Self::rectangle(0.5, 0.5)
393        }
394        #[cfg(feature = "3d")]
395        {
396            Self::cuboid(0.5, 0.5, 0.5)
397        }
398    }
399}
400
401impl AnyCollider for Collider {
402    type Context = ();
403
404    fn aabb_with_context(
405        &self,
406        position: Vector,
407        rotation: impl Into<Rotation>,
408        _: AabbContext<Self::Context>,
409    ) -> ColliderAabb {
410        let aabb = self
411            .shape_scaled()
412            .compute_aabb(&make_pose(position, rotation));
413        ColliderAabb {
414            min: aabb.mins,
415            max: aabb.maxs,
416        }
417    }
418
419    fn contact_manifolds_with_context(
420        &self,
421        other: &Self,
422        position1: Vector,
423        rotation1: impl Into<Rotation>,
424        position2: Vector,
425        rotation2: impl Into<Rotation>,
426        prediction_distance: Scalar,
427        manifolds: &mut Vec<ContactManifold>,
428        _: ContactManifoldContext<Self::Context>,
429    ) {
430        contact_query::contact_manifolds(
431            self,
432            position1,
433            rotation1,
434            other,
435            position2,
436            rotation2,
437            prediction_distance,
438            manifolds,
439        )
440    }
441}
442
443// TODO: `bevy_heavy` supports computing the individual mass properties efficiently for Bevy's primitive shapes,
444//       but Parry doesn't support it for its own shapes, so we have to compute all mass properties in each method :(
445#[cfg(feature = "2d")]
446impl ComputeMassProperties for Collider {
447    fn mass(&self, density: f32) -> f32 {
448        let props = self.shape_scaled().mass_properties(density as Scalar);
449        props.mass() as f32
450    }
451
452    fn unit_angular_inertia(&self) -> f32 {
453        self.angular_inertia(1.0)
454    }
455
456    fn angular_inertia(&self, mass: f32) -> f32 {
457        let props = self.shape_scaled().mass_properties(mass as Scalar);
458        props.principal_inertia() as f32
459    }
460
461    fn center_of_mass(&self) -> Vec2 {
462        let props = self.shape_scaled().mass_properties(1.0);
463        props.local_com.f32()
464    }
465
466    fn mass_properties(&self, density: f32) -> MassProperties {
467        let props = self.shape_scaled().mass_properties(density as Scalar);
468
469        MassProperties {
470            mass: props.mass() as f32,
471            #[cfg(feature = "2d")]
472            angular_inertia: props.principal_inertia() as f32,
473            #[cfg(feature = "3d")]
474            principal_angular_inertia: props.principal_inertia().f32(),
475            #[cfg(feature = "3d")]
476            local_inertial_frame: props.principal_inertia_local_frame.f32(),
477            center_of_mass: props.local_com.f32(),
478        }
479    }
480}
481
482#[cfg(feature = "3d")]
483impl ComputeMassProperties for Collider {
484    fn mass(&self, density: f32) -> f32 {
485        let props = self.shape_scaled().mass_properties(density as Scalar);
486        props.mass() as f32
487    }
488
489    fn unit_principal_angular_inertia(&self) -> Vec3 {
490        self.principal_angular_inertia(1.0)
491    }
492
493    fn principal_angular_inertia(&self, mass: f32) -> Vec3 {
494        let props = self.shape_scaled().mass_properties(mass as Scalar);
495        props.principal_inertia().f32()
496    }
497
498    fn local_inertial_frame(&self) -> Quat {
499        let props = self.shape_scaled().mass_properties(1.0);
500        props.principal_inertia_local_frame.f32()
501    }
502
503    fn center_of_mass(&self) -> Vec3 {
504        let props = self.shape_scaled().mass_properties(1.0);
505        props.local_com.f32()
506    }
507
508    fn mass_properties(&self, density: f32) -> MassProperties {
509        let props = self.shape_scaled().mass_properties(density as Scalar);
510
511        MassProperties {
512            mass: props.mass() as f32,
513            #[cfg(feature = "2d")]
514            angular_inertia: props.principal_inertia() as f32,
515            #[cfg(feature = "3d")]
516            principal_angular_inertia: props.principal_inertia().f32(),
517            #[cfg(feature = "3d")]
518            local_inertial_frame: props.principal_inertia_local_frame.f32(),
519            center_of_mass: props.local_com.f32(),
520        }
521    }
522}
523
524impl ScalableCollider for Collider {
525    fn scale(&self) -> Vector {
526        self.scale()
527    }
528
529    fn set_scale(&mut self, scale: Vector, detail: u32) {
530        self.set_scale(scale, detail)
531    }
532}
533
534impl Collider {
535    /// Returns the raw unscaled shape of the collider.
536    pub fn shape(&self) -> &SharedShape {
537        &self.shape
538    }
539
540    /// Returns a mutable reference to the raw unscaled shape of the collider.
541    pub fn shape_mut(&mut self) -> &mut SharedShape {
542        &mut self.shape
543    }
544
545    /// Returns the shape of the collider with the scale from its `GlobalTransform` applied.
546    pub fn shape_scaled(&self) -> &SharedShape {
547        &self.scaled_shape
548    }
549
550    /// Sets the unscaled shape of the collider. The collider's scale will be applied to this shape.
551    pub fn set_shape(&mut self, shape: SharedShape) {
552        self.shape = shape;
553
554        // TODO: The number of subdivisions probably shouldn't be hard-coded
555        if let Ok(scaled) = scale_shape(&self.shape, self.scale, 10) {
556            self.scaled_shape = scaled;
557        } else {
558            log::error!("Failed to create convex hull for scaled collider.");
559        }
560    }
561
562    /// Returns the global scale of the collider.
563    pub fn scale(&self) -> Vector {
564        self.scale
565    }
566
567    /// Set the global scaling factor of this shape.
568    ///
569    /// If the scaling factor is not uniform, and the scaled shape can’t be
570    /// represented as a supported shape, the shape is approximated as
571    /// a convex polygon or polyhedron using `num_subdivisions`.
572    ///
573    /// For example, if a ball was scaled to an ellipse, the new shape would be approximated.
574    pub fn set_scale(&mut self, scale: Vector, num_subdivisions: u32) {
575        if scale == self.scale {
576            return;
577        }
578
579        if scale == Vector::ONE {
580            // Trivial case.
581            self.scaled_shape = self.shape.clone();
582            self.scale = Vector::ONE;
583            return;
584        }
585
586        if let Ok(scaled) = scale_shape(&self.shape, scale, num_subdivisions) {
587            self.scaled_shape = scaled;
588            self.scale = scale;
589        } else {
590            log::error!("Failed to create convex hull for scaled collider.");
591        }
592    }
593
594    /// Projects the given `point` onto `self` transformed by `translation` and `rotation`.
595    /// The returned tuple contains the projected point and whether it is inside the collider.
596    ///
597    /// If `solid` is true and the given `point` is inside of the collider, the projection will be at the point.
598    /// Otherwise, the collider will be treated as hollow, and the projection will be at the collider's boundary.
599    pub fn project_point(
600        &self,
601        translation: impl Into<Position>,
602        rotation: impl Into<Rotation>,
603        point: Vector,
604        solid: bool,
605    ) -> (Vector, bool) {
606        let projection =
607            self.shape_scaled()
608                .project_point(&make_pose(translation, rotation), point, solid);
609        (projection.point, projection.is_inside)
610    }
611
612    /// Computes the minimum distance between the given `point` and `self` transformed by `translation` and `rotation`.
613    ///
614    /// If `solid` is true and the given `point` is inside of the collider, the returned distance will be `0.0`.
615    /// Otherwise, the collider will be treated as hollow, and the distance will be the distance
616    /// to the collider's boundary.
617    pub fn distance_to_point(
618        &self,
619        translation: impl Into<Position>,
620        rotation: impl Into<Rotation>,
621        point: Vector,
622        solid: bool,
623    ) -> Scalar {
624        self.shape_scaled()
625            .distance_to_point(&make_pose(translation, rotation), point, solid)
626    }
627
628    /// Tests whether the given `point` is inside of `self` transformed by `translation` and `rotation`.
629    pub fn contains_point(
630        &self,
631        translation: impl Into<Position>,
632        rotation: impl Into<Rotation>,
633        point: Vector,
634    ) -> bool {
635        self.shape_scaled()
636            .contains_point(&make_pose(translation, rotation), point)
637    }
638
639    /// Computes the distance and normal between the given ray and `self`
640    /// transformed by `translation` and `rotation`.
641    ///
642    /// The returned tuple is in the format `(distance, normal)`.
643    ///
644    /// # Arguments
645    ///
646    /// - `ray_origin`: Where the ray is cast from.
647    /// - `ray_direction`: What direction the ray is cast in.
648    /// - `max_distance`: The maximum distance the ray can travel.
649    /// - `solid`: If true and the ray origin is inside of a collider, the hit point will be the ray origin itself.
650    ///   Otherwise, the collider will be treated as hollow, and the hit point will be at the collider's boundary.
651    pub fn cast_ray(
652        &self,
653        translation: impl Into<Position>,
654        rotation: impl Into<Rotation>,
655        ray_origin: Vector,
656        ray_direction: Vector,
657        max_distance: Scalar,
658        solid: bool,
659    ) -> Option<(Scalar, Vector)> {
660        let hit = self.shape_scaled().cast_ray_and_get_normal(
661            &make_pose(translation, rotation),
662            &parry::query::Ray::new(ray_origin, ray_direction),
663            max_distance,
664            solid,
665        );
666        hit.map(|hit| (hit.time_of_impact, hit.normal))
667    }
668
669    /// Tests whether the given ray intersects `self` transformed by `translation` and `rotation`.
670    ///
671    /// # Arguments
672    ///
673    /// - `ray_origin`: Where the ray is cast from.
674    /// - `ray_direction`: What direction the ray is cast in.
675    /// - `max_distance`: The maximum distance the ray can travel.
676    pub fn intersects_ray(
677        &self,
678        translation: impl Into<Position>,
679        rotation: impl Into<Rotation>,
680        ray_origin: Vector,
681        ray_direction: Vector,
682        max_distance: Scalar,
683    ) -> bool {
684        self.shape_scaled().intersects_ray(
685            &make_pose(translation, rotation),
686            &parry::query::Ray::new(ray_origin, ray_direction),
687            max_distance,
688        )
689    }
690
691    /// Creates a collider with a compound shape defined by a given vector of colliders with a position and a rotation.
692    ///
693    /// Especially for dynamic rigid bodies, compound shape colliders should be preferred over triangle meshes and polylines,
694    /// because convex shapes typically provide more reliable results.
695    ///
696    /// If you want to create a compound shape from a 3D triangle mesh or 2D polyline, consider using the
697    /// [`Collider::convex_decomposition`] method.
698    pub fn compound(
699        shapes: Vec<(
700            impl Into<Position>,
701            impl Into<Rotation>,
702            impl Into<Collider>,
703        )>,
704    ) -> Self {
705        let shapes = shapes
706            .into_iter()
707            .map(|(p, r, c)| {
708                (
709                    make_pose(*p.into(), r.into()),
710                    c.into().shape_scaled().clone(),
711                )
712            })
713            .collect::<Vec<_>>();
714        SharedShape::compound(shapes).into()
715    }
716
717    /// Creates a collider with a circle shape defined by its radius.
718    #[cfg(feature = "2d")]
719    pub fn circle(radius: Scalar) -> Self {
720        SharedShape::ball(radius).into()
721    }
722
723    /// Creates a collider with a sphere shape defined by its radius.
724    #[cfg(feature = "3d")]
725    pub fn sphere(radius: Scalar) -> Self {
726        SharedShape::ball(radius).into()
727    }
728
729    /// Creates a collider with an ellipse shape defined by a half-width and half-height.
730    #[cfg(feature = "2d")]
731    pub fn ellipse(half_width: Scalar, half_height: Scalar) -> Self {
732        SharedShape::new(EllipseColliderShape(Ellipse::new(
733            half_width as f32,
734            half_height as f32,
735        )))
736        .into()
737    }
738
739    /// Creates a collider with a rectangle shape defined by its extents.
740    #[cfg(feature = "2d")]
741    pub fn rectangle(x_length: Scalar, y_length: Scalar) -> Self {
742        SharedShape::cuboid(x_length * 0.5, y_length * 0.5).into()
743    }
744
745    /// Creates a collider with a cuboid shape defined by its extents.
746    #[cfg(feature = "3d")]
747    pub fn cuboid(x_length: Scalar, y_length: Scalar, z_length: Scalar) -> Self {
748        SharedShape::cuboid(x_length * 0.5, y_length * 0.5, z_length * 0.5).into()
749    }
750
751    /// Creates a collider with a rectangle shape defined by its extents and rounded corners.
752    #[cfg(feature = "2d")]
753    pub fn round_rectangle(x_length: Scalar, y_length: Scalar, border_radius: Scalar) -> Self {
754        SharedShape::round_cuboid(x_length * 0.5, y_length * 0.5, border_radius).into()
755    }
756
757    /// Creates a collider with a cuboid shape defined by its extents and rounded corners.
758    #[cfg(feature = "3d")]
759    pub fn round_cuboid(
760        x_length: Scalar,
761        y_length: Scalar,
762        z_length: Scalar,
763        border_radius: Scalar,
764    ) -> Self {
765        SharedShape::round_cuboid(
766            x_length * 0.5,
767            y_length * 0.5,
768            z_length * 0.5,
769            border_radius,
770        )
771        .into()
772    }
773
774    /// Creates a collider with a cylinder shape defined by its radius
775    /// on the `XZ` plane and its height along the `Y` axis.
776    #[cfg(feature = "3d")]
777    pub fn cylinder(radius: Scalar, height: Scalar) -> Self {
778        SharedShape::cylinder(height * 0.5, radius).into()
779    }
780
781    /// Creates a collider with a cone shape defined by the radius of its base
782    /// on the `XZ` plane and its height along the `Y` axis.
783    #[cfg(feature = "3d")]
784    pub fn cone(radius: Scalar, height: Scalar) -> Self {
785        SharedShape::cone(height * 0.5, radius).into()
786    }
787
788    /// Creates a collider with a capsule shape defined by its radius
789    /// and its height along the `Y` axis, excluding the hemispheres.
790    pub fn capsule(radius: Scalar, length: Scalar) -> Self {
791        SharedShape::capsule(
792            Vector::Y * length * 0.5,
793            Vector::NEG_Y * length * 0.5,
794            radius,
795        )
796        .into()
797    }
798
799    /// Creates a collider with a capsule shape defined by its radius and endpoints `a` and `b`.
800    pub fn capsule_endpoints(radius: Scalar, a: Vector, b: Vector) -> Self {
801        SharedShape::capsule(a, b, radius).into()
802    }
803
804    /// Creates a collider with a [half-space](https://en.wikipedia.org/wiki/Half-space_(geometry)) shape
805    /// defined by the outward normal of its planar boundary.
806    pub fn half_space(outward_normal: Vector) -> Self {
807        SharedShape::halfspace(outward_normal.normalize_or_zero()).into()
808    }
809
810    /// Creates a collider with a segment shape defined by its endpoints `a` and `b`.
811    pub fn segment(a: Vector, b: Vector) -> Self {
812        SharedShape::segment(a, b).into()
813    }
814
815    /// Creates a collider with a triangle shape defined by its points `a`, `b`, and `c`.
816    ///
817    /// If the triangle is oriented clockwise, it will be reversed to be counterclockwise
818    /// by swapping `b` and `c`. This is needed for collision detection.
819    ///
820    /// If you know that the given points produce a counterclockwise triangle,
821    /// consider using [`Collider::triangle_unchecked`] instead.
822    #[cfg(feature = "2d")]
823    pub fn triangle(a: Vector, b: Vector, c: Vector) -> Self {
824        let mut triangle = parry::shape::Triangle::new(a, b, c);
825
826        // Make sure the triangle is counterclockwise. This is needed for collision detection.
827        if triangle.orientation(1e-8) == parry::shape::TriangleOrientation::Clockwise {
828            triangle.reverse();
829        }
830
831        SharedShape::new(triangle).into()
832    }
833
834    /// Creates a collider with a triangle shape defined by its points `a`, `b`, and `c`.
835    ///
836    /// The orientation of the triangle is assumed to be counterclockwise.
837    /// This is needed for collision detection.
838    ///
839    /// If you are unsure about the orientation of the triangle, consider using [`Collider::triangle`] instead.
840    #[cfg(feature = "2d")]
841    pub fn triangle_unchecked(a: Vector, b: Vector, c: Vector) -> Self {
842        SharedShape::triangle(a, b, c).into()
843    }
844
845    /// Creates a collider with a triangle shape defined by its points `a`, `b`, and `c`.
846    #[cfg(feature = "3d")]
847    pub fn triangle(a: Vector, b: Vector, c: Vector) -> Self {
848        SharedShape::triangle(a, b, c).into()
849    }
850
851    /// Creates a collider with a regular polygon shape defined by the circumradius and the number of sides.
852    #[cfg(feature = "2d")]
853    pub fn regular_polygon(circumradius: f32, sides: u32) -> Self {
854        RegularPolygon::new(circumradius, sides).collider()
855    }
856
857    /// Creates a collider with a polyline shape defined by its vertices and optionally an index buffer.
858    pub fn polyline(vertices: Vec<Vector>, indices: Option<Vec<[u32; 2]>>) -> Self {
859        SharedShape::polyline(vertices, indices).into()
860    }
861
862    /// Creates a collider with a triangle mesh shape defined by its vertex and index buffers.
863    ///
864    /// Note that the resulting collider will be hollow and have no interior.
865    /// This makes it more prone to tunneling and other collision issues.
866    ///
867    /// The [`CollisionMargin`] component can be used to add thickness to the shape if needed.
868    /// For thin shapes like triangle meshes, it can help improve collision stability and performance.
869    ///
870    /// # Panics
871    ///
872    /// Panics if the given vertex and index buffers do not contain any triangles,
873    /// there are duplicate vertices, or if at least two adjacent triangles have opposite orientations.
874    pub fn trimesh(vertices: Vec<Vector>, indices: Vec<[u32; 3]>) -> Self {
875        Self::try_trimesh(vertices, indices)
876            .unwrap_or_else(|error| panic!("Trimesh creation failed: {error:?}"))
877    }
878
879    /// Tries to create a collider with a triangle mesh shape defined by its vertex and index buffers.
880    ///
881    /// Note that the resulting collider will be hollow and have no interior.
882    /// This makes it more prone to tunneling and other collision issues.
883    ///
884    /// The [`CollisionMargin`] component can be used to add thickness to the shape if needed.
885    /// For thin shapes like triangle meshes, it can help improve collision stability and performance.
886    ///
887    /// # Errors
888    ///
889    /// Returns a [`TrimeshBuilderError`] if the given vertex and index buffers do not contain any triangles,
890    /// there are duplicate vertices, or if at least two adjacent triangles have opposite orientations.
891    pub fn try_trimesh(
892        vertices: Vec<Vector>,
893        indices: Vec<[u32; 3]>,
894    ) -> Result<Self, TrimeshBuilderError> {
895        SharedShape::trimesh(vertices, indices).map(|trimesh| trimesh.into())
896    }
897
898    /// Creates a collider with a triangle mesh shape defined by its vertex and index buffers
899    /// and flags controlling the preprocessing.
900    ///
901    /// Note that the resulting collider will be hollow and have no interior.
902    /// This makes it more prone to tunneling and other collision issues.
903    ///
904    /// The [`CollisionMargin`] component can be used to add thickness to the shape if needed.
905    /// For thin shapes like triangle meshes, it can help improve collision stability and performance.
906    ///
907    /// # Panics
908    ///
909    /// Panics if after preprocessing the given vertex and index buffers do not contain any triangles,
910    /// there are duplicate vertices, or if at least two adjacent triangles have opposite orientations.
911    pub fn trimesh_with_config(
912        vertices: Vec<Vector>,
913        indices: Vec<[u32; 3]>,
914        flags: TrimeshFlags,
915    ) -> Self {
916        Self::try_trimesh_with_config(vertices, indices, flags)
917            .unwrap_or_else(|error| panic!("Trimesh creation failed: {error:?}"))
918    }
919
920    /// Tries to create a collider with a triangle mesh shape defined by its vertex and index buffers
921    /// and flags controlling the preprocessing.
922    ///
923    /// Note that the resulting collider will be hollow and have no interior.
924    /// This makes it more prone to tunneling and other collision issues.
925    ///
926    /// The [`CollisionMargin`] component can be used to add thickness to the shape if needed.
927    /// For thin shapes like triangle meshes, it can help improve collision stability and performance.
928    ///
929    /// # Errors
930    ///
931    /// Returns a [`TrimeshBuilderError`] if after preprocessing the given vertex and index buffers do not contain any triangles,
932    /// there are duplicate vertices, or if at least two adjacent triangles have opposite orientations.
933    pub fn try_trimesh_with_config(
934        vertices: Vec<Vector>,
935        indices: Vec<[u32; 3]>,
936        flags: TrimeshFlags,
937    ) -> Result<Self, TrimeshBuilderError> {
938        SharedShape::trimesh_with_flags(vertices, indices, flags.into())
939            .map(|trimesh| trimesh.into())
940    }
941
942    /// Creates a collider shape with a compound shape obtained from the decomposition of a given polyline
943    /// defined by its vertex and index buffers.
944    #[cfg(feature = "2d")]
945    pub fn convex_decomposition(vertices: Vec<Vector>, indices: Vec<[u32; 2]>) -> Self {
946        SharedShape::convex_decomposition(&vertices, &indices).into()
947    }
948
949    /// Creates a collider shape with a compound shape obtained from the decomposition of a given trimesh
950    /// defined by its vertex and index buffers.
951    #[cfg(feature = "3d")]
952    pub fn convex_decomposition(vertices: Vec<Vector>, indices: Vec<[u32; 3]>) -> Self {
953        SharedShape::convex_decomposition(&vertices, &indices).into()
954    }
955
956    /// Creates a collider shape with a compound shape obtained from the decomposition of a given polyline
957    /// defined by its vertex and index buffers. The given [`VhacdParameters`] are used for configuring
958    /// the decomposition process.
959    #[cfg(feature = "2d")]
960    pub fn convex_decomposition_with_config(
961        vertices: Vec<Vector>,
962        indices: Vec<[u32; 2]>,
963        params: &VhacdParameters,
964    ) -> Self {
965        SharedShape::convex_decomposition_with_params(&vertices, &indices, &params.clone().into())
966            .into()
967    }
968
969    /// Creates a collider shape with a compound shape obtained from the decomposition of a given trimesh
970    /// defined by its vertex and index buffers. The given [`VhacdParameters`] are used for configuring
971    /// the decomposition process.
972    #[cfg(feature = "3d")]
973    pub fn convex_decomposition_with_config(
974        vertices: Vec<Vector>,
975        indices: Vec<[u32; 3]>,
976        params: VhacdParameters,
977    ) -> Self {
978        SharedShape::convex_decomposition_with_params(&vertices, &indices, &params.clone().into())
979            .into()
980    }
981
982    /// Creates a collider with a [convex polygon](https://en.wikipedia.org/wiki/Convex_polygon) shape obtained after computing
983    /// the [convex hull](https://en.wikipedia.org/wiki/Convex_hull) of the given points.
984    #[cfg(feature = "2d")]
985    pub fn convex_hull(points: Vec<Vector>) -> Option<Self> {
986        SharedShape::convex_hull(&points).map(Into::into)
987    }
988
989    /// Creates a collider with a [convex polyhedron](https://en.wikipedia.org/wiki/Convex_polytope) shape obtained after computing
990    /// the [convex hull](https://en.wikipedia.org/wiki/Convex_hull) of the given points.
991    #[cfg(feature = "3d")]
992    pub fn convex_hull(points: Vec<Vector>) -> Option<Self> {
993        SharedShape::convex_hull(&points).map(Into::into)
994    }
995
996    /// Creates a collider with a [convex polygon](https://en.wikipedia.org/wiki/Convex_polygon) shape **without** computing
997    /// the [convex hull](https://en.wikipedia.org/wiki/Convex_hull) of the given points: convexity of the input is
998    /// assumed and not checked.
999    #[cfg(feature = "2d")]
1000    pub fn convex_polyline(points: Vec<Vector>) -> Option<Self> {
1001        SharedShape::convex_polyline(points).map(Into::into)
1002    }
1003
1004    /// Creates a collider shape made of voxels.
1005    ///
1006    /// Each voxel has the size `voxel_size` and grid coordinate given by `grid_coordinates`.
1007    pub fn voxels(voxel_size: Vector, grid_coordinates: &[IVector]) -> Self {
1008        #[cfg(all(feature = "2d", feature = "f64"))]
1009        let grid_coordinates = &grid_coordinates
1010            .iter()
1011            .map(|c| c.as_i64vec2())
1012            .collect::<Vec<_>>();
1013        #[cfg(all(feature = "3d", feature = "f64"))]
1014        let grid_coordinates = &grid_coordinates
1015            .iter()
1016            .map(|c| c.as_i64vec3())
1017            .collect::<Vec<_>>();
1018        let shape = Voxels::new(voxel_size, grid_coordinates);
1019        SharedShape::new(shape).into()
1020    }
1021
1022    /// Creates a collider shape made of voxels.
1023    ///
1024    /// Each voxel has the size `voxel_size` and contains at least one point from `points`.
1025    pub fn voxels_from_points(voxel_size: Vector, points: &[Vector]) -> Self {
1026        SharedShape::voxels_from_points(voxel_size, points).into()
1027    }
1028
1029    /// Creates a voxel collider obtained from the decomposition of the given polyline into voxelized convex parts.
1030    #[cfg(feature = "2d")]
1031    pub fn voxelized_polyline(
1032        vertices: &[Vector],
1033        indices: &[[u32; 2]],
1034        voxel_size: Scalar,
1035        fill_mode: FillMode,
1036    ) -> Self {
1037        SharedShape::voxelized_mesh(vertices, indices, voxel_size, fill_mode.into()).into()
1038    }
1039
1040    /// Creates a voxel collider obtained from the decomposition of the given trimesh into voxelized convex parts.
1041    #[cfg(feature = "3d")]
1042    pub fn voxelized_trimesh(
1043        vertices: &[Vector],
1044        indices: &[[u32; 3]],
1045        voxel_size: Scalar,
1046        fill_mode: FillMode,
1047    ) -> Self {
1048        SharedShape::voxelized_mesh(vertices, indices, voxel_size, fill_mode.into()).into()
1049    }
1050
1051    /// Creates a voxel collider obtained from the decomposition of the given `Mesh` into voxelized convex parts.
1052    ///
1053    /// This method is only available if the `collider-from-mesh` feature is enabled.
1054    #[cfg(feature = "collider-from-mesh")]
1055    pub fn voxelized_trimesh_from_mesh(
1056        mesh: &Mesh,
1057        voxel_size: Scalar,
1058        fill_mode: FillMode,
1059    ) -> Option<Self> {
1060        extract_mesh_vertices_indices(mesh).map(|(vertices, indices)| {
1061            SharedShape::voxelized_mesh(&vertices, &indices, voxel_size, fill_mode.into()).into()
1062        })
1063    }
1064
1065    #[cfg_attr(
1066        feature = "2d",
1067        doc = "Creates a collider with a compound shape obtained from the decomposition of the given polyline into voxelized convex parts."
1068    )]
1069    #[cfg_attr(
1070        feature = "3d",
1071        doc = "Creates a collider with a compound shape obtained from the decomposition of the given trimesh into voxelized convex parts."
1072    )]
1073    pub fn voxelized_convex_decomposition(
1074        vertices: &[Vector],
1075        indices: &[[u32; DIM]],
1076    ) -> Vec<Self> {
1077        Self::voxelized_convex_decomposition_with_config(
1078            vertices,
1079            indices,
1080            &VhacdParameters::default(),
1081        )
1082    }
1083
1084    #[cfg_attr(
1085        feature = "2d",
1086        doc = "Creates a collider with a compound shape obtained from the decomposition of the given polyline into voxelized convex parts."
1087    )]
1088    #[cfg_attr(
1089        feature = "3d",
1090        doc = "Creates a collider with a compound shape obtained from the decomposition of the given trimesh into voxelized convex parts."
1091    )]
1092    pub fn voxelized_convex_decomposition_with_config(
1093        vertices: &[Vector],
1094        indices: &[[u32; DIM]],
1095        parameters: &VhacdParameters,
1096    ) -> Vec<Self> {
1097        SharedShape::voxelized_convex_decomposition_with_params(
1098            vertices,
1099            indices,
1100            &parameters.clone().into(),
1101        )
1102        .into_iter()
1103        .map(|c| c.into())
1104        .collect()
1105    }
1106
1107    /// Creates a collider with a heightfield shape.
1108    ///
1109    /// A 2D heightfield is a segment along the `X` axis, subdivided at regular intervals.
1110    ///
1111    /// `heights` is a list indicating the altitude of each subdivision point, and `scale` controls
1112    /// the scaling factor along each axis.
1113    #[cfg(feature = "2d")]
1114    pub fn heightfield(heights: Vec<Scalar>, scale: Vector) -> Self {
1115        SharedShape::heightfield(heights, scale).into()
1116    }
1117
1118    /// Creates a collider with a heightfield shape.
1119    ///
1120    /// A 3D heightfield is a rectangle on the `XZ` plane, subdivided in a grid pattern at regular intervals.
1121    ///
1122    /// `heights` is a matrix indicating the altitude of each subdivision point. The number of rows indicates
1123    /// the number of subdivisions along the `X` axis, while the number of columns indicates the number of
1124    /// subdivisions along the `Z` axis.
1125    ///
1126    /// `scale` controls the scaling factor along each axis.
1127    #[cfg(feature = "3d")]
1128    pub fn heightfield(heights: Vec<Vec<Scalar>>, scale: Vector) -> Self {
1129        let row_count = heights.len();
1130        let column_count = heights[0].len();
1131        let data: Vec<Scalar> = heights.into_iter().flatten().collect();
1132
1133        assert_eq!(
1134            data.len(),
1135            row_count * column_count,
1136            "Each row in `heights` must have the same amount of points"
1137        );
1138
1139        let heights = parry::utils::Array2::new(row_count, column_count, data);
1140        SharedShape::heightfield(heights, scale).into()
1141    }
1142
1143    /// Creates a collider with a triangle mesh shape from a `Mesh`.
1144    ///
1145    /// Note that the resulting collider will be hollow and have no interior.
1146    /// This makes it more prone to tunneling and other collision issues.
1147    ///
1148    /// The [`CollisionMargin`] component can be used to add thickness to the shape if needed.
1149    /// For thin shapes like triangle meshes, it can help improve collision stability and performance.
1150    ///
1151    /// When encountering issues with
1152    /// [Ghost Collisions](https://box2d.org/posts/2020/06/ghost-collisions/) where bodies collide
1153    /// with the internal edges of the trimesh, try setting the flag
1154    /// [`TrimeshFlags::FIX_INTERNAL_EDGES`] via [`Self::trimesh_from_mesh_with_config`], which
1155    /// should fix the issue for edges where none of the adjacent triangles border their neighbor
1156    /// triangles convexly (with an angle less than 180°). Note that this will also not fix the
1157    /// issues with boundary edges between two different colliders.
1158    ///
1159    /// # Example
1160    ///
1161    /// ```
1162    /// use avian3d::prelude::*;
1163    /// use bevy::prelude::*;
1164    ///
1165    /// fn setup(mut commands: Commands, mut meshes: ResMut<Assets<Mesh>>) {
1166    ///     let mesh = Mesh::from(Cuboid::default());
1167    ///     commands.spawn((
1168    ///         Collider::trimesh_from_mesh(&mesh).unwrap(),
1169    ///         Mesh3d(meshes.add(mesh)),
1170    ///     ));
1171    /// }
1172    /// ```
1173    #[cfg(feature = "collider-from-mesh")]
1174    pub fn trimesh_from_mesh(mesh: &Mesh) -> Option<Self> {
1175        extract_mesh_vertices_indices(mesh).and_then(|(vertices, indices)| {
1176            SharedShape::trimesh_with_flags(
1177                vertices,
1178                indices,
1179                TrimeshFlags::MERGE_DUPLICATE_VERTICES.into(),
1180            )
1181            .map(|trimesh| trimesh.into())
1182            .ok()
1183        })
1184    }
1185
1186    /// Creates a collider with a triangle mesh shape from a `Mesh` using the given [`TrimeshFlags`]
1187    /// for controlling the preprocessing.
1188    ///
1189    /// Note that the resulting collider will be hollow and have no interior.
1190    /// This makes it more prone to tunneling and other collision issues.
1191    ///
1192    /// The [`CollisionMargin`] component can be used to add thickness to the shape if needed.
1193    /// For thin shapes like triangle meshes, it can help improve collision stability and performance.
1194    ///
1195    /// When encountering issues with
1196    /// [Ghost Collisions](https://box2d.org/posts/2020/06/ghost-collisions/) where bodies collide
1197    /// with the internal edges of the trimesh, try setting the flag
1198    /// [`TrimeshFlags::FIX_INTERNAL_EDGES`], which should fix the issue for edges where none of
1199    /// the adjacent triangles border their neighbor triangles convexly (with an angle less than
1200    /// 180°). Note that this will also not fix the issues with boundary edges between two different
1201    /// colliders.
1202    ///
1203    /// # Example
1204    ///
1205    /// ```
1206    /// use avian3d::prelude::*;
1207    /// use bevy::prelude::*;
1208    ///
1209    /// fn setup(mut commands: Commands, mut meshes: ResMut<Assets<Mesh>>) {
1210    ///     let mesh = Mesh::from(Cuboid::default());
1211    ///     commands.spawn((
1212    ///         Collider::trimesh_from_mesh_with_config(&mesh, TrimeshFlags::all()).unwrap(),
1213    ///         Mesh3d(meshes.add(mesh)),
1214    ///     ));
1215    /// }
1216    /// ```
1217    #[cfg(feature = "collider-from-mesh")]
1218    pub fn trimesh_from_mesh_with_config(mesh: &Mesh, flags: TrimeshFlags) -> Option<Self> {
1219        extract_mesh_vertices_indices(mesh).and_then(|(vertices, indices)| {
1220            SharedShape::trimesh_with_flags(vertices, indices, flags.into())
1221                .map(|trimesh| trimesh.into())
1222                .ok()
1223        })
1224    }
1225
1226    /// Creates a collider with a convex polygon shape obtained from the convex hull of a `Mesh`.
1227    ///
1228    /// # Example
1229    ///
1230    /// ```
1231    /// use avian3d::prelude::*;
1232    /// use bevy::prelude::*;
1233    ///
1234    /// fn setup(mut commands: Commands, mut meshes: ResMut<Assets<Mesh>>) {
1235    ///     let mesh = Mesh::from(Cuboid::default());
1236    ///     commands.spawn((
1237    ///         Collider::convex_hull_from_mesh(&mesh).unwrap(),
1238    ///         Mesh3d(meshes.add(mesh)),
1239    ///     ));
1240    /// }
1241    /// ```
1242    #[cfg(feature = "collider-from-mesh")]
1243    pub fn convex_hull_from_mesh(mesh: &Mesh) -> Option<Self> {
1244        extract_mesh_vertices_indices(mesh)
1245            .and_then(|(vertices, _)| SharedShape::convex_hull(&vertices).map(|shape| shape.into()))
1246    }
1247
1248    /// Creates a compound shape obtained from the decomposition of a `Mesh`.
1249    ///
1250    /// # Example
1251    ///
1252    /// ```
1253    /// use avian3d::prelude::*;
1254    /// use bevy::prelude::*;
1255    ///
1256    /// fn setup(mut commands: Commands, mut meshes: ResMut<Assets<Mesh>>) {
1257    ///     let mesh = Mesh::from(Cuboid::default());
1258    ///     commands.spawn((
1259    ///         Collider::convex_decomposition_from_mesh(&mesh).unwrap(),
1260    ///         Mesh3d(meshes.add(mesh)),
1261    ///     ));
1262    /// }
1263    /// ```
1264    #[cfg(feature = "collider-from-mesh")]
1265    pub fn convex_decomposition_from_mesh(mesh: &Mesh) -> Option<Self> {
1266        extract_mesh_vertices_indices(mesh).map(|(vertices, indices)| {
1267            SharedShape::convex_decomposition(&vertices, &indices).into()
1268        })
1269    }
1270
1271    /// Creates a compound shape obtained from the decomposition of a `Mesh`
1272    /// with the given [`VhacdParameters`] passed to the decomposition algorithm.
1273    ///
1274    /// # Example
1275    ///
1276    /// ```
1277    /// use avian3d::prelude::*;
1278    /// use bevy::prelude::*;
1279    ///
1280    /// fn setup(mut commands: Commands, mut meshes: ResMut<Assets<Mesh>>) {
1281    ///     let mesh = Mesh::from(Cuboid::default());
1282    ///     let config = VhacdParameters {
1283    ///         convex_hull_approximation: false,
1284    ///         ..default()
1285    ///     };
1286    ///     commands.spawn((
1287    ///         Collider::convex_decomposition_from_mesh_with_config(&mesh, &config).unwrap(),
1288    ///         Mesh3d(meshes.add(mesh)),
1289    ///     ));
1290    /// }
1291    /// ```
1292    #[cfg(feature = "collider-from-mesh")]
1293    pub fn convex_decomposition_from_mesh_with_config(
1294        mesh: &Mesh,
1295        parameters: &VhacdParameters,
1296    ) -> Option<Self> {
1297        extract_mesh_vertices_indices(mesh).map(|(vertices, indices)| {
1298            SharedShape::convex_decomposition_with_params(
1299                &vertices,
1300                &indices,
1301                &parameters.clone().into(),
1302            )
1303            .into()
1304        })
1305    }
1306
1307    /// Attempts to create a collider with the given [`ColliderConstructor`].
1308    /// By using this, you can serialize and deserialize the collider's creation method
1309    /// separately from the collider itself via the [`ColliderConstructor`] enum.
1310    ///
1311    #[cfg_attr(
1312        feature = "collider-from-mesh",
1313        doc = "Returns `None` in the following cases:
1314- The given [`ColliderConstructor`] requires a mesh, but none was provided.
1315- Creating the collider from the given [`ColliderConstructor`] failed."
1316    )]
1317    #[cfg_attr(
1318        not(feature = "collider-from-mesh"),
1319        doc = "Returns `None` if creating the collider from the given [`ColliderConstructor`] failed."
1320    )]
1321    pub fn try_from_constructor(
1322        collider_constructor: ColliderConstructor,
1323        #[cfg(feature = "collider-from-mesh")] mesh: Option<&Mesh>,
1324    ) -> Option<Self> {
1325        match collider_constructor {
1326            #[cfg(feature = "2d")]
1327            ColliderConstructor::Circle { radius } => Some(Self::circle(radius)),
1328            #[cfg(feature = "3d")]
1329            ColliderConstructor::Sphere { radius } => Some(Self::sphere(radius)),
1330            #[cfg(feature = "2d")]
1331            ColliderConstructor::Ellipse {
1332                half_width,
1333                half_height,
1334            } => Some(Self::ellipse(half_width, half_height)),
1335            #[cfg(feature = "2d")]
1336            ColliderConstructor::Rectangle { x_length, y_length } => {
1337                Some(Self::rectangle(x_length, y_length))
1338            }
1339            #[cfg(feature = "3d")]
1340            ColliderConstructor::Cuboid {
1341                x_length,
1342                y_length,
1343                z_length,
1344            } => Some(Self::cuboid(x_length, y_length, z_length)),
1345            #[cfg(feature = "2d")]
1346            ColliderConstructor::RoundRectangle {
1347                x_length,
1348                y_length,
1349                border_radius,
1350            } => Some(Self::round_rectangle(x_length, y_length, border_radius)),
1351            #[cfg(feature = "3d")]
1352            ColliderConstructor::RoundCuboid {
1353                x_length,
1354                y_length,
1355                z_length,
1356                border_radius,
1357            } => Some(Self::round_cuboid(
1358                x_length,
1359                y_length,
1360                z_length,
1361                border_radius,
1362            )),
1363            #[cfg(feature = "3d")]
1364            ColliderConstructor::Cylinder { radius, height } => {
1365                Some(Self::cylinder(radius, height))
1366            }
1367            #[cfg(feature = "3d")]
1368            ColliderConstructor::Cone { radius, height } => Some(Self::cone(radius, height)),
1369            ColliderConstructor::Capsule { radius, height } => Some(Self::capsule(radius, height)),
1370            ColliderConstructor::CapsuleEndpoints { radius, a, b } => {
1371                Some(Self::capsule_endpoints(radius, a, b))
1372            }
1373            ColliderConstructor::HalfSpace { outward_normal } => {
1374                Some(Self::half_space(outward_normal))
1375            }
1376            ColliderConstructor::Segment { a, b } => Some(Self::segment(a, b)),
1377            ColliderConstructor::Triangle { a, b, c } => Some(Self::triangle(a, b, c)),
1378            #[cfg(feature = "2d")]
1379            ColliderConstructor::RegularPolygon {
1380                circumradius,
1381                sides,
1382            } => Some(Self::regular_polygon(circumradius, sides)),
1383            ColliderConstructor::Polyline { vertices, indices } => {
1384                Some(Self::polyline(vertices, indices))
1385            }
1386            ColliderConstructor::Trimesh { vertices, indices } => {
1387                Some(Self::trimesh(vertices, indices))
1388            }
1389            ColliderConstructor::TrimeshWithConfig {
1390                vertices,
1391                indices,
1392                flags,
1393            } => Some(Self::trimesh_with_config(vertices, indices, flags)),
1394            #[cfg(feature = "2d")]
1395            ColliderConstructor::ConvexDecomposition { vertices, indices } => {
1396                Some(Self::convex_decomposition(vertices, indices))
1397            }
1398            #[cfg(feature = "3d")]
1399            ColliderConstructor::ConvexDecomposition { vertices, indices } => {
1400                Some(Self::convex_decomposition(vertices, indices))
1401            }
1402            #[cfg(feature = "2d")]
1403            ColliderConstructor::ConvexDecompositionWithConfig {
1404                vertices,
1405                indices,
1406                params,
1407            } => Some(Self::convex_decomposition_with_config(
1408                vertices, indices, &params,
1409            )),
1410            #[cfg(feature = "3d")]
1411            ColliderConstructor::ConvexDecompositionWithConfig {
1412                vertices,
1413                indices,
1414                params,
1415            } => Some(Self::convex_decomposition_with_config(
1416                vertices, indices, params,
1417            )),
1418            #[cfg(feature = "2d")]
1419            ColliderConstructor::ConvexHull { points } => Self::convex_hull(points),
1420            #[cfg(feature = "3d")]
1421            ColliderConstructor::ConvexHull { points } => Self::convex_hull(points),
1422            #[cfg(feature = "2d")]
1423            ColliderConstructor::ConvexPolyline { points } => Self::convex_polyline(points),
1424            ColliderConstructor::Voxels {
1425                voxel_size,
1426                grid_coordinates,
1427            } => Some(Self::voxels(voxel_size, &grid_coordinates)),
1428            #[cfg(feature = "2d")]
1429            ColliderConstructor::VoxelizedPolyline {
1430                vertices,
1431                indices,
1432                voxel_size,
1433                fill_mode,
1434            } => Some(Self::voxelized_polyline(
1435                &vertices, &indices, voxel_size, fill_mode,
1436            )),
1437            #[cfg(feature = "3d")]
1438            ColliderConstructor::VoxelizedTrimesh {
1439                vertices,
1440                indices,
1441                voxel_size,
1442                fill_mode,
1443            } => Some(Self::voxelized_trimesh(
1444                &vertices, &indices, voxel_size, fill_mode,
1445            )),
1446            #[cfg(feature = "2d")]
1447            ColliderConstructor::Heightfield { heights, scale } => {
1448                Some(Self::heightfield(heights, scale))
1449            }
1450            #[cfg(feature = "3d")]
1451            ColliderConstructor::Heightfield { heights, scale } => {
1452                Some(Self::heightfield(heights, scale))
1453            }
1454            #[cfg(feature = "collider-from-mesh")]
1455            ColliderConstructor::TrimeshFromMesh => Self::trimesh_from_mesh(mesh?),
1456            #[cfg(all(feature = "collider-from-mesh", feature = "default-collider"))]
1457            ColliderConstructor::TrimeshFromMeshWithConfig(flags) => {
1458                Self::trimesh_from_mesh_with_config(mesh?, flags)
1459            }
1460            #[cfg(feature = "collider-from-mesh")]
1461            ColliderConstructor::ConvexDecompositionFromMesh => {
1462                Self::convex_decomposition_from_mesh(mesh?)
1463            }
1464            #[cfg(all(feature = "collider-from-mesh", feature = "default-collider"))]
1465            ColliderConstructor::ConvexDecompositionFromMeshWithConfig(params) => {
1466                Self::convex_decomposition_from_mesh_with_config(mesh?, &params)
1467            }
1468            #[cfg(feature = "collider-from-mesh")]
1469            ColliderConstructor::ConvexHullFromMesh => Self::convex_hull_from_mesh(mesh?),
1470            #[cfg(feature = "collider-from-mesh")]
1471            ColliderConstructor::VoxelizedTrimeshFromMesh {
1472                voxel_size,
1473                fill_mode,
1474            } => Self::voxelized_trimesh_from_mesh(mesh?, voxel_size, fill_mode),
1475            ColliderConstructor::Compound(compound_constructors) => {
1476                let shapes: Vec<_> =
1477                    ColliderConstructor::flatten_compound_constructors(compound_constructors)
1478                        .into_iter()
1479                        .filter_map(|(position, rotation, collider_constructor)| {
1480                            Self::try_from_constructor(
1481                                collider_constructor,
1482                                #[cfg(feature = "collider-from-mesh")]
1483                                mesh,
1484                            )
1485                            .map(|collider| (position, rotation, collider))
1486                        })
1487                        .collect();
1488
1489                (!shapes.is_empty()).then(|| Self::compound(shapes))
1490            }
1491        }
1492    }
1493}
1494
1495#[cfg(feature = "collider-from-mesh")]
1496type VerticesIndices = (Vec<Vector>, Vec<[u32; 3]>);
1497
1498#[cfg(feature = "collider-from-mesh")]
1499fn extract_mesh_vertices_indices(mesh: &Mesh) -> Option<VerticesIndices> {
1500    let vertices = mesh.attribute(Mesh::ATTRIBUTE_POSITION)?;
1501    let indices = mesh.indices()?;
1502
1503    let vtx: Vec<_> = match vertices {
1504        VertexAttributeValues::Float32(vtx) => Some(
1505            vtx.chunks(3)
1506                .map(|v| [v[0] as Scalar, v[1] as Scalar, v[2] as Scalar].into())
1507                .collect(),
1508        ),
1509        VertexAttributeValues::Float32x3(vtx) => Some(
1510            vtx.iter()
1511                .map(|v| [v[0] as Scalar, v[1] as Scalar, v[2] as Scalar].into())
1512                .collect(),
1513        ),
1514        _ => None,
1515    }?;
1516
1517    let idx = match indices {
1518        Indices::U16(idx) => idx
1519            .chunks_exact(3)
1520            .map(|i| [i[0] as u32, i[1] as u32, i[2] as u32])
1521            .collect(),
1522        Indices::U32(idx) => idx.chunks_exact(3).map(|i| [i[0], i[1], i[2]]).collect(),
1523    };
1524
1525    Some((vtx, idx))
1526}
1527
1528fn scale_shape(
1529    shape: &SharedShape,
1530    scale: Vector,
1531    num_subdivisions: u32,
1532) -> Result<SharedShape, UnsupportedShape> {
1533    let scale = scale.abs();
1534    match shape.as_typed_shape() {
1535        TypedShape::Cuboid(s) => Ok(SharedShape::new(s.scaled(scale.abs()))),
1536        TypedShape::RoundCuboid(s) => Ok(SharedShape::new(RoundShape {
1537            border_radius: s.border_radius,
1538            inner_shape: s.inner_shape.scaled(scale.abs()),
1539        })),
1540        TypedShape::Capsule(c) => match c.scaled(scale.abs(), num_subdivisions) {
1541            None => {
1542                log::error!("Failed to apply scale {} to Capsule shape.", scale);
1543                Ok(SharedShape::ball(0.0))
1544            }
1545            Some(Either::Left(b)) => Ok(SharedShape::new(b)),
1546            Some(Either::Right(b)) => Ok(SharedShape::new(b)),
1547        },
1548        TypedShape::Ball(b) => {
1549            #[cfg(feature = "2d")]
1550            {
1551                if scale.x == scale.y {
1552                    Ok(SharedShape::ball(b.radius * scale.x.abs()))
1553                } else {
1554                    // A 2D circle becomes an ellipse when scaled non-uniformly.
1555                    Ok(SharedShape::new(EllipseColliderShape(Ellipse {
1556                        half_size: Vec2::splat(b.radius as f32) * scale.f32().abs(),
1557                    })))
1558                }
1559            }
1560            #[cfg(feature = "3d")]
1561            match b.scaled(scale.abs(), num_subdivisions) {
1562                None => {
1563                    log::error!("Failed to apply scale {} to Ball shape.", scale);
1564                    Ok(SharedShape::ball(0.0))
1565                }
1566                Some(Either::Left(b)) => Ok(SharedShape::new(b)),
1567                Some(Either::Right(b)) => Ok(SharedShape::new(b)),
1568            }
1569        }
1570        TypedShape::Segment(s) => Ok(SharedShape::new(s.scaled(scale))),
1571        TypedShape::Triangle(t) => Ok(SharedShape::new(t.scaled(scale))),
1572        TypedShape::RoundTriangle(t) => Ok(SharedShape::new(RoundShape {
1573            border_radius: t.border_radius,
1574            inner_shape: t.inner_shape.scaled(scale),
1575        })),
1576        TypedShape::TriMesh(t) => Ok(SharedShape::new(t.clone().scaled(scale))),
1577        TypedShape::Polyline(p) => Ok(SharedShape::new(p.clone().scaled(scale))),
1578        TypedShape::HalfSpace(h) => match h.scaled(scale) {
1579            None => {
1580                log::error!("Failed to apply scale {} to HalfSpace shape.", scale);
1581                Ok(SharedShape::ball(0.0))
1582            }
1583            Some(scaled) => Ok(SharedShape::new(scaled)),
1584        },
1585        TypedShape::Voxels(v) => Ok(SharedShape::new(v.clone().scaled(scale))),
1586        TypedShape::HeightField(h) => Ok(SharedShape::new(h.clone().scaled(scale))),
1587        #[cfg(feature = "2d")]
1588        TypedShape::ConvexPolygon(cp) => match cp.clone().scaled(scale) {
1589            None => {
1590                log::error!("Failed to apply scale {} to ConvexPolygon shape.", scale);
1591                Ok(SharedShape::ball(0.0))
1592            }
1593            Some(scaled) => Ok(SharedShape::new(scaled)),
1594        },
1595        #[cfg(feature = "2d")]
1596        TypedShape::RoundConvexPolygon(cp) => match cp.inner_shape.clone().scaled(scale) {
1597            None => {
1598                log::error!(
1599                    "Failed to apply scale {} to RoundConvexPolygon shape.",
1600                    scale
1601                );
1602                Ok(SharedShape::ball(0.0))
1603            }
1604            Some(scaled) => Ok(SharedShape::new(RoundShape {
1605                border_radius: cp.border_radius,
1606                inner_shape: scaled,
1607            })),
1608        },
1609        #[cfg(feature = "3d")]
1610        TypedShape::ConvexPolyhedron(cp) => match cp.clone().scaled(scale) {
1611            None => {
1612                log::error!("Failed to apply scale {} to ConvexPolyhedron shape.", scale);
1613                Ok(SharedShape::ball(0.0))
1614            }
1615            Some(scaled) => Ok(SharedShape::new(scaled)),
1616        },
1617        #[cfg(feature = "3d")]
1618        TypedShape::RoundConvexPolyhedron(cp) => match cp.clone().inner_shape.scaled(scale) {
1619            None => {
1620                log::error!(
1621                    "Failed to apply scale {} to RoundConvexPolyhedron shape.",
1622                    scale
1623                );
1624                Ok(SharedShape::ball(0.0))
1625            }
1626            Some(scaled) => Ok(SharedShape::new(RoundShape {
1627                border_radius: cp.border_radius,
1628                inner_shape: scaled,
1629            })),
1630        },
1631        #[cfg(feature = "3d")]
1632        TypedShape::Cylinder(c) => match c.scaled(scale.abs(), num_subdivisions) {
1633            None => {
1634                log::error!("Failed to apply scale {} to Cylinder shape.", scale);
1635                Ok(SharedShape::ball(0.0))
1636            }
1637            Some(Either::Left(b)) => Ok(SharedShape::new(b)),
1638            Some(Either::Right(b)) => Ok(SharedShape::new(b)),
1639        },
1640        #[cfg(feature = "3d")]
1641        TypedShape::RoundCylinder(c) => match c.inner_shape.scaled(scale.abs(), num_subdivisions) {
1642            None => {
1643                log::error!("Failed to apply scale {} to RoundCylinder shape.", scale);
1644                Ok(SharedShape::ball(0.0))
1645            }
1646            Some(Either::Left(scaled)) => Ok(SharedShape::new(RoundShape {
1647                border_radius: c.border_radius,
1648                inner_shape: scaled,
1649            })),
1650            Some(Either::Right(scaled)) => Ok(SharedShape::new(RoundShape {
1651                border_radius: c.border_radius,
1652                inner_shape: scaled,
1653            })),
1654        },
1655        #[cfg(feature = "3d")]
1656        TypedShape::Cone(c) => match c.scaled(scale, num_subdivisions) {
1657            None => {
1658                log::error!("Failed to apply scale {} to Cone shape.", scale);
1659                Ok(SharedShape::ball(0.0))
1660            }
1661            Some(Either::Left(b)) => Ok(SharedShape::new(b)),
1662            Some(Either::Right(b)) => Ok(SharedShape::new(b)),
1663        },
1664        #[cfg(feature = "3d")]
1665        TypedShape::RoundCone(c) => match c.inner_shape.scaled(scale, num_subdivisions) {
1666            None => {
1667                log::error!("Failed to apply scale {} to RoundCone shape.", scale);
1668                Ok(SharedShape::ball(0.0))
1669            }
1670            Some(Either::Left(scaled)) => Ok(SharedShape::new(RoundShape {
1671                border_radius: c.border_radius,
1672                inner_shape: scaled,
1673            })),
1674            Some(Either::Right(scaled)) => Ok(SharedShape::new(RoundShape {
1675                border_radius: c.border_radius,
1676                inner_shape: scaled,
1677            })),
1678        },
1679        TypedShape::Compound(c) => {
1680            let mut scaled = Vec::with_capacity(c.shapes().len());
1681
1682            for (pose, shape) in c.shapes() {
1683                #[cfg(feature = "2d")]
1684                scaled.push((
1685                    make_pose(
1686                        pose.translation * scale,
1687                        Rotation::radians(pose.rotation.angle()),
1688                    ),
1689                    scale_shape(shape, scale, num_subdivisions)?,
1690                ));
1691                #[cfg(feature = "3d")]
1692                scaled.push((
1693                    make_pose(pose.translation * scale, pose.rotation),
1694                    scale_shape(shape, scale, num_subdivisions)?,
1695                ));
1696            }
1697            Ok(SharedShape::compound(scaled))
1698        }
1699        TypedShape::Custom(_shape) => {
1700            #[cfg(feature = "2d")]
1701            {
1702                if let Some(ellipse) = _shape.as_shape::<EllipseColliderShape>() {
1703                    return Ok(SharedShape::new(EllipseColliderShape(Ellipse {
1704                        half_size: ellipse.half_size * scale.f32().abs(),
1705                    })));
1706                }
1707                if let Some(polygon) = _shape.as_shape::<RegularPolygonColliderShape>() {
1708                    if scale.x == scale.y {
1709                        return Ok(SharedShape::new(RegularPolygonColliderShape(
1710                            RegularPolygon::new(
1711                                polygon.circumradius() * scale.x.abs() as f32,
1712                                polygon.sides,
1713                            ),
1714                        )));
1715                    } else {
1716                        let vertices = polygon
1717                            .vertices(0.0)
1718                            .into_iter()
1719                            .map(|v| v.adjust_precision())
1720                            .collect::<Vec<_>>();
1721
1722                        return scale_shape(
1723                            &SharedShape::convex_hull(&vertices).unwrap(),
1724                            scale,
1725                            num_subdivisions,
1726                        );
1727                    }
1728                }
1729            }
1730            Err(parry::query::Unsupported)
1731        }
1732    }
1733}
1734
1735#[cfg(all(test, feature = "3d"))]
1736mod tests {
1737    use super::*;
1738    use approx::assert_relative_eq;
1739
1740    #[test]
1741    fn test_flatten_compound_constructors() {
1742        let input = vec![
1743            (
1744                Position(Vector::new(10.0, 0.0, 0.0)),
1745                Rotation::default(),
1746                ColliderConstructor::Sphere { radius: 1.0 },
1747            ),
1748            (
1749                Position(Vector::new(5.0, 0.0, 0.0)),
1750                Rotation::from(Quaternion::from_rotation_z(PI / 2.0)),
1751                ColliderConstructor::Compound(vec![
1752                    (
1753                        Position(Vector::new(2.0, 0.0, 0.0)),
1754                        Rotation::from(Quaternion::from_rotation_y(PI)),
1755                        ColliderConstructor::Compound(vec![(
1756                            Position(Vector::new(1.0, 0.0, 0.0)),
1757                            Rotation::default(),
1758                            ColliderConstructor::Sphere { radius: 0.5 },
1759                        )]),
1760                    ),
1761                    (
1762                        Position(Vector::new(0.0, 3.0, 0.0)),
1763                        Rotation::default(),
1764                        ColliderConstructor::Sphere { radius: 0.25 },
1765                    ),
1766                ]),
1767            ),
1768        ];
1769
1770        let flattened = ColliderConstructor::flatten_compound_constructors(input);
1771        assert_eq!(flattened.len(), 3);
1772
1773        let unchanged_simple_sphere = &flattened[0];
1774        let flattened_grandchild = &flattened[1];
1775        let flattened_sibling = &flattened[2];
1776
1777        // Top level colliders should remain unchanged
1778        assert_eq!(
1779            unchanged_simple_sphere.0,
1780            Position(Vector::new(10.0, 0.0, 0.0))
1781        );
1782        assert_eq!(unchanged_simple_sphere.1, Rotation::default());
1783
1784        // Grandchild local position: (1, 0, 0)
1785        // 1. Apply parent's 180 Y rotation -> (-1, 0, 0)
1786        // 2. Add parent's position (2, 0, 0) -> (1, 0, 0)
1787        // 3. Apply grandparent's 90 Z rotation -> (0, 1, 0)
1788        // 4. Add grandparent's position (5, 0, 0) -> (5, 1, 0)
1789        let expected_grandchild_world_pos = Vector::new(5.0, 1.0, 0.0);
1790        let actual_grandchild_world_pos = flattened_grandchild.0.0;
1791
1792        assert_relative_eq!(
1793            actual_grandchild_world_pos.x,
1794            expected_grandchild_world_pos.x,
1795            epsilon = 1e-6
1796        );
1797        assert_relative_eq!(
1798            actual_grandchild_world_pos.y,
1799            expected_grandchild_world_pos.y,
1800            epsilon = 1e-6
1801        );
1802        assert_relative_eq!(
1803            actual_grandchild_world_pos.z,
1804            expected_grandchild_world_pos.z,
1805            epsilon = 1e-6
1806        );
1807
1808        // Sibling local position: (0, 3, 0)
1809        // 1. Apply parent's 90 Z rotation -> (-3, 0, 0)
1810        // 2. Add parent's position (5, 0, 0) -> (2, 0, 0)
1811        let expected_sibling_world_pos = Vector::new(2.0, 0.0, 0.0);
1812        let actual_sibling_world_pos = flattened_sibling.0.0;
1813
1814        assert_relative_eq!(
1815            actual_sibling_world_pos.x,
1816            expected_sibling_world_pos.x,
1817            epsilon = 1e-6
1818        );
1819        assert_relative_eq!(
1820            actual_sibling_world_pos.y,
1821            expected_sibling_world_pos.y,
1822            epsilon = 1e-6
1823        );
1824        assert_relative_eq!(
1825            actual_sibling_world_pos.z,
1826            expected_sibling_world_pos.z,
1827            epsilon = 1e-6
1828        );
1829    }
1830}