use crate::{
dynamics::{
joints::EntityConstraint,
solver::{
SolverDiagnostics,
constraint_graph::{
COLOR_OVERFLOW_INDEX, ConstraintGraph, ContactManifoldHandle, GraphColor,
},
contact::ContactConstraint,
schedule::SubstepSolverSystems,
softness_parameters::{SoftnessCoefficients, SoftnessParameters},
solver_body::{SolverBody, SolverBodyInertia},
},
},
prelude::*,
};
use bevy::{
ecs::{query::QueryData, system::lifetimeless::Read},
prelude::*,
};
use core::cmp::Ordering;
pub struct SolverPlugin {
length_unit: Scalar,
}
impl Default for SolverPlugin {
fn default() -> Self {
Self::new_with_length_unit(1.0)
}
}
impl SolverPlugin {
pub fn new_with_length_unit(unit: Scalar) -> Self {
Self { length_unit: unit }
}
}
impl Plugin for SolverPlugin {
fn build(&self, app: &mut App) {
app.init_resource::<SolverConfig>()
.init_resource::<ContactSoftnessCoefficients>()
.init_resource::<ContactConstraints>()
.init_resource::<ConstraintGraph>();
if app
.world()
.get_resource::<PhysicsLengthUnit>()
.is_none_or(|unit| unit.0 == 1.0)
{
app.insert_resource(PhysicsLengthUnit(self.length_unit));
}
let physics = app
.get_schedule_mut(PhysicsSchedule)
.expect("add PhysicsSchedule first");
physics.add_systems(update_contact_softness.before(PhysicsStepSystems::NarrowPhase));
physics.add_systems(
prepare_contact_constraints.in_set(SolverSystems::PrepareContactConstraints),
);
physics.add_systems(solve_restitution.in_set(SolverSystems::Restitution));
physics.add_systems(store_contact_impulses.in_set(SolverSystems::StoreContactImpulses));
let substeps = app
.get_schedule_mut(SubstepSchedule)
.expect("add SubstepSchedule first");
substeps.add_systems(warm_start.in_set(SubstepSolverSystems::WarmStart));
substeps.add_systems(solve_contacts::<true>.in_set(SubstepSolverSystems::SolveConstraints));
substeps.add_systems(solve_contacts::<false>.in_set(SubstepSolverSystems::Relax));
substeps.add_systems(
(
joint_damping::<FixedJoint>,
joint_damping::<RevoluteJoint>,
#[cfg(feature = "3d")]
joint_damping::<SphericalJoint>,
joint_damping::<PrismaticJoint>,
joint_damping::<DistanceJoint>,
)
.chain()
.in_set(SubstepSolverSystems::Damping),
);
}
fn finish(&self, app: &mut App) {
app.register_physics_diagnostics::<SolverDiagnostics>();
}
}
#[derive(Resource, Clone, Debug, Deref, DerefMut, PartialEq, Reflect)]
#[reflect(Resource)]
pub struct PhysicsLengthUnit(pub Scalar);
impl Default for PhysicsLengthUnit {
fn default() -> Self {
Self(1.0)
}
}
#[derive(Resource, Clone, Debug, PartialEq, Reflect)]
#[reflect(Resource)]
pub struct SolverConfig {
pub contact_damping_ratio: Scalar,
pub contact_frequency_factor: Scalar,
pub max_overlap_solve_speed: Scalar,
pub warm_start_coefficient: Scalar,
pub restitution_threshold: Scalar,
pub restitution_iterations: usize,
}
impl Default for SolverConfig {
fn default() -> Self {
Self {
contact_damping_ratio: 10.0,
contact_frequency_factor: 1.5,
max_overlap_solve_speed: 4.0,
warm_start_coefficient: 1.0,
restitution_threshold: 1.0,
restitution_iterations: 1,
}
}
}
#[derive(Resource, Clone, Copy, PartialEq, Reflect)]
#[reflect(Resource)]
pub struct ContactSoftnessCoefficients {
pub dynamic: SoftnessCoefficients,
pub non_dynamic: SoftnessCoefficients,
}
impl Default for ContactSoftnessCoefficients {
fn default() -> Self {
Self {
dynamic: SoftnessParameters::new(10.0, 30.0).compute_coefficients(1.0 / 60.0),
non_dynamic: SoftnessParameters::new(10.0, 60.0).compute_coefficients(1.0 / 60.0),
}
}
}
fn update_contact_softness(
mut coefficients: ResMut<ContactSoftnessCoefficients>,
solver_config: Res<SolverConfig>,
physics_time: Res<Time<Physics>>,
substep_time: Res<Time<Substeps>>,
) {
if solver_config.is_changed() || physics_time.is_changed() || substep_time.is_changed() {
let dt = physics_time.delta_secs_f64() as Scalar;
let h = substep_time.delta_secs_f64() as Scalar;
let max_hz = 1.0 / (dt * 2.0);
let hz = solver_config.contact_frequency_factor * max_hz.min(0.25 / h);
coefficients.dynamic = SoftnessParameters::new(solver_config.contact_damping_ratio, hz)
.compute_coefficients(h);
coefficients.non_dynamic =
SoftnessParameters::new(solver_config.contact_damping_ratio, 2.0 * hz)
.compute_coefficients(h);
}
}
#[derive(Resource, Default, Deref, DerefMut)]
pub struct ContactConstraints(pub Vec<ContactConstraint>);
#[derive(QueryData)]
pub(super) struct BodyQuery {
pub(super) rb: Read<RigidBody>,
pub(super) linear_velocity: Read<LinearVelocity>,
pub(super) inertia: Option<Read<SolverBodyInertia>>,
}
fn prepare_contact_constraints(
contact_graph: Res<ContactGraph>,
mut constraint_graph: ResMut<ConstraintGraph>,
mut diagnostics: ResMut<SolverDiagnostics>,
bodies: Query<BodyQuery, RigidBodyActiveFilter>,
narrow_phase_config: Res<NarrowPhaseConfig>,
contact_softness: Res<ContactSoftnessCoefficients>,
) {
let start = crate::utils::Instant::now();
for color in constraint_graph.colors.iter_mut() {
color.contact_constraints.clear();
}
let mut active_colors = constraint_graph
.colors
.iter_mut()
.filter(|color| !color.manifold_handles.is_empty())
.collect::<Vec<&mut GraphColor>>();
crate::utils::par_for_each(&mut active_colors, 2, |_i, color| {
for handle in color.manifold_handles.iter() {
let contact_pair = contact_graph
.get_by_id(handle.contact_id)
.unwrap_or_else(|| {
panic!("Contact pair not found in graph: {:?}", handle.contact_id)
})
.1;
let manifold_index = handle.manifold_index;
let manifold = &contact_pair.manifolds[manifold_index];
if !contact_pair.generates_constraints() {
continue;
}
let (Some(body1_entity), Some(body2_entity)) = (contact_pair.body1, contact_pair.body2)
else {
continue;
};
let Ok(body1) = bodies.get(body1_entity) else {
continue;
};
let Ok(body2) = bodies.get(body2_entity) else {
continue;
};
if !body1.rb.is_dynamic() && !body2.rb.is_dynamic() {
continue;
}
let constraint = ContactConstraint::generate(
body1_entity,
body2_entity,
body1,
body2,
contact_pair.contact_id,
manifold,
manifold_index,
narrow_phase_config.match_contacts,
&contact_softness,
);
if !constraint.points.is_empty() {
color.contact_constraints.push(constraint);
}
}
});
diagnostics.prepare_constraints += start.elapsed();
diagnostics.contact_constraint_count = constraint_graph
.colors
.iter()
.map(|color| color.contact_constraints.len())
.sum::<usize>() as u32;
}
fn warm_start(
bodies: Query<(&mut SolverBody, &SolverBodyInertia)>,
mut constraint_graph: ResMut<ConstraintGraph>,
solver_config: Res<SolverConfig>,
mut diagnostics: ResMut<SolverDiagnostics>,
) {
let start = crate::utils::Instant::now();
for constraint in constraint_graph.colors[COLOR_OVERFLOW_INDEX]
.contact_constraints
.iter_mut()
{
warm_start_internal(&bodies, constraint, solver_config.warm_start_coefficient);
}
for color in constraint_graph
.colors
.iter_mut()
.take(COLOR_OVERFLOW_INDEX)
.filter(|color| !color.contact_constraints.is_empty())
{
crate::utils::par_for_each(&mut color.contact_constraints, 64, |_i, constraint| {
warm_start_internal(&bodies, constraint, solver_config.warm_start_coefficient);
});
}
diagnostics.warm_start += start.elapsed();
}
fn warm_start_internal(
bodies: &Query<(&mut SolverBody, &SolverBodyInertia)>,
constraint: &mut ContactConstraint,
warm_start_coefficient: Scalar,
) {
debug_assert!(!constraint.points.is_empty());
let mut dummy_body1 = SolverBody::DUMMY;
let mut dummy_body2 = SolverBody::DUMMY;
let (mut body1, mut inertia1) = (&mut dummy_body1, &SolverBodyInertia::DUMMY);
let (mut body2, mut inertia2) = (&mut dummy_body2, &SolverBodyInertia::DUMMY);
if let Ok((body, inertia)) = unsafe { bodies.get_unchecked(constraint.body1) } {
body1 = body.into_inner();
inertia1 = inertia;
}
if let Ok((body, inertia)) = unsafe { bodies.get_unchecked(constraint.body2) } {
body2 = body.into_inner();
inertia2 = inertia;
}
match constraint.relative_dominance.cmp(&0) {
Ordering::Greater => inertia1 = &SolverBodyInertia::DUMMY,
Ordering::Less => inertia2 = &SolverBodyInertia::DUMMY,
_ => {}
}
constraint.warm_start(body1, body2, inertia1, inertia2, warm_start_coefficient);
}
#[allow(clippy::too_many_arguments)]
#[allow(clippy::type_complexity)]
fn solve_contacts<const USE_BIAS: bool>(
bodies: Query<(&mut SolverBody, &SolverBodyInertia)>,
mut constraint_graph: ResMut<ConstraintGraph>,
solver_config: Res<SolverConfig>,
length_unit: Res<PhysicsLengthUnit>,
time: Res<Time>,
mut diagnostics: ResMut<SolverDiagnostics>,
) {
let start = crate::utils::Instant::now();
let delta_secs = time.delta_seconds_adjusted();
let max_overlap_solve_speed = solver_config.max_overlap_solve_speed * length_unit.0;
for constraint in constraint_graph.colors[COLOR_OVERFLOW_INDEX]
.contact_constraints
.iter_mut()
{
solve_contacts_internal::<USE_BIAS>(
&bodies,
constraint,
max_overlap_solve_speed,
delta_secs,
);
}
for color in constraint_graph
.colors
.iter_mut()
.take(COLOR_OVERFLOW_INDEX)
.filter(|color| !color.contact_constraints.is_empty())
{
crate::utils::par_for_each(&mut color.contact_constraints, 64, |_i, constraint| {
solve_contacts_internal::<USE_BIAS>(
&bodies,
constraint,
max_overlap_solve_speed,
delta_secs,
);
});
}
if USE_BIAS {
diagnostics.solve_constraints += start.elapsed();
} else {
diagnostics.relax_velocities += start.elapsed();
}
}
fn solve_contacts_internal<const USE_BIAS: bool>(
bodies: &Query<(&mut SolverBody, &SolverBodyInertia)>,
constraint: &mut ContactConstraint,
max_overlap_solve_speed: Scalar,
delta_secs: Scalar,
) {
let mut dummy_body1 = SolverBody::DUMMY;
let mut dummy_body2 = SolverBody::DUMMY;
let (mut body1, mut inertia1) = (&mut dummy_body1, &SolverBodyInertia::DUMMY);
let (mut body2, mut inertia2) = (&mut dummy_body2, &SolverBodyInertia::DUMMY);
if let Ok((body, inertia)) = unsafe { bodies.get_unchecked(constraint.body1) } {
body1 = body.into_inner();
inertia1 = inertia;
}
if let Ok((body, inertia)) = unsafe { bodies.get_unchecked(constraint.body2) } {
body2 = body.into_inner();
inertia2 = inertia;
}
match constraint.relative_dominance.cmp(&0) {
Ordering::Greater => inertia1 = &SolverBodyInertia::DUMMY,
Ordering::Less => inertia2 = &SolverBodyInertia::DUMMY,
_ => {}
}
constraint.solve(
body1,
body2,
inertia1,
inertia2,
delta_secs,
USE_BIAS,
max_overlap_solve_speed,
);
}
#[allow(clippy::too_many_arguments)]
#[allow(clippy::type_complexity)]
fn solve_restitution(
bodies: Query<(&mut SolverBody, &SolverBodyInertia)>,
mut constraint_graph: ResMut<ConstraintGraph>,
solver_config: Res<SolverConfig>,
length_unit: Res<PhysicsLengthUnit>,
mut diagnostics: ResMut<SolverDiagnostics>,
) {
let start = crate::utils::Instant::now();
let threshold = solver_config.restitution_threshold * length_unit.0;
for constraint in constraint_graph.colors[COLOR_OVERFLOW_INDEX]
.contact_constraints
.iter_mut()
{
solve_restitution_internal(
&bodies,
constraint,
threshold,
solver_config.restitution_iterations,
);
}
for color in constraint_graph
.colors
.iter_mut()
.take(COLOR_OVERFLOW_INDEX)
.filter(|color| !color.contact_constraints.is_empty())
{
crate::utils::par_for_each(&mut color.contact_constraints, 64, |_i, constraint| {
solve_restitution_internal(
&bodies,
constraint,
threshold,
solver_config.restitution_iterations,
);
});
}
diagnostics.apply_restitution += start.elapsed();
}
fn solve_restitution_internal(
bodies: &Query<(&mut SolverBody, &SolverBodyInertia)>,
constraint: &mut ContactConstraint,
threshold: Scalar,
iterations: usize,
) {
let restitution = constraint.restitution;
if restitution == 0.0 {
return;
}
let mut dummy_body1 = SolverBody::DUMMY;
let mut dummy_body2 = SolverBody::DUMMY;
let (mut body1, mut inertia1) = (&mut dummy_body1, &SolverBodyInertia::DUMMY);
let (mut body2, mut inertia2) = (&mut dummy_body2, &SolverBodyInertia::DUMMY);
if let Ok((body, inertia)) = unsafe { bodies.get_unchecked(constraint.body1) } {
body1 = body.into_inner();
inertia1 = inertia;
}
if let Ok((body, inertia)) = unsafe { bodies.get_unchecked(constraint.body2) } {
body2 = body.into_inner();
inertia2 = inertia;
}
match constraint.relative_dominance.cmp(&0) {
Ordering::Greater => inertia1 = &SolverBodyInertia::DUMMY,
Ordering::Less => inertia2 = &SolverBodyInertia::DUMMY,
_ => {}
}
let point_count = constraint.points.len();
let iterations = if point_count > 1 { iterations } else { 1 };
for _ in 0..iterations {
constraint.apply_restitution(body1, body2, inertia1, inertia2, threshold);
}
}
fn store_contact_impulses(
mut contact_graph: ResMut<ContactGraph>,
mut constraint_graph: ResMut<ConstraintGraph>,
mut diagnostics: ResMut<SolverDiagnostics>,
) {
let start = crate::utils::Instant::now();
for color in constraint_graph.colors.iter_mut() {
for constraint in &mut color.contact_constraints {
let Some(manifold) = contact_graph.get_manifold_mut(ContactManifoldHandle {
contact_id: constraint.contact_id,
manifold_index: constraint.manifold_index,
}) else {
unreachable!(
"Contact manifold {:?} for contact ID {:?} not found in contact graph.",
constraint.contact_id, constraint.manifold_index
);
};
for (contact, constraint_point) in
manifold.points.iter_mut().zip(constraint.points.iter())
{
contact.warm_start_normal_impulse = constraint_point.normal_part.impulse;
contact.warm_start_tangent_impulse = constraint_point
.tangent_part
.as_ref()
.map_or(default(), |part| part.impulse);
contact.normal_impulse = constraint_point.normal_part.total_impulse;
}
}
}
diagnostics.store_impulses += start.elapsed();
}
#[allow(clippy::type_complexity)]
pub fn joint_damping<T: Component + EntityConstraint<2>>(
bodies: Query<(&mut SolverBody, &SolverBodyInertia)>,
joints: Query<(&T, &JointDamping)>,
time: Res<Time>,
) {
let delta_secs = time.delta_seconds_adjusted();
let mut dummy_body1 = SolverBody::DUMMY;
let mut dummy_body2 = SolverBody::DUMMY;
for (joint, damping) in &joints {
let entities = joint.entities();
let (mut body1, mut inertia1) = (&mut dummy_body1, &SolverBodyInertia::DUMMY);
let (mut body2, mut inertia2) = (&mut dummy_body2, &SolverBodyInertia::DUMMY);
if let Ok((body, inertia)) = unsafe { bodies.get_unchecked(entities[0]) } {
body1 = body.into_inner();
inertia1 = inertia;
}
if let Ok((body, inertia)) = unsafe { bodies.get_unchecked(entities[1]) } {
body2 = body.into_inner();
inertia2 = inertia;
}
let delta_omega = (body2.angular_velocity - body1.angular_velocity)
* (damping.angular * delta_secs).min(1.0);
if !body1.flags.is_kinematic() {
body1.angular_velocity += delta_omega;
}
if !body2.flags.is_kinematic() {
body2.angular_velocity -= delta_omega;
}
let delta_v = (body2.linear_velocity - body1.linear_velocity)
* (damping.linear * delta_secs).min(1.0);
let w1 = inertia1.effective_inv_mass();
let w2 = inertia2.effective_inv_mass();
let p = delta_v * (w1 + w2).recip_or_zero();
body1.linear_velocity += p * w1;
body2.linear_velocity -= p * w2;
}
}