1use 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#[cfg_attr(feature = "2d", doc = "use avian2d::prelude::*;")]
41#[cfg_attr(feature = "3d", doc = "use avian3d::prelude::*;")]
42pub struct ColliderBackendPlugin<C: ScalableCollider> {
67 schedule: Interned<dyn ScheduleLabel>,
68 _phantom: PhantomData<C>,
69}
70
71impl<C: ScalableCollider> ColliderBackendPlugin<C> {
72 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 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 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 hooks
112 .on_add(|mut world, ctx| {
113 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 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 app.world_mut()
162 .register_component_hooks::<C>()
163 .on_remove(|mut world, ctx| {
164 world
168 .commands()
169 .entity(ctx.entity)
170 .try_remove::<ColliderMarker>();
171
172 let entity_ref = world.entity_mut(ctx.entity);
173
174 let Some(ColliderOf { body }) = entity_ref.get::<ColliderOf>().copied() else {
176 return;
177 };
178
179 world
181 .commands()
182 .entity(body)
183 .try_insert(RecomputeMassProperties);
184 });
185
186 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 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 *collider_mass_properties == ColliderMassProperties::ZERO {
223 return;
224 }
225
226 if let Ok(mut entity_commands) = commands.get_entity(body) {
228 entity_commands.insert(RecomputeMassProperties);
229 }
230 }
231 },
232 );
233
234 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 *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 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#[derive(Reflect, Component, Clone, Copy, Debug, Default)]
293#[reflect(Component, Debug, Default)]
294pub struct ColliderMarker;
295
296#[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 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#[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 continue;
386 }
387 } else {
388 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 let Some(collider_data) = collider_data else {
427 continue;
428 };
429
430 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 continue;
444 };
445 let Some(mesh) = meshes.get(mesh_handle) else {
446 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#[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 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 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 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 *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#[allow(clippy::type_complexity)]
628pub fn update_collider_scale<C: ScalableCollider>(
629 mut colliders: ParamSet<(
630 Query<(&Transform, &mut C), (Without<ChildOf>, Or<(Changed<Transform>, Changed<C>)>)>,
632 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 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 collider.set_scale(scale, 10);
651 }
652 }
653 }
654 for (collider_transform, mut collider) in &mut colliders.p1() {
656 if collider_transform.scale != collider.scale() {
657 collider.set_scale(collider_transform.scale, 10);
660 }
661 }
662}
663
664#[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 *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 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 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}