avian2d/collision/collider/
backend.rs

1//! Handles generic collider backend logic, like initializing colliders and AABBs and updating related components.
2//!
3//! See [`ColliderBackendPlugin`].
4
5use core::marker::PhantomData;
6
7#[cfg(all(feature = "collider-from-mesh", feature = "default-collider"))]
8use crate::collision::collider::cache::ColliderCache;
9use crate::{
10    collision::broad_phase::BroadPhaseSystems,
11    physics_transform::{PhysicsTransformConfig, PhysicsTransformSystems, init_physics_transform},
12    prelude::*,
13};
14#[cfg(all(feature = "bevy_scene", feature = "default-collider"))]
15use bevy::scene::SceneInstance;
16use bevy::{
17    ecs::{intern::Interned, schedule::ScheduleLabel, system::StaticSystemParam},
18    prelude::*,
19};
20use mass_properties::{MassPropertySystems, components::RecomputeMassProperties};
21
22/// A plugin for handling generic collider backend logic.
23///
24/// - Initializes colliders, handles [`ColliderConstructor`] and [`ColliderConstructorHierarchy`].
25/// - Updates [`ColliderAabb`]s.
26/// - Updates collider scale based on `Transform` scale.
27/// - Updates [`ColliderMassProperties`].
28///
29/// This plugin should typically be used together with the [`ColliderHierarchyPlugin`].
30///
31/// # Custom Collision Backends
32///
33/// By default, [`PhysicsPlugins`] adds this plugin for the [`Collider`] component.
34/// You can also create custom collider backends by implementing the [`AnyCollider`]
35/// and [`ScalableCollider`] traits for a type.
36///
37/// To use a custom collider backend, simply add the [`ColliderBackendPlugin`] with your collider type:
38///
39/// ```no_run
40#[cfg_attr(feature = "2d", doc = "use avian2d::prelude::*;")]
41#[cfg_attr(feature = "3d", doc = "use avian3d::prelude::*;")]
42/// use bevy::prelude::*;
43/// #
44/// # type MyCollider = Collider;
45///
46/// fn main() {
47///     App::new()
48///         .add_plugins((
49///             DefaultPlugins,
50///             PhysicsPlugins::default(),
51///             // MyCollider must implement AnyCollider and ScalableCollider.
52///             ColliderBackendPlugin::<MyCollider>::default(),
53///             // To enable collision detection for the collider,
54///             // we also need to add the NarrowPhasePlugin for it.
55///             NarrowPhasePlugin::<MyCollider>::default(),
56///         ))
57///         // ...your other plugins, systems and resources
58///         .run();
59/// }
60/// ```
61///
62/// Assuming you have implemented the required traits correctly,
63/// it should now work with the rest of the engine just like normal [`Collider`]s!
64///
65/// **Note**: [Spatial queries](spatial_query) are not supported for custom colliders yet.
66pub struct ColliderBackendPlugin<C: ScalableCollider> {
67    schedule: Interned<dyn ScheduleLabel>,
68    _phantom: PhantomData<C>,
69}
70
71impl<C: ScalableCollider> ColliderBackendPlugin<C> {
72    /// Creates a [`ColliderBackendPlugin`] with the schedule that is used for running the [`PhysicsSchedule`].
73    ///
74    /// The default schedule is `FixedPostUpdate`.
75    pub fn new(schedule: impl ScheduleLabel) -> Self {
76        Self {
77            schedule: schedule.intern(),
78            _phantom: PhantomData,
79        }
80    }
81}
82
83impl<C: ScalableCollider> Default for ColliderBackendPlugin<C> {
84    fn default() -> Self {
85        Self {
86            schedule: FixedPostUpdate.intern(),
87            _phantom: PhantomData,
88        }
89    }
90}
91
92impl<C: ScalableCollider> Plugin for ColliderBackendPlugin<C> {
93    fn build(&self, app: &mut App) {
94        // Register required components for the collider type.
95        let _ = app.try_register_required_components_with::<C, Position>(|| Position::PLACEHOLDER);
96        let _ = app.try_register_required_components_with::<C, Rotation>(|| Rotation::PLACEHOLDER);
97        let _ = app.try_register_required_components::<C, ColliderMarker>();
98        let _ = app.try_register_required_components::<C, ColliderAabb>();
99        let _ = app.try_register_required_components::<C, CollisionLayers>();
100        let _ = app.try_register_required_components::<C, ColliderDensity>();
101        let _ = app.try_register_required_components::<C, ColliderMassProperties>();
102
103        // Make sure the necessary resources are initialized.
104        app.init_resource::<PhysicsTransformConfig>();
105        app.init_resource::<NarrowPhaseConfig>();
106        app.init_resource::<PhysicsLengthUnit>();
107
108        let hooks = app.world_mut().register_component_hooks::<C>();
109
110        // Initialize missing components for colliders.
111        hooks
112            .on_add(|mut world, ctx| {
113                // Initialize the global physics transform for the collider.
114                // Avoid doing this twice for rigid bodies added at the same time.
115                // TODO: The special case for rigid bodies is a bit of a hack here.
116                if !world.entity(ctx.entity).contains::<RigidBody>() {
117                    init_physics_transform(&mut world, &ctx);
118                }
119            })
120            .on_insert(|mut world, ctx| {
121                let scale = world
122                    .entity(ctx.entity)
123                    .get::<GlobalTransform>()
124                    .map(|gt| gt.scale())
125                    .unwrap_or_default();
126                #[cfg(feature = "2d")]
127                let scale = scale.xy();
128
129                let mut entity_mut = world.entity_mut(ctx.entity);
130
131                // Make sure the collider is initialized with the correct scale.
132                // This overwrites the scale set by the constructor, but that one is
133                // meant to be only changed after initialization.
134                entity_mut
135                    .get_mut::<C>()
136                    .unwrap()
137                    .set_scale(scale.adjust_precision(), 10);
138
139                let collider = entity_mut.get::<C>().unwrap();
140
141                let density = entity_mut
142                    .get::<ColliderDensity>()
143                    .copied()
144                    .unwrap_or_default();
145
146                let mass_properties = if entity_mut.get::<Sensor>().is_some() {
147                    MassProperties::ZERO
148                } else {
149                    collider.mass_properties(density.0)
150                };
151
152                if let Some(mut collider_mass_properties) =
153                    entity_mut.get_mut::<ColliderMassProperties>()
154                {
155                    *collider_mass_properties = ColliderMassProperties::from(mass_properties);
156                }
157            });
158
159        // Register a component hook that removes `ColliderMarker` components
160        // and updates rigid bodies when their collider is removed.
161        app.world_mut()
162            .register_component_hooks::<C>()
163            .on_remove(|mut world, ctx| {
164                // Remove the `ColliderMarker` associated with the collider.
165                // TODO: If the same entity had multiple *different* types of colliders, this would
166                //       get removed even if just one collider was removed. This is a very niche edge case though.
167                world
168                    .commands()
169                    .entity(ctx.entity)
170                    .try_remove::<ColliderMarker>();
171
172                let entity_ref = world.entity_mut(ctx.entity);
173
174                // Get the rigid body entity that the collider is attached to.
175                let Some(ColliderOf { body }) = entity_ref.get::<ColliderOf>().copied() else {
176                    return;
177                };
178
179                // Queue the rigid body for a mass property update.
180                world
181                    .commands()
182                    .entity(body)
183                    .try_insert(RecomputeMassProperties);
184            });
185
186        // Initialize `ColliderAabb` for colliders.
187        app.add_observer(
188            |trigger: On<Add, C>,
189             mut query: Query<(
190                &C,
191                &Position,
192                &Rotation,
193                Option<&CollisionMargin>,
194                &mut ColliderAabb,
195            )>,
196             narrow_phase_config: Res<NarrowPhaseConfig>,
197             length_unit: Res<PhysicsLengthUnit>,
198             collider_context: StaticSystemParam<C::Context>| {
199                let contact_tolerance = length_unit.0 * narrow_phase_config.contact_tolerance;
200                let aabb_context = AabbContext::new(trigger.entity, &*collider_context);
201
202                if let Ok((collider, pos, rot, collision_margin, mut aabb)) =
203                    query.get_mut(trigger.entity)
204                {
205                    let collision_margin = collision_margin.map_or(0.0, |m| m.0);
206                    *aabb = collider
207                        .aabb_with_context(pos.0, *rot, aabb_context)
208                        .grow(Vector::splat(contact_tolerance + collision_margin));
209                }
210            },
211        );
212
213        // When the `Sensor` component is added to a collider, queue its rigid body for a mass property update.
214        app.add_observer(
215            |trigger: On<Add, Sensor>,
216             mut commands: Commands,
217             query: Query<(&ColliderMassProperties, &ColliderOf)>| {
218                if let Ok((collider_mass_properties, &ColliderOf { body })) =
219                    query.get(trigger.entity)
220                {
221                    // If the collider mass properties are zero, there is nothing to subtract.
222                    if *collider_mass_properties == ColliderMassProperties::ZERO {
223                        return;
224                    }
225
226                    // Queue the rigid body for a mass property update.
227                    if let Ok(mut entity_commands) = commands.get_entity(body) {
228                        entity_commands.insert(RecomputeMassProperties);
229                    }
230                }
231            },
232        );
233
234        // When the `Sensor` component is removed from a collider, update its mass properties.
235        app.add_observer(
236            |trigger: On<Remove, Sensor>,
237             mut collider_query: Query<(
238                Ref<C>,
239                &ColliderDensity,
240                &mut ColliderMassProperties,
241            )>| {
242                if let Ok((collider, density, mut collider_mass_properties)) =
243                    collider_query.get_mut(trigger.entity)
244                {
245                    // Update collider mass props.
246                    *collider_mass_properties =
247                        ColliderMassProperties::from(collider.mass_properties(density.0));
248                }
249            },
250        );
251
252        app.add_systems(
253            self.schedule,
254            (
255                update_collider_scale::<C>
256                    .in_set(PhysicsSystems::Prepare)
257                    .after(PhysicsTransformSystems::TransformToPosition),
258                update_collider_mass_properties::<C>
259                    .in_set(MassPropertySystems::UpdateColliderMassProperties),
260            )
261                .chain(),
262        );
263
264        let physics_schedule = app
265            .get_schedule_mut(PhysicsSchedule)
266            .expect("add PhysicsSchedule first");
267
268        // Allowing ambiguities is required so that it's possible
269        // to have multiple collision backends at the same time.
270        physics_schedule.add_systems(
271            update_aabb::<C>
272                .in_set(PhysicsStepSystems::BroadPhase)
273                .after(BroadPhaseSystems::First)
274                .before(BroadPhaseSystems::UpdateStructures)
275                .ambiguous_with_all(),
276        );
277
278        #[cfg(feature = "default-collider")]
279        app.add_systems(
280            Update,
281            (
282                init_collider_constructors,
283                init_collider_constructor_hierarchies,
284            ),
285        );
286    }
287}
288
289/// A marker component for colliders. Inserted and removed automatically.
290///
291/// This is useful for filtering collider entities regardless of the [collider backend](ColliderBackendPlugin).
292#[derive(Reflect, Component, Clone, Copy, Debug, Default)]
293#[reflect(Component, Debug, Default)]
294pub struct ColliderMarker;
295
296/// Generates [`Collider`]s based on [`ColliderConstructor`]s.
297///
298/// If a [`ColliderConstructor`] requires a mesh, the system keeps running
299/// until the mesh associated with the mesh handle is available.
300///
301/// # Panics
302///
303/// Panics if the [`ColliderConstructor`] requires a mesh but no mesh handle is found.
304#[cfg(feature = "default-collider")]
305fn init_collider_constructors(
306    mut commands: Commands,
307    #[cfg(feature = "collider-from-mesh")] meshes: Res<Assets<Mesh>>,
308    #[cfg(feature = "collider-from-mesh")] mesh_handles: Query<&Mesh3d>,
309    #[cfg(feature = "collider-from-mesh")] mut collider_cache: Option<ResMut<ColliderCache>>,
310    constructors: Query<(
311        Entity,
312        Option<&Collider>,
313        Option<&Name>,
314        &ColliderConstructor,
315    )>,
316) {
317    for (entity, existing_collider, name, constructor) in constructors.iter() {
318        let name = pretty_name(name, entity);
319        if existing_collider.is_some() {
320            warn!(
321                "Tried to add a collider to entity {name} via {constructor:#?}, \
322                but that entity already holds a collider. Skipping.",
323            );
324            commands.entity(entity).remove::<ColliderConstructor>();
325            continue;
326        }
327        #[cfg(feature = "collider-from-mesh")]
328        let collider = if constructor.requires_mesh() {
329            let mesh_handle = mesh_handles.get(entity).unwrap_or_else(|_| panic!(
330                "Tried to add a collider to entity {name} via {constructor:#?} that requires a mesh, \
331                but no mesh handle was found"));
332            let Some(mesh) = meshes.get(mesh_handle) else {
333                // Mesh required, but not loaded yet
334                continue;
335            };
336            collider_cache
337                .as_mut()
338                .map(|cache| cache.get_or_insert(mesh_handle, mesh, constructor.clone()))
339                .unwrap_or_else(|| Collider::try_from_constructor(constructor.clone(), Some(mesh)))
340        } else {
341            Collider::try_from_constructor(constructor.clone(), None)
342        };
343
344        #[cfg(not(feature = "collider-from-mesh"))]
345        let collider = Collider::try_from_constructor(constructor.clone());
346
347        if let Some(collider) = collider {
348            commands.entity(entity).insert(collider);
349            commands.trigger(ColliderConstructorReady { entity })
350        } else {
351            error!(
352                "Tried to add a collider to entity {name} via {constructor:#?}, \
353                but the collider could not be generated. Skipping.",
354            );
355        }
356        commands.entity(entity).remove::<ColliderConstructor>();
357    }
358}
359
360/// Generates [`Collider`]s for descendants of entities with the [`ColliderConstructorHierarchy`] component.
361///
362/// If an entity has a `SceneInstance`, its collider hierarchy is only generated once the scene is ready.
363#[cfg(feature = "default-collider")]
364fn init_collider_constructor_hierarchies(
365    mut commands: Commands,
366    #[cfg(feature = "collider-from-mesh")] meshes: Res<Assets<Mesh>>,
367    #[cfg(feature = "collider-from-mesh")] mesh_handles: Query<&Mesh3d>,
368    #[cfg(feature = "collider-from-mesh")] mut collider_cache: Option<ResMut<ColliderCache>>,
369    #[cfg(feature = "bevy_scene")] scene_spawner: Res<SceneSpawner>,
370    #[cfg(feature = "bevy_scene")] scenes: Query<&SceneRoot>,
371    #[cfg(feature = "bevy_scene")] scene_instances: Query<&SceneInstance>,
372    collider_constructors: Query<(Entity, &ColliderConstructorHierarchy)>,
373    children: Query<&Children>,
374    child_query: Query<(Option<&Name>, Option<&Collider>)>,
375) {
376    use super::ColliderConstructorHierarchyConfig;
377
378    for (scene_entity, collider_constructor_hierarchy) in collider_constructors.iter() {
379        #[cfg(feature = "bevy_scene")]
380        {
381            if scenes.contains(scene_entity) {
382                if let Ok(scene_instance) = scene_instances.get(scene_entity) {
383                    if !scene_spawner.instance_is_ready(**scene_instance) {
384                        // Wait for the scene to be ready
385                        continue;
386                    }
387                } else {
388                    // SceneInstance is added in the SpawnScene schedule, so it might not be available yet
389                    continue;
390                }
391            }
392        }
393
394        for child_entity in children.iter_descendants(scene_entity) {
395            let Ok((name, existing_collider)) = child_query.get(child_entity) else {
396                continue;
397            };
398
399            let pretty_name = pretty_name(name, child_entity);
400
401            let default_collider = || {
402                Some(ColliderConstructorHierarchyConfig {
403                    constructor: collider_constructor_hierarchy.default_constructor.clone(),
404                    ..default()
405                })
406            };
407
408            let collider_data = if let Some(name) = name {
409                collider_constructor_hierarchy
410                    .config
411                    .get(name.as_str())
412                    .cloned()
413                    .unwrap_or_else(default_collider)
414            } else if existing_collider.is_some() {
415                warn!(
416                    "Tried to add a collider to entity {pretty_name} via {collider_constructor_hierarchy:#?}, \
417                        but that entity already holds a collider. Skipping. \
418                        If this was intentional, add the name of the collider to overwrite to `ColliderConstructorHierarchy.config`."
419                );
420                continue;
421            } else {
422                default_collider()
423            };
424
425            // If the configuration is explicitly set to `None`, skip this entity.
426            let Some(collider_data) = collider_data else {
427                continue;
428            };
429
430            // Use the configured constructor if specified, otherwise use the default constructor.
431            // If both are `None`, skip this entity.
432            let Some(constructor) = collider_data
433                .constructor
434                .or_else(|| collider_constructor_hierarchy.default_constructor.clone())
435            else {
436                continue;
437            };
438
439            #[cfg(feature = "collider-from-mesh")]
440            let collider = if constructor.requires_mesh() {
441                let Ok(mesh_handle) = mesh_handles.get(child_entity) else {
442                    // This child entity does not have a mesh, so we skip it.
443                    continue;
444                };
445                let Some(mesh) = meshes.get(mesh_handle) else {
446                    // Mesh required, but not loaded yet
447                    continue;
448                };
449                collider_cache
450                    .as_mut()
451                    .map(|cache| cache.get_or_insert(mesh_handle, mesh, constructor.clone()))
452                    .unwrap_or_else(|| {
453                        Collider::try_from_constructor(constructor.clone(), Some(mesh))
454                    })
455            } else {
456                Collider::try_from_constructor(constructor.clone(), None)
457            };
458
459            #[cfg(not(feature = "collider-from-mesh"))]
460            let collider = Collider::try_from_constructor(constructor);
461
462            if let Some(collider) = collider {
463                commands.entity(child_entity).insert((
464                    collider,
465                    collider_data
466                        .layers
467                        .unwrap_or(collider_constructor_hierarchy.default_layers),
468                    collider_data
469                        .density
470                        .unwrap_or(collider_constructor_hierarchy.default_density),
471                ));
472            } else {
473                error!(
474                    "Tried to add a collider to entity {pretty_name} via {collider_constructor_hierarchy:#?}, \
475                        but the collider could not be generated. Skipping.",
476                );
477            }
478        }
479
480        commands
481            .entity(scene_entity)
482            .remove::<ColliderConstructorHierarchy>();
483
484        commands.trigger(ColliderConstructorHierarchyReady {
485            entity: scene_entity,
486        })
487    }
488}
489
490#[cfg(feature = "default-collider")]
491fn pretty_name(name: Option<&Name>, entity: Entity) -> String {
492    name.map(|n| n.to_string())
493        .unwrap_or_else(|| format!("<unnamed entity {}>", entity.index()))
494}
495
496/// Updates the Axis-Aligned Bounding Boxes of all colliders.
497#[allow(clippy::type_complexity)]
498fn update_aabb<C: AnyCollider>(
499    mut colliders: Query<
500        (
501            Entity,
502            &C,
503            &mut ColliderAabb,
504            &Position,
505            &Rotation,
506            Option<&ColliderOf>,
507            Option<&CollisionMargin>,
508            Option<&SpeculativeMargin>,
509            Has<SweptCcd>,
510            Option<&LinearVelocity>,
511            Option<&AngularVelocity>,
512        ),
513        Or<(
514            Changed<Position>,
515            Changed<Rotation>,
516            Changed<LinearVelocity>,
517            Changed<AngularVelocity>,
518            Changed<C>,
519        )>,
520    >,
521    rb_velocities: Query<
522        (
523            &Position,
524            &Rotation,
525            &ComputedCenterOfMass,
526            &LinearVelocity,
527            &AngularVelocity,
528        ),
529        With<Children>,
530    >,
531    narrow_phase_config: Res<NarrowPhaseConfig>,
532    length_unit: Res<PhysicsLengthUnit>,
533    time: Res<Time>,
534    collider_context: StaticSystemParam<C::Context>,
535) {
536    let delta_secs = time.delta_seconds_adjusted();
537    let default_speculative_margin = length_unit.0 * narrow_phase_config.default_speculative_margin;
538    let contact_tolerance = length_unit.0 * narrow_phase_config.contact_tolerance;
539
540    for (
541        entity,
542        collider,
543        mut aabb,
544        pos,
545        rot,
546        collider_of,
547        collision_margin,
548        speculative_margin,
549        has_swept_ccd,
550        lin_vel,
551        ang_vel,
552    ) in &mut colliders
553    {
554        let collision_margin = collision_margin.map_or(0.0, |margin| margin.0);
555        let speculative_margin = if has_swept_ccd {
556            Scalar::MAX
557        } else {
558            speculative_margin.map_or(default_speculative_margin, |margin| margin.0)
559        };
560
561        let context = AabbContext::new(entity, &*collider_context);
562
563        if speculative_margin <= 0.0 {
564            *aabb = collider
565                .aabb_with_context(pos.0, *rot, context)
566                .grow(Vector::splat(contact_tolerance + collision_margin));
567            continue;
568        }
569
570        // Expand the AABB based on the body's velocity and CCD speculative margin.
571        let (lin_vel, ang_vel) = if let (Some(lin_vel), Some(ang_vel)) = (lin_vel, ang_vel) {
572            (*lin_vel, *ang_vel)
573        } else if let Some(Ok((rb_pos, rb_rot, center_of_mass, lin_vel, ang_vel))) =
574            collider_of.map(|&ColliderOf { body }| rb_velocities.get(body))
575        {
576            // If the rigid body is rotating, off-center colliders will orbit around it,
577            // which affects their linear velocities. We need to compute the linear velocity
578            // at the offset position.
579            // TODO: This assumes that the colliders would continue moving in the same direction,
580            //       but because they are orbiting, the direction will change. We should take
581            //       into account the uniform circular motion.
582            let offset = pos.0 - rb_pos.0 - rb_rot * center_of_mass.0;
583            #[cfg(feature = "2d")]
584            let vel_at_offset =
585                lin_vel.0 + Vector::new(-ang_vel.0 * offset.y, ang_vel.0 * offset.x) * 1.0;
586            #[cfg(feature = "3d")]
587            let vel_at_offset = lin_vel.0 + ang_vel.cross(offset);
588            (LinearVelocity(vel_at_offset), *ang_vel)
589        } else {
590            (LinearVelocity::ZERO, AngularVelocity::ZERO)
591        };
592
593        // Current position and predicted position for next feame
594        let (start_pos, start_rot) = (*pos, *rot);
595        let (end_pos, end_rot) = {
596            #[cfg(feature = "2d")]
597            {
598                (
599                    pos.0
600                        + (lin_vel.0 * delta_secs)
601                            .clamp_length_max(speculative_margin.max(contact_tolerance)),
602                    *rot * Rotation::radians(ang_vel.0 * delta_secs),
603                )
604            }
605            #[cfg(feature = "3d")]
606            {
607                let end_rot =
608                    Rotation(Quaternion::from_scaled_axis(ang_vel.0 * delta_secs) * rot.0)
609                        .fast_renormalize();
610                (
611                    pos.0
612                        + (lin_vel.0 * delta_secs)
613                            .clamp_length_max(speculative_margin.max(contact_tolerance)),
614                    end_rot,
615                )
616            }
617        };
618        // Compute swept AABB, the space that the body would occupy if it was integrated for one frame
619        // TODO: Should we expand the AABB in all directions for speculative contacts?
620        *aabb = collider
621            .swept_aabb_with_context(start_pos.0, start_rot, end_pos, end_rot, context)
622            .grow(Vector::splat(contact_tolerance + collision_margin));
623    }
624}
625
626/// Updates the scale of colliders based on [`Transform`] scale.
627#[allow(clippy::type_complexity)]
628pub fn update_collider_scale<C: ScalableCollider>(
629    mut colliders: ParamSet<(
630        // Root bodies
631        Query<(&Transform, &mut C), (Without<ChildOf>, Or<(Changed<Transform>, Changed<C>)>)>,
632        // Child colliders
633        Query<
634            (&ColliderTransform, &mut C),
635            (With<ChildOf>, Or<(Changed<ColliderTransform>, Changed<C>)>),
636        >,
637    )>,
638    config: Res<PhysicsTransformConfig>,
639) {
640    if config.transform_to_collider_scale {
641        // Update collider scale for root bodies
642        for (transform, mut collider) in &mut colliders.p0() {
643            #[cfg(feature = "2d")]
644            let scale = transform.scale.truncate().adjust_precision();
645            #[cfg(feature = "3d")]
646            let scale = transform.scale.adjust_precision();
647            if scale != collider.scale() {
648                // TODO: Support configurable subdivision count for shapes that
649                //       can't be represented without approximations after scaling.
650                collider.set_scale(scale, 10);
651            }
652        }
653    }
654    // Update collider scale for child colliders
655    for (collider_transform, mut collider) in &mut colliders.p1() {
656        if collider_transform.scale != collider.scale() {
657            // TODO: Support configurable subdivision count for shapes that
658            //       can't be represented without approximations after scaling.
659            collider.set_scale(collider_transform.scale, 10);
660        }
661    }
662}
663
664/// Updates the mass properties of [`Collider`].
665#[allow(clippy::type_complexity)]
666pub(crate) fn update_collider_mass_properties<C: AnyCollider>(
667    mut query: Query<
668        (Ref<C>, &ColliderDensity, &mut ColliderMassProperties),
669        (Or<(Changed<C>, Changed<ColliderDensity>)>, Without<Sensor>),
670    >,
671) {
672    for (collider, density, mut collider_mass_properties) in &mut query {
673        // Update the collider's mass properties.
674        *collider_mass_properties =
675            ColliderMassProperties::from(collider.mass_properties(density.0));
676    }
677}
678
679#[cfg(test)]
680mod tests {
681    #![allow(clippy::unnecessary_cast)]
682
683    #[cfg(feature = "default-collider")]
684    use super::*;
685
686    #[test]
687    #[cfg(feature = "default-collider")]
688    fn sensor_mass_properties() {
689        let mut app = App::new();
690
691        app.init_schedule(PhysicsSchedule)
692            .init_schedule(SubstepSchedule);
693
694        app.add_plugins((
695            MassPropertyPlugin::new(FixedPostUpdate),
696            ColliderHierarchyPlugin,
697            ColliderTransformPlugin::default(),
698            ColliderBackendPlugin::<Collider>::new(FixedPostUpdate),
699        ));
700
701        let collider = Collider::capsule(0.5, 2.0);
702        let mass_properties = MassPropertiesBundle::from_shape(&collider, 1.0);
703
704        let parent = app
705            .world_mut()
706            .spawn((
707                RigidBody::Dynamic,
708                mass_properties.clone(),
709                Transform::default(),
710            ))
711            .id();
712
713        let child = app
714            .world_mut()
715            .spawn((
716                collider,
717                Transform::from_xyz(1.0, 0.0, 0.0),
718                ChildOf(parent),
719            ))
720            .id();
721
722        app.world_mut().run_schedule(FixedPostUpdate);
723
724        assert_eq!(
725            app.world()
726                .entity(parent)
727                .get::<ComputedMass>()
728                .expect("rigid body should have mass")
729                .value() as f32,
730            2.0 * mass_properties.mass.0,
731        );
732        assert!(
733            app.world()
734                .entity(parent)
735                .get::<ComputedCenterOfMass>()
736                .expect("rigid body should have a center of mass")
737                .x
738                > 0.0,
739        );
740
741        // Mark the collider as a sensor. It should no longer contribute to the mass properties of the rigid body.
742        let mut entity_mut = app.world_mut().entity_mut(child);
743        entity_mut.insert(Sensor);
744
745        app.world_mut().run_schedule(FixedPostUpdate);
746
747        assert_eq!(
748            app.world()
749                .entity(parent)
750                .get::<ComputedMass>()
751                .expect("rigid body should have mass")
752                .value() as f32,
753            mass_properties.mass.0,
754        );
755        assert!(
756            app.world()
757                .entity(parent)
758                .get::<ComputedCenterOfMass>()
759                .expect("rigid body should have a center of mass")
760                .x
761                == 0.0,
762        );
763
764        // Remove the sensor component. The collider should contribute to the mass properties of the rigid body again.
765        let mut entity_mut = app.world_mut().entity_mut(child);
766        entity_mut.remove::<Sensor>();
767
768        app.world_mut().run_schedule(FixedPostUpdate);
769
770        assert_eq!(
771            app.world()
772                .entity(parent)
773                .get::<ComputedMass>()
774                .expect("rigid body should have mass")
775                .value() as f32,
776            2.0 * mass_properties.mass.0,
777        );
778        assert!(
779            app.world()
780                .entity(parent)
781                .get::<ComputedCenterOfMass>()
782                .expect("rigid body should have a center of mass")
783                .x
784                > 0.0,
785        );
786    }
787}