1use crate::{
2 dynamics::{
3 joints::EntityConstraint,
4 solver::{
5 SolverDiagnostics,
6 constraint_graph::{
7 COLOR_OVERFLOW_INDEX, ConstraintGraph, ContactManifoldHandle, GraphColor,
8 },
9 contact::ContactConstraint,
10 schedule::SubstepSolverSystems,
11 softness_parameters::{SoftnessCoefficients, SoftnessParameters},
12 solver_body::{SolverBody, SolverBodyInertia},
13 },
14 },
15 prelude::*,
16};
17use bevy::{
18 ecs::{query::QueryData, system::lifetimeless::Read},
19 prelude::*,
20};
21use core::cmp::Ordering;
22
23pub struct SolverPlugin {
69 length_unit: Scalar,
70}
71
72impl Default for SolverPlugin {
73 fn default() -> Self {
74 Self::new_with_length_unit(1.0)
75 }
76}
77
78impl SolverPlugin {
79 pub fn new_with_length_unit(unit: Scalar) -> Self {
84 Self { length_unit: unit }
85 }
86}
87
88impl Plugin for SolverPlugin {
89 fn build(&self, app: &mut App) {
90 app.init_resource::<SolverConfig>()
91 .init_resource::<ContactSoftnessCoefficients>()
92 .init_resource::<ContactConstraints>()
93 .init_resource::<ConstraintGraph>();
94
95 if app
96 .world()
97 .get_resource::<PhysicsLengthUnit>()
98 .is_none_or(|unit| unit.0 == 1.0)
99 {
100 app.insert_resource(PhysicsLengthUnit(self.length_unit));
101 }
102
103 let physics = app
105 .get_schedule_mut(PhysicsSchedule)
106 .expect("add PhysicsSchedule first");
107
108 physics.add_systems(update_contact_softness.before(PhysicsStepSystems::NarrowPhase));
109
110 physics.add_systems(
112 prepare_contact_constraints.in_set(SolverSystems::PrepareContactConstraints),
113 );
114
115 physics.add_systems(solve_restitution.in_set(SolverSystems::Restitution));
117
118 physics.add_systems(store_contact_impulses.in_set(SolverSystems::StoreContactImpulses));
120
121 let substeps = app
123 .get_schedule_mut(SubstepSchedule)
124 .expect("add SubstepSchedule first");
125
126 substeps.add_systems(warm_start.in_set(SubstepSolverSystems::WarmStart));
130
131 substeps.add_systems(solve_contacts::<true>.in_set(SubstepSolverSystems::SolveConstraints));
133
134 substeps.add_systems(solve_contacts::<false>.in_set(SubstepSolverSystems::Relax));
137
138 substeps.add_systems(
140 (
141 joint_damping::<FixedJoint>,
142 joint_damping::<RevoluteJoint>,
143 #[cfg(feature = "3d")]
144 joint_damping::<SphericalJoint>,
145 joint_damping::<PrismaticJoint>,
146 joint_damping::<DistanceJoint>,
147 )
148 .chain()
149 .in_set(SubstepSolverSystems::Damping),
150 );
151 }
152
153 fn finish(&self, app: &mut App) {
154 app.register_physics_diagnostics::<SolverDiagnostics>();
156 }
157}
158
159#[derive(Resource, Clone, Debug, Deref, DerefMut, PartialEq, Reflect)]
200#[reflect(Resource)]
201pub struct PhysicsLengthUnit(pub Scalar);
202
203impl Default for PhysicsLengthUnit {
204 fn default() -> Self {
205 Self(1.0)
206 }
207}
208
209#[derive(Resource, Clone, Debug, PartialEq, Reflect)]
215#[reflect(Resource)]
216pub struct SolverConfig {
217 pub contact_damping_ratio: Scalar,
228
229 pub contact_frequency_factor: Scalar,
241
242 pub max_overlap_solve_speed: Scalar,
251
252 pub warm_start_coefficient: Scalar,
263
264 pub restitution_threshold: Scalar,
276
277 pub restitution_iterations: usize,
289}
290
291impl Default for SolverConfig {
292 fn default() -> Self {
293 Self {
294 contact_damping_ratio: 10.0,
295 contact_frequency_factor: 1.5,
296 max_overlap_solve_speed: 4.0,
297 warm_start_coefficient: 1.0,
298 restitution_threshold: 1.0,
299 restitution_iterations: 1,
300 }
301 }
302}
303
304#[derive(Resource, Clone, Copy, PartialEq, Reflect)]
309#[reflect(Resource)]
310pub struct ContactSoftnessCoefficients {
311 pub dynamic: SoftnessCoefficients,
313 pub non_dynamic: SoftnessCoefficients,
315}
316
317impl Default for ContactSoftnessCoefficients {
318 fn default() -> Self {
319 Self {
320 dynamic: SoftnessParameters::new(10.0, 30.0).compute_coefficients(1.0 / 60.0),
321 non_dynamic: SoftnessParameters::new(10.0, 60.0).compute_coefficients(1.0 / 60.0),
322 }
323 }
324}
325
326fn update_contact_softness(
327 mut coefficients: ResMut<ContactSoftnessCoefficients>,
328 solver_config: Res<SolverConfig>,
329 physics_time: Res<Time<Physics>>,
330 substep_time: Res<Time<Substeps>>,
331) {
332 if solver_config.is_changed() || physics_time.is_changed() || substep_time.is_changed() {
333 let dt = physics_time.delta_secs_f64() as Scalar;
334 let h = substep_time.delta_secs_f64() as Scalar;
335
336 let max_hz = 1.0 / (dt * 2.0);
339 let hz = solver_config.contact_frequency_factor * max_hz.min(0.25 / h);
340
341 coefficients.dynamic = SoftnessParameters::new(solver_config.contact_damping_ratio, hz)
342 .compute_coefficients(h);
343
344 coefficients.non_dynamic =
347 SoftnessParameters::new(solver_config.contact_damping_ratio, 2.0 * hz)
348 .compute_coefficients(h);
349 }
350}
351
352#[derive(Resource, Default, Deref, DerefMut)]
354pub struct ContactConstraints(pub Vec<ContactConstraint>);
355
356#[derive(QueryData)]
357pub(super) struct BodyQuery {
358 pub(super) rb: Read<RigidBody>,
359 pub(super) linear_velocity: Read<LinearVelocity>,
360 pub(super) inertia: Option<Read<SolverBodyInertia>>,
361}
362
363fn prepare_contact_constraints(
364 contact_graph: Res<ContactGraph>,
365 mut constraint_graph: ResMut<ConstraintGraph>,
366 mut diagnostics: ResMut<SolverDiagnostics>,
367 bodies: Query<BodyQuery, RigidBodyActiveFilter>,
368 narrow_phase_config: Res<NarrowPhaseConfig>,
369 contact_softness: Res<ContactSoftnessCoefficients>,
370) {
371 let start = crate::utils::Instant::now();
372
373 for color in constraint_graph.colors.iter_mut() {
374 color.contact_constraints.clear();
378 }
379
380 let mut active_colors = constraint_graph
382 .colors
383 .iter_mut()
384 .filter(|color| !color.manifold_handles.is_empty())
385 .collect::<Vec<&mut GraphColor>>();
386
387 crate::utils::par_for_each(&mut active_colors, 2, |_i, color| {
389 for handle in color.manifold_handles.iter() {
390 let contact_pair = contact_graph
392 .get_by_id(handle.contact_id)
393 .unwrap_or_else(|| {
394 panic!("Contact pair not found in graph: {:?}", handle.contact_id)
395 })
396 .1;
397 let manifold_index = handle.manifold_index;
398 let manifold = &contact_pair.manifolds[manifold_index];
399
400 if !contact_pair.generates_constraints() {
401 continue;
402 }
403
404 let (Some(body1_entity), Some(body2_entity)) = (contact_pair.body1, contact_pair.body2)
405 else {
406 continue;
407 };
408
409 let Ok(body1) = bodies.get(body1_entity) else {
411 continue;
412 };
413 let Ok(body2) = bodies.get(body2_entity) else {
414 continue;
415 };
416
417 if !body1.rb.is_dynamic() && !body2.rb.is_dynamic() {
420 continue;
422 }
423
424 let constraint = ContactConstraint::generate(
425 body1_entity,
426 body2_entity,
427 body1,
428 body2,
429 contact_pair.contact_id,
430 manifold,
431 manifold_index,
432 narrow_phase_config.match_contacts,
433 &contact_softness,
434 );
435
436 if !constraint.points.is_empty() {
437 color.contact_constraints.push(constraint);
438 }
439 }
440 });
441
442 diagnostics.prepare_constraints += start.elapsed();
443 diagnostics.contact_constraint_count = constraint_graph
444 .colors
445 .iter()
446 .map(|color| color.contact_constraints.len())
447 .sum::<usize>() as u32;
448}
449
450fn warm_start(
454 bodies: Query<(&mut SolverBody, &SolverBodyInertia)>,
455 mut constraint_graph: ResMut<ConstraintGraph>,
456 solver_config: Res<SolverConfig>,
457 mut diagnostics: ResMut<SolverDiagnostics>,
458) {
459 let start = crate::utils::Instant::now();
460
461 for constraint in constraint_graph.colors[COLOR_OVERFLOW_INDEX]
463 .contact_constraints
464 .iter_mut()
465 {
466 warm_start_internal(&bodies, constraint, solver_config.warm_start_coefficient);
467 }
468
469 for color in constraint_graph
471 .colors
472 .iter_mut()
473 .take(COLOR_OVERFLOW_INDEX)
474 .filter(|color| !color.contact_constraints.is_empty())
475 {
476 crate::utils::par_for_each(&mut color.contact_constraints, 64, |_i, constraint| {
477 warm_start_internal(&bodies, constraint, solver_config.warm_start_coefficient);
478 });
479 }
480
481 diagnostics.warm_start += start.elapsed();
482}
483
484fn warm_start_internal(
485 bodies: &Query<(&mut SolverBody, &SolverBodyInertia)>,
486 constraint: &mut ContactConstraint,
487 warm_start_coefficient: Scalar,
488) {
489 debug_assert!(!constraint.points.is_empty());
490
491 let mut dummy_body1 = SolverBody::DUMMY;
492 let mut dummy_body2 = SolverBody::DUMMY;
493
494 let (mut body1, mut inertia1) = (&mut dummy_body1, &SolverBodyInertia::DUMMY);
495 let (mut body2, mut inertia2) = (&mut dummy_body2, &SolverBodyInertia::DUMMY);
496
497 if let Ok((body, inertia)) = unsafe { bodies.get_unchecked(constraint.body1) } {
499 body1 = body.into_inner();
500 inertia1 = inertia;
501 }
502 if let Ok((body, inertia)) = unsafe { bodies.get_unchecked(constraint.body2) } {
503 body2 = body.into_inner();
504 inertia2 = inertia;
505 }
506
507 match constraint.relative_dominance.cmp(&0) {
509 Ordering::Greater => inertia1 = &SolverBodyInertia::DUMMY,
510 Ordering::Less => inertia2 = &SolverBodyInertia::DUMMY,
511 _ => {}
512 }
513
514 constraint.warm_start(body1, body2, inertia1, inertia2, warm_start_coefficient);
515}
516
517#[allow(clippy::too_many_arguments)]
530#[allow(clippy::type_complexity)]
531fn solve_contacts<const USE_BIAS: bool>(
532 bodies: Query<(&mut SolverBody, &SolverBodyInertia)>,
533 mut constraint_graph: ResMut<ConstraintGraph>,
534 solver_config: Res<SolverConfig>,
535 length_unit: Res<PhysicsLengthUnit>,
536 time: Res<Time>,
537 mut diagnostics: ResMut<SolverDiagnostics>,
538) {
539 let start = crate::utils::Instant::now();
540
541 let delta_secs = time.delta_seconds_adjusted();
542 let max_overlap_solve_speed = solver_config.max_overlap_solve_speed * length_unit.0;
543
544 for constraint in constraint_graph.colors[COLOR_OVERFLOW_INDEX]
546 .contact_constraints
547 .iter_mut()
548 {
549 solve_contacts_internal::<USE_BIAS>(
550 &bodies,
551 constraint,
552 max_overlap_solve_speed,
553 delta_secs,
554 );
555 }
556
557 for color in constraint_graph
559 .colors
560 .iter_mut()
561 .take(COLOR_OVERFLOW_INDEX)
562 .filter(|color| !color.contact_constraints.is_empty())
563 {
564 crate::utils::par_for_each(&mut color.contact_constraints, 64, |_i, constraint| {
565 solve_contacts_internal::<USE_BIAS>(
566 &bodies,
567 constraint,
568 max_overlap_solve_speed,
569 delta_secs,
570 );
571 });
572 }
573
574 if USE_BIAS {
575 diagnostics.solve_constraints += start.elapsed();
576 } else {
577 diagnostics.relax_velocities += start.elapsed();
578 }
579}
580
581fn solve_contacts_internal<const USE_BIAS: bool>(
582 bodies: &Query<(&mut SolverBody, &SolverBodyInertia)>,
583 constraint: &mut ContactConstraint,
584 max_overlap_solve_speed: Scalar,
585 delta_secs: Scalar,
586) {
587 let mut dummy_body1 = SolverBody::DUMMY;
588 let mut dummy_body2 = SolverBody::DUMMY;
589
590 let (mut body1, mut inertia1) = (&mut dummy_body1, &SolverBodyInertia::DUMMY);
591 let (mut body2, mut inertia2) = (&mut dummy_body2, &SolverBodyInertia::DUMMY);
592
593 if let Ok((body, inertia)) = unsafe { bodies.get_unchecked(constraint.body1) } {
595 body1 = body.into_inner();
596 inertia1 = inertia;
597 }
598 if let Ok((body, inertia)) = unsafe { bodies.get_unchecked(constraint.body2) } {
599 body2 = body.into_inner();
600 inertia2 = inertia;
601 }
602
603 match constraint.relative_dominance.cmp(&0) {
605 Ordering::Greater => inertia1 = &SolverBodyInertia::DUMMY,
606 Ordering::Less => inertia2 = &SolverBodyInertia::DUMMY,
607 _ => {}
608 }
609
610 constraint.solve(
611 body1,
612 body2,
613 inertia1,
614 inertia2,
615 delta_secs,
616 USE_BIAS,
617 max_overlap_solve_speed,
618 );
619}
620
621#[allow(clippy::too_many_arguments)]
629#[allow(clippy::type_complexity)]
630fn solve_restitution(
631 bodies: Query<(&mut SolverBody, &SolverBodyInertia)>,
632 mut constraint_graph: ResMut<ConstraintGraph>,
633 solver_config: Res<SolverConfig>,
634 length_unit: Res<PhysicsLengthUnit>,
635 mut diagnostics: ResMut<SolverDiagnostics>,
636) {
637 let start = crate::utils::Instant::now();
638
639 let threshold = solver_config.restitution_threshold * length_unit.0;
641
642 for constraint in constraint_graph.colors[COLOR_OVERFLOW_INDEX]
644 .contact_constraints
645 .iter_mut()
646 {
647 solve_restitution_internal(
648 &bodies,
649 constraint,
650 threshold,
651 solver_config.restitution_iterations,
652 );
653 }
654
655 for color in constraint_graph
657 .colors
658 .iter_mut()
659 .take(COLOR_OVERFLOW_INDEX)
660 .filter(|color| !color.contact_constraints.is_empty())
661 {
662 crate::utils::par_for_each(&mut color.contact_constraints, 64, |_i, constraint| {
663 solve_restitution_internal(
664 &bodies,
665 constraint,
666 threshold,
667 solver_config.restitution_iterations,
668 );
669 });
670 }
671
672 diagnostics.apply_restitution += start.elapsed();
673}
674
675fn solve_restitution_internal(
676 bodies: &Query<(&mut SolverBody, &SolverBodyInertia)>,
677 constraint: &mut ContactConstraint,
678 threshold: Scalar,
679 iterations: usize,
680) {
681 let restitution = constraint.restitution;
682
683 if restitution == 0.0 {
684 return;
685 }
686
687 let mut dummy_body1 = SolverBody::DUMMY;
688 let mut dummy_body2 = SolverBody::DUMMY;
689
690 let (mut body1, mut inertia1) = (&mut dummy_body1, &SolverBodyInertia::DUMMY);
691 let (mut body2, mut inertia2) = (&mut dummy_body2, &SolverBodyInertia::DUMMY);
692
693 if let Ok((body, inertia)) = unsafe { bodies.get_unchecked(constraint.body1) } {
695 body1 = body.into_inner();
696 inertia1 = inertia;
697 }
698 if let Ok((body, inertia)) = unsafe { bodies.get_unchecked(constraint.body2) } {
699 body2 = body.into_inner();
700 inertia2 = inertia;
701 }
702
703 match constraint.relative_dominance.cmp(&0) {
705 Ordering::Greater => inertia1 = &SolverBodyInertia::DUMMY,
706 Ordering::Less => inertia2 = &SolverBodyInertia::DUMMY,
707 _ => {}
708 }
709
710 let point_count = constraint.points.len();
713 let iterations = if point_count > 1 { iterations } else { 1 };
714
715 for _ in 0..iterations {
716 constraint.apply_restitution(body1, body2, inertia1, inertia2, threshold);
717 }
718}
719
720fn store_contact_impulses(
723 mut contact_graph: ResMut<ContactGraph>,
724 mut constraint_graph: ResMut<ConstraintGraph>,
725 mut diagnostics: ResMut<SolverDiagnostics>,
726) {
727 let start = crate::utils::Instant::now();
728
729 for color in constraint_graph.colors.iter_mut() {
730 for constraint in &mut color.contact_constraints {
731 let Some(manifold) = contact_graph.get_manifold_mut(ContactManifoldHandle {
732 contact_id: constraint.contact_id,
733 manifold_index: constraint.manifold_index,
734 }) else {
735 unreachable!(
736 "Contact manifold {:?} for contact ID {:?} not found in contact graph.",
737 constraint.contact_id, constraint.manifold_index
738 );
739 };
740
741 for (contact, constraint_point) in
742 manifold.points.iter_mut().zip(constraint.points.iter())
743 {
744 contact.warm_start_normal_impulse = constraint_point.normal_part.impulse;
745 contact.warm_start_tangent_impulse = constraint_point
746 .tangent_part
747 .as_ref()
748 .map_or(default(), |part| part.impulse);
749 contact.normal_impulse = constraint_point.normal_part.total_impulse;
750 }
751 }
752 }
753
754 diagnostics.store_impulses += start.elapsed();
755}
756
757#[allow(clippy::type_complexity)]
759pub fn joint_damping<T: Component + EntityConstraint<2>>(
760 bodies: Query<(&mut SolverBody, &SolverBodyInertia)>,
761 joints: Query<(&T, &JointDamping)>,
762 time: Res<Time>,
763) {
764 let delta_secs = time.delta_seconds_adjusted();
765
766 let mut dummy_body1 = SolverBody::DUMMY;
767 let mut dummy_body2 = SolverBody::DUMMY;
768
769 for (joint, damping) in &joints {
770 let entities = joint.entities();
771
772 let (mut body1, mut inertia1) = (&mut dummy_body1, &SolverBodyInertia::DUMMY);
773 let (mut body2, mut inertia2) = (&mut dummy_body2, &SolverBodyInertia::DUMMY);
774
775 if let Ok((body, inertia)) = unsafe { bodies.get_unchecked(entities[0]) } {
777 body1 = body.into_inner();
778 inertia1 = inertia;
779 }
780 if let Ok((body, inertia)) = unsafe { bodies.get_unchecked(entities[1]) } {
781 body2 = body.into_inner();
782 inertia2 = inertia;
783 }
784
785 let delta_omega = (body2.angular_velocity - body1.angular_velocity)
786 * (damping.angular * delta_secs).min(1.0);
787
788 if !body1.flags.is_kinematic() {
789 body1.angular_velocity += delta_omega;
790 }
791 if !body2.flags.is_kinematic() {
792 body2.angular_velocity -= delta_omega;
793 }
794
795 let delta_v = (body2.linear_velocity - body1.linear_velocity)
796 * (damping.linear * delta_secs).min(1.0);
797
798 let w1 = inertia1.effective_inv_mass();
799 let w2 = inertia2.effective_inv_mass();
800
801 let p = delta_v * (w1 + w2).recip_or_zero();
802
803 body1.linear_velocity += p * w1;
804 body2.linear_velocity -= p * w2;
805 }
806}