use bevy::{
ecs::{entity_disabling::Disabled, query::QueryFilter, world::DeferredWorld},
prelude::*,
};
use super::{SolverBody, SolverBodyInertia};
use crate::{
AngularVelocity, LinearVelocity, PhysicsSchedule, Position, RigidBody, RigidBodyActiveFilter,
RigidBodyDisabled, Rotation, Sleeping, SolverSystems, Vector,
dynamics::solver::{SolverDiagnostics, solver_body::SolverBodyFlags},
prelude::{
AppDiagnosticsExt, ComputedAngularInertia, ComputedCenterOfMass, ComputedMass, Dominance,
LockedAxes,
},
};
#[cfg(feature = "3d")]
use crate::{
MatExt,
dynamics::integrator::{IntegrationSystems, integrate_positions},
prelude::SubstepSchedule,
};
pub struct SolverBodyPlugin;
impl Plugin for SolverBodyPlugin {
fn build(&self, app: &mut App) {
app.add_observer(on_insert_rigid_body);
app.add_observer(
|trigger: On<Remove, RigidBodyDisabled>,
rb_query: Query<&RigidBody, Without<Sleeping>>,
commands: Commands| {
add_solver_body::<Without<Sleeping>>(In(trigger.entity), rb_query, commands);
},
);
app.add_observer(
|trigger: On<Remove, Disabled>,
rb_query: Query<
&RigidBody,
(
// The body still has `Disabled` at this point,
// and we need to include in the query to match against the entity.
With<Disabled>,
Without<RigidBodyDisabled>,
Without<Sleeping>,
),
>,
commands: Commands| {
add_solver_body::<(
With<Disabled>,
Without<RigidBodyDisabled>,
Without<Sleeping>,
)>(In(trigger.entity), rb_query, commands);
},
);
app.add_observer(
|trigger: On<Remove, Sleeping>,
rb_query: Query<&RigidBody, Without<RigidBodyDisabled>>,
commands: Commands| {
add_solver_body::<Without<RigidBodyDisabled>>(
In(trigger.entity),
rb_query,
commands,
);
},
);
app.add_observer(
|trigger: On<Remove, RigidBody>, deferred_world: DeferredWorld| {
remove_solver_body(In(trigger.entity), deferred_world);
},
);
app.add_observer(
|trigger: On<Add, (Disabled, RigidBodyDisabled, Sleeping)>,
deferred_world: DeferredWorld| {
remove_solver_body(In(trigger.entity), deferred_world);
},
);
app.add_systems(
PhysicsSchedule,
prepare_solver_bodies
.chain()
.in_set(SolverSystems::PrepareSolverBodies),
);
app.add_systems(
PhysicsSchedule,
writeback_solver_bodies.in_set(SolverSystems::Finalize),
);
#[cfg(feature = "3d")]
app.add_systems(
SubstepSchedule,
update_solver_body_angular_inertia
.in_set(IntegrationSystems::Position)
.after(integrate_positions),
);
}
fn finish(&self, app: &mut App) {
app.register_physics_diagnostics::<SolverDiagnostics>();
}
}
fn on_insert_rigid_body(
trigger: On<Insert, RigidBody>,
bodies: Query<(&RigidBody, Has<SolverBody>), RigidBodyActiveFilter>,
mut commands: Commands,
) {
let Ok((rb, has_solver_body)) = bodies.get(trigger.entity) else {
return;
};
if rb.is_static() && has_solver_body {
commands
.entity(trigger.entity)
.try_remove::<(SolverBody, SolverBodyInertia)>();
} else if !rb.is_static() && !has_solver_body {
commands
.entity(trigger.entity)
.try_insert((SolverBody::default(), SolverBodyInertia::default()));
}
}
fn add_solver_body<F: QueryFilter>(
In(entity): In<Entity>,
mut rb_query: Query<&RigidBody, F>,
mut commands: Commands,
) {
if let Ok(rb) = rb_query.get_mut(entity) {
if rb.is_static() {
return;
}
commands
.entity(entity)
.try_insert((SolverBody::default(), SolverBodyInertia::default()));
}
}
fn remove_solver_body(In(entity): In<Entity>, mut deferred_world: DeferredWorld) {
let entity_ref = deferred_world.entity(entity);
if entity_ref.contains::<SolverBody>() {
deferred_world
.commands()
.entity(entity)
.try_remove::<(SolverBody, SolverBodyInertia)>();
}
}
fn prepare_solver_bodies(
mut query: Query<(
&RigidBody,
&mut SolverBody,
&mut SolverBodyInertia,
&LinearVelocity,
&AngularVelocity,
&Rotation,
&ComputedMass,
&ComputedAngularInertia,
Option<&LockedAxes>,
Option<&Dominance>,
)>,
) {
#[allow(unused_variables)]
query.par_iter_mut().for_each(
|(
rb,
mut solver_body,
mut inertial_properties,
linear_velocity,
angular_velocity,
rotation,
mass,
angular_inertia,
locked_axes,
dominance,
)| {
solver_body.linear_velocity = linear_velocity.0;
solver_body.angular_velocity = angular_velocity.0;
solver_body.delta_position = Vector::ZERO;
solver_body.delta_rotation = Rotation::IDENTITY;
let locked_axes = locked_axes.copied().unwrap_or_default();
*inertial_properties = SolverBodyInertia::new(
mass.inverse(),
#[cfg(feature = "2d")]
angular_inertia.inverse(),
#[cfg(feature = "3d")]
angular_inertia.rotated(rotation.0).inverse(),
locked_axes,
dominance.map_or(0, |dominance| dominance.0),
rb.is_dynamic(),
);
solver_body.flags = SolverBodyFlags(locked_axes.to_bits() as u32);
solver_body
.flags
.set(SolverBodyFlags::IS_KINEMATIC, rb.is_kinematic());
#[cfg(feature = "3d")]
{
let epsilon = 1e-6;
let is_gyroscopic = !locked_axes.is_rotation_locked()
&& !angular_inertia.inverse().is_isotropic(epsilon);
solver_body
.flags
.set(SolverBodyFlags::GYROSCOPIC_MOTION, is_gyroscopic);
}
},
);
}
#[allow(clippy::type_complexity)]
fn writeback_solver_bodies(
mut query: Query<(
&SolverBody,
&mut Position,
&mut Rotation,
&ComputedCenterOfMass,
&mut LinearVelocity,
&mut AngularVelocity,
)>,
mut diagnostics: ResMut<SolverDiagnostics>,
) {
let start = bevy::platform::time::Instant::now();
query.par_iter_mut().for_each(
|(solver_body, mut pos, mut rot, com, mut lin_vel, mut ang_vel)| {
let old_world_com = *rot * com.0;
*rot = (solver_body.delta_rotation * *rot).fast_renormalize();
let new_world_com = *rot * com.0;
pos.0 += solver_body.delta_position + old_world_com - new_world_com;
lin_vel.0 = solver_body.linear_velocity;
ang_vel.0 = solver_body.angular_velocity;
},
);
diagnostics.finalize += start.elapsed();
}
#[cfg(feature = "3d")]
pub(crate) fn update_solver_body_angular_inertia(
mut query: Query<(&mut SolverBodyInertia, &ComputedAngularInertia, &Rotation)>,
) {
query
.par_iter_mut()
.for_each(|(mut inertia, angular_inertia, rotation)| {
inertia.update_effective_inv_angular_inertia(angular_inertia, rotation.0);
});
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{PhysicsSchedulePlugin, SolverSchedulePlugin};
fn create_app() -> App {
let mut app = App::new();
app.add_plugins((
PhysicsSchedulePlugin::default(),
SolverSchedulePlugin,
SolverBodyPlugin,
));
app
}
fn has_solver_body(app: &App, entity: Entity) -> bool {
app.world().get::<SolverBody>(entity).is_some()
}
#[test]
fn add_remove_solver_bodies() {
let mut app = create_app();
let entity1 = app.world_mut().spawn(RigidBody::Dynamic).id();
let entity2 = app.world_mut().spawn(RigidBody::Kinematic).id();
let entity3 = app.world_mut().spawn(RigidBody::Static).id();
app.update();
assert!(has_solver_body(&app, entity1));
assert!(has_solver_body(&app, entity2));
assert!(!has_solver_body(&app, entity3));
app.world_mut()
.entity_mut(entity1)
.insert(RigidBodyDisabled);
app.update();
assert!(!has_solver_body(&app, entity1));
app.world_mut()
.entity_mut(entity1)
.remove::<RigidBodyDisabled>();
app.update();
assert!(has_solver_body(&app, entity1));
}
}