Fix user changes handling (#803)

* add failing test from @Johannes0021

* apply fix on update_positions

* apply fix on ColliderSet::iter_mut

* fix clippy..

* more complete test

* feat: refactor modified sets into a wrapper to avoid future mistakes

* chore: fix typos

---------

Co-authored-by: Sébastien Crozet <sebcrozet@dimforge.com>
This commit is contained in:
Thierry Berger
2025-03-28 12:48:25 +01:00
committed by GitHub
parent d291041278
commit 176c3bae14
10 changed files with 272 additions and 78 deletions

View File

@@ -2,8 +2,10 @@
pub use self::arena::{Arena, Index};
pub use self::coarena::Coarena;
pub(crate) use self::modified_objects::{HasModifiedFlag, ModifiedObjects};
pub mod arena;
mod coarena;
pub(crate) mod graph;
mod modified_objects;
pub mod pubsub;

View File

@@ -0,0 +1,65 @@
use std::marker::PhantomData;
use std::ops::Deref;
/// Contains handles of modified objects.
///
/// This is a wrapper over a `Vec` to ensure we dont forget to set the objects
/// MODIFIED flag when adding it to this set.
/// It is possible to bypass the wrapper with `.as_mut_internal`. But this should only
/// be done for internal engine usage (like the physics pipeline).
#[cfg_attr(feature = "serde-serialize", derive(Serialize, Deserialize))]
#[derive(Clone, Debug)]
pub(crate) struct ModifiedObjects<Handle, Object>(Vec<Handle>, PhantomData<Object>);
impl<Handle, Object> Default for ModifiedObjects<Handle, Object> {
fn default() -> Self {
Self(Vec::new(), PhantomData)
}
}
pub(crate) trait HasModifiedFlag {
fn has_modified_flag(&self) -> bool;
fn set_modified_flag(&mut self);
}
impl<Handle, Object> Deref for ModifiedObjects<Handle, Object> {
type Target = Vec<Handle>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl<Handle, Object: HasModifiedFlag> ModifiedObjects<Handle, Object> {
pub fn with_capacity(capacity: usize) -> Self {
Self(Vec::with_capacity(capacity), PhantomData)
}
/// Remove every handle from this set.
///
/// Note that the corresponding object MODIFIED flags wont be reset automatically by this function.
pub fn clear(&mut self) {
self.0.clear()
}
/// Pushes a object handle to this set after checking that it doesnt have the MODIFIED
/// flag set.
///
/// This will also set the objects MODIFIED flag.
pub fn push_once(&mut self, handle: Handle, object: &mut Object) {
if !object.has_modified_flag() {
self.push_unchecked(handle, object);
}
}
/// Pushes an object handle to this set without checking if the object already has the MODIFIED
/// flags.
///
/// Only use in situation where you are certain (due to other contextual information) that
/// the object isnt already in the set.
///
/// This will also set the objects MODIFIED flag.
pub fn push_unchecked(&mut self, handle: Handle, object: &mut Object) {
object.set_modified_flag();
self.0.push(handle);
}
}

View File

@@ -8,6 +8,7 @@ pub(crate) use self::joint::JointGraphEdge;
pub(crate) use self::joint::JointIndex;
pub use self::joint::*;
pub use self::rigid_body_components::*;
pub(crate) use self::rigid_body_set::ModifiedRigidBodies;
// #[cfg(not(feature = "parallel"))]
pub(crate) use self::solver::IslandSolver;
// #[cfg(feature = "parallel")]

View File

@@ -4,7 +4,7 @@ use crate::control::PdErrors;
use crate::dynamics::MassProperties;
use crate::geometry::{
ColliderChanges, ColliderHandle, ColliderMassProps, ColliderParent, ColliderPosition,
ColliderSet, ColliderShape,
ColliderSet, ColliderShape, ModifiedColliders,
};
use crate::math::{
AngVector, AngularInertia, Isometry, Point, Real, Rotation, Translation, Vector,
@@ -1020,10 +1020,10 @@ impl RigidBodyColliders {
}
/// Update the positions of all the colliders attached to this rigid-body.
pub fn update_positions(
pub(crate) fn update_positions(
&self,
colliders: &mut ColliderSet,
modified_colliders: &mut Vec<ColliderHandle>,
modified_colliders: &mut ModifiedColliders,
parent_pos: &Isometry<Real>,
) {
for handle in &self.0 {
@@ -1031,12 +1031,10 @@ impl RigidBodyColliders {
let co = colliders.index_mut_internal(*handle);
let new_pos = parent_pos * co.parent.as_ref().unwrap().pos_wrt_parent;
if !co.changes.contains(ColliderChanges::MODIFIED) {
modified_colliders.push(*handle);
}
// Set the modification flag so we can benefit from the modification-tracking
// when updating the narrow-phase/broad-phase afterwards.
modified_colliders.push_once(*handle, co);
co.changes |= ColliderChanges::POSITION;
co.pos = ColliderPosition(new_pos);
}

View File

@@ -1,4 +1,4 @@
use crate::data::Arena;
use crate::data::{Arena, HasModifiedFlag, ModifiedObjects};
use crate::dynamics::{
ImpulseJointSet, IslandManager, MultibodyJointSet, RigidBody, RigidBodyChanges, RigidBodyHandle,
};
@@ -22,6 +22,20 @@ impl BodyPair {
}
}
pub(crate) type ModifiedRigidBodies = ModifiedObjects<RigidBodyHandle, RigidBody>;
impl HasModifiedFlag for RigidBody {
#[inline]
fn has_modified_flag(&self) -> bool {
self.changes.contains(RigidBodyChanges::MODIFIED)
}
#[inline]
fn set_modified_flag(&mut self) {
self.changes |= RigidBodyChanges::MODIFIED;
}
}
#[cfg_attr(feature = "serde-serialize", derive(Serialize, Deserialize))]
#[derive(Clone, Default, Debug)]
/// A set of rigid bodies that can be handled by a physics pipeline.
@@ -31,7 +45,7 @@ pub struct RigidBodySet {
// parallelism because the `Receiver` breaks the Sync impl.
// Could we avoid this?
pub(crate) bodies: Arena<RigidBody>,
pub(crate) modified_bodies: Vec<RigidBodyHandle>,
pub(crate) modified_bodies: ModifiedRigidBodies,
}
impl RigidBodySet {
@@ -39,7 +53,7 @@ impl RigidBodySet {
pub fn new() -> Self {
RigidBodySet {
bodies: Arena::new(),
modified_bodies: Vec::new(),
modified_bodies: ModifiedObjects::default(),
}
}
@@ -47,11 +61,11 @@ impl RigidBodySet {
pub fn with_capacity(capacity: usize) -> Self {
RigidBodySet {
bodies: Arena::with_capacity(capacity),
modified_bodies: Vec::with_capacity(capacity),
modified_bodies: ModifiedRigidBodies::with_capacity(capacity),
}
}
pub(crate) fn take_modified(&mut self) -> Vec<RigidBodyHandle> {
pub(crate) fn take_modified(&mut self) -> ModifiedRigidBodies {
std::mem::take(&mut self.modified_bodies)
}
@@ -79,7 +93,10 @@ impl RigidBodySet {
rb.changes.set(RigidBodyChanges::all(), true);
let handle = RigidBodyHandle(self.bodies.insert(rb));
self.modified_bodies.push(handle);
// Using push_unchecked because this is a brand new rigid-body with the MODIFIED
// flags set but isnt in the modified_bodies yet.
self.modified_bodies
.push_unchecked(handle, &mut self.bodies[handle.0]);
handle
}
@@ -152,7 +169,7 @@ impl RigidBodySet {
pub fn get_unknown_gen_mut(&mut self, i: u32) -> Option<(&mut RigidBody, RigidBodyHandle)> {
let (rb, handle) = self.bodies.get_unknown_gen_mut(i)?;
let handle = RigidBodyHandle(handle);
Self::mark_as_modified(handle, rb, &mut self.modified_bodies);
self.modified_bodies.push_once(handle, rb);
Some((rb, handle))
}
@@ -161,22 +178,11 @@ impl RigidBodySet {
self.bodies.get(handle.0)
}
pub(crate) fn mark_as_modified(
handle: RigidBodyHandle,
rb: &mut RigidBody,
modified_bodies: &mut Vec<RigidBodyHandle>,
) {
if !rb.changes.contains(RigidBodyChanges::MODIFIED) {
rb.changes = RigidBodyChanges::MODIFIED;
modified_bodies.push(handle);
}
}
/// Gets a mutable reference to the rigid-body with the given handle.
#[cfg(not(feature = "dev-remove-slow-accessors"))]
pub fn get_mut(&mut self, handle: RigidBodyHandle) -> Option<&mut RigidBody> {
let result = self.bodies.get_mut(handle.0)?;
Self::mark_as_modified(handle, result, &mut self.modified_bodies);
self.modified_bodies.push_once(handle, result);
Some(result)
}
@@ -195,7 +201,7 @@ impl RigidBodySet {
handle: RigidBodyHandle,
) -> Option<&mut RigidBody> {
let result = self.bodies.get_mut(handle.0)?;
Self::mark_as_modified(handle, result, &mut self.modified_bodies);
self.modified_bodies.push_once(handle, result);
Some(result)
}
@@ -210,7 +216,9 @@ impl RigidBodySet {
self.modified_bodies.clear();
let modified_bodies = &mut self.modified_bodies;
self.bodies.iter_mut().map(move |(h, b)| {
modified_bodies.push(RigidBodyHandle(h));
// NOTE: using `push_unchecked` because we just cleared `modified_bodies`
// before iterating.
modified_bodies.push_unchecked(RigidBodyHandle(h), b);
(RigidBodyHandle(h), b)
})
}
@@ -256,7 +264,7 @@ impl Index<crate::data::Index> for RigidBodySet {
impl IndexMut<RigidBodyHandle> for RigidBodySet {
fn index_mut(&mut self, handle: RigidBodyHandle) -> &mut RigidBody {
let rb = &mut self.bodies[handle.0];
Self::mark_as_modified(handle, rb, &mut self.modified_bodies);
self.modified_bodies.push_once(handle, rb);
rb
}
}

View File

@@ -1,15 +1,30 @@
use crate::data::arena::Arena;
use crate::data::{HasModifiedFlag, ModifiedObjects};
use crate::dynamics::{IslandManager, RigidBodyHandle, RigidBodySet};
use crate::geometry::{Collider, ColliderChanges, ColliderHandle, ColliderParent};
use crate::math::Isometry;
use std::ops::{Index, IndexMut};
pub(crate) type ModifiedColliders = ModifiedObjects<ColliderHandle, Collider>;
impl HasModifiedFlag for Collider {
#[inline]
fn has_modified_flag(&self) -> bool {
self.changes.contains(ColliderChanges::MODIFIED)
}
#[inline]
fn set_modified_flag(&mut self) {
self.changes |= ColliderChanges::MODIFIED;
}
}
#[cfg_attr(feature = "serde-serialize", derive(Serialize, Deserialize))]
#[derive(Clone, Default, Debug)]
/// A set of colliders that can be handled by a physics `World`.
pub struct ColliderSet {
pub(crate) colliders: Arena<Collider>,
pub(crate) modified_colliders: Vec<ColliderHandle>,
pub(crate) modified_colliders: ModifiedColliders,
pub(crate) removed_colliders: Vec<ColliderHandle>,
}
@@ -18,7 +33,7 @@ impl ColliderSet {
pub fn new() -> Self {
ColliderSet {
colliders: Arena::new(),
modified_colliders: Vec::new(),
modified_colliders: Default::default(),
removed_colliders: Vec::new(),
}
}
@@ -29,12 +44,12 @@ impl ColliderSet {
pub fn with_capacity(capacity: usize) -> Self {
ColliderSet {
colliders: Arena::with_capacity(capacity),
modified_colliders: Vec::with_capacity(capacity),
modified_colliders: ModifiedColliders::with_capacity(capacity),
removed_colliders: Vec::new(),
}
}
pub(crate) fn take_modified(&mut self) -> Vec<ColliderHandle> {
pub(crate) fn take_modified(&mut self) -> ModifiedColliders {
std::mem::take(&mut self.modified_colliders)
}
@@ -65,9 +80,11 @@ impl ColliderSet {
pub fn iter_mut(&mut self) -> impl Iterator<Item = (ColliderHandle, &mut Collider)> {
self.modified_colliders.clear();
let modified_colliders = &mut self.modified_colliders;
self.colliders.iter_mut().map(move |(h, b)| {
modified_colliders.push(ColliderHandle(h));
(ColliderHandle(h), b)
self.colliders.iter_mut().map(move |(h, co)| {
// NOTE: we push unchecked here since we are just re-populating the
// `modified_colliders` set that we just cleared before iteration.
modified_colliders.push_unchecked(ColliderHandle(h), co);
(ColliderHandle(h), co)
})
}
@@ -100,7 +117,11 @@ impl ColliderSet {
coll.reset_internal_references();
coll.parent = None;
let handle = ColliderHandle(self.colliders.insert(coll));
self.modified_colliders.push(handle);
// NOTE: we push unchecked because this is a brand-new collider
// so it was initialized with the changed flag but isnt in
// the set yet.
self.modified_colliders
.push_unchecked(handle, &mut self.colliders[handle.0]);
handle
}
@@ -131,9 +152,12 @@ impl ColliderSet {
.get_mut_internal_with_modification_tracking(parent_handle)
.expect("Parent rigid body not found.");
let handle = ColliderHandle(self.colliders.insert(coll));
self.modified_colliders.push(handle);
let coll = self.colliders.get_mut(handle.0).unwrap();
// NOTE: we push unchecked because this is a brand-new collider
// so it was initialized with the changed flag but isnt in
// the set yet.
self.modified_colliders.push_unchecked(handle, coll);
parent.add_collider_internal(
handle,
coll.parent.as_mut().unwrap(),
@@ -258,7 +282,7 @@ impl ColliderSet {
pub fn get_unknown_gen_mut(&mut self, i: u32) -> Option<(&mut Collider, ColliderHandle)> {
let (collider, handle) = self.colliders.get_unknown_gen_mut(i)?;
let handle = ColliderHandle(handle);
Self::mark_as_modified(handle, collider, &mut self.modified_colliders);
self.modified_colliders.push_once(handle, collider);
Some((collider, handle))
}
@@ -267,22 +291,11 @@ impl ColliderSet {
self.colliders.get(handle.0)
}
fn mark_as_modified(
handle: ColliderHandle,
collider: &mut Collider,
modified_colliders: &mut Vec<ColliderHandle>,
) {
if !collider.changes.contains(ColliderChanges::MODIFIED) {
collider.changes = ColliderChanges::MODIFIED;
modified_colliders.push(handle);
}
}
/// Gets a mutable reference to the collider with the given handle.
#[cfg(not(feature = "dev-remove-slow-accessors"))]
pub fn get_mut(&mut self, handle: ColliderHandle) -> Option<&mut Collider> {
let result = self.colliders.get_mut(handle.0)?;
Self::mark_as_modified(handle, result, &mut self.modified_colliders);
self.modified_colliders.push_once(handle, result);
Some(result)
}
@@ -302,7 +315,7 @@ impl ColliderSet {
handle: ColliderHandle,
) -> Option<&mut Collider> {
let result = self.colliders.get_mut(handle.0)?;
Self::mark_as_modified(handle, result, &mut self.modified_colliders);
self.modified_colliders.push_once(handle, result);
Some(result)
}
}
@@ -327,7 +340,7 @@ impl Index<ColliderHandle> for ColliderSet {
impl IndexMut<ColliderHandle> for ColliderSet {
fn index_mut(&mut self, handle: ColliderHandle) -> &mut Collider {
let collider = &mut self.colliders[handle.0];
Self::mark_as_modified(handle, collider, &mut self.modified_colliders);
self.modified_colliders.push_once(handle, collider);
collider
}
}

View File

@@ -182,6 +182,7 @@ impl ContactForceEvent {
}
pub(crate) use self::broad_phase::BroadPhaseProxyIndex;
pub(crate) use self::collider_set::ModifiedColliders;
pub(crate) use self::narrow_phase::ContactManifoldIndex;
pub(crate) use parry::partitioning::Qbvh;
pub use parry::shape::*;

View File

@@ -2,7 +2,8 @@
use crate::dynamics::{ImpulseJointSet, MultibodyJointSet};
use crate::geometry::{
BroadPhase, BroadPhasePairEvent, ColliderChanges, ColliderHandle, ColliderPair, NarrowPhase,
BroadPhase, BroadPhasePairEvent, ColliderChanges, ColliderHandle, ColliderPair,
ModifiedColliders, NarrowPhase,
};
use crate::math::Real;
use crate::pipeline::{EventHandler, PhysicsHooks, QueryPipeline};
@@ -97,13 +98,15 @@ impl CollisionPipeline {
fn clear_modified_colliders(
&mut self,
colliders: &mut ColliderSet,
modified_colliders: &mut Vec<ColliderHandle>,
modified_colliders: &mut ModifiedColliders,
) {
for handle in modified_colliders.drain(..) {
if let Some(co) = colliders.get_mut_internal(handle) {
for handle in modified_colliders.iter() {
if let Some(co) = colliders.get_mut_internal(*handle) {
co.changes = ColliderChanges::empty();
}
}
modified_colliders.clear();
}
/// Executes one step of the collision detection.

View File

@@ -7,14 +7,15 @@ use crate::dynamics::IslandSolver;
use crate::dynamics::JointGraphEdge;
use crate::dynamics::{
CCDSolver, ImpulseJointSet, IntegrationParameters, IslandManager, MultibodyJointSet,
RigidBodyChanges, RigidBodyHandle, RigidBodyPosition, RigidBodyType,
RigidBodyChanges, RigidBodyPosition, RigidBodyType,
};
use crate::geometry::{
BroadPhase, BroadPhasePairEvent, ColliderChanges, ColliderHandle, ColliderPair,
ContactManifoldIndex, NarrowPhase, TemporaryInteractionIndex,
ContactManifoldIndex, ModifiedColliders, NarrowPhase, TemporaryInteractionIndex,
};
use crate::math::{Real, Vector};
use crate::pipeline::{EventHandler, PhysicsHooks, QueryPipeline};
use crate::prelude::ModifiedRigidBodies;
use {crate::dynamics::RigidBodySet, crate::geometry::ColliderSet};
/// The physics pipeline, responsible for stepping the whole physics simulation.
@@ -68,25 +69,29 @@ impl PhysicsPipeline {
fn clear_modified_colliders(
&mut self,
colliders: &mut ColliderSet,
modified_colliders: &mut Vec<ColliderHandle>,
modified_colliders: &mut ModifiedColliders,
) {
for handle in modified_colliders.drain(..) {
if let Some(co) = colliders.get_mut_internal(handle) {
for handle in modified_colliders.iter() {
if let Some(co) = colliders.get_mut_internal(*handle) {
co.changes = ColliderChanges::empty();
}
}
modified_colliders.clear();
}
fn clear_modified_bodies(
&mut self,
bodies: &mut RigidBodySet,
modified_bodies: &mut Vec<RigidBodyHandle>,
modified_bodies: &mut ModifiedRigidBodies,
) {
for handle in modified_bodies.drain(..) {
if let Some(rb) = bodies.get_mut_internal(handle) {
for handle in modified_bodies.iter() {
if let Some(rb) = bodies.get_mut_internal(*handle) {
rb.changes = RigidBodyChanges::empty();
}
}
modified_bodies.clear();
}
fn detect_collisions(
@@ -359,7 +364,7 @@ impl PhysicsPipeline {
islands: &IslandManager,
bodies: &mut RigidBodySet,
colliders: &mut ColliderSet,
modified_colliders: &mut Vec<ColliderHandle>,
modified_colliders: &mut ModifiedColliders,
) {
// Set the rigid-bodies and kinematic bodies to their final position.
for handle in islands.iter_active_bodies() {
@@ -1011,4 +1016,107 @@ mod test {
assert!(rotation.w.is_finite());
}
}
#[test]
#[cfg(feature = "dim2")]
fn test_multi_sap_disable_body() {
use na::vector;
let mut rigid_body_set = RigidBodySet::new();
let mut collider_set = ColliderSet::new();
/* Create the ground. */
let collider = ColliderBuilder::cuboid(100.0, 0.1).build();
collider_set.insert(collider);
/* Create the bouncing ball. */
let rigid_body = RigidBodyBuilder::dynamic()
.translation(vector![0.0, 10.0])
.build();
let collider = ColliderBuilder::ball(0.5).restitution(0.7).build();
let ball_body_handle = rigid_body_set.insert(rigid_body);
collider_set.insert_with_parent(collider, ball_body_handle, &mut rigid_body_set);
/* Create other structures necessary for the simulation. */
let gravity = vector![0.0, -9.81];
let integration_parameters = IntegrationParameters::default();
let mut physics_pipeline = PhysicsPipeline::new();
let mut island_manager = IslandManager::new();
let mut broad_phase = BroadPhaseMultiSap::new();
let mut narrow_phase = NarrowPhase::new();
let mut impulse_joint_set = ImpulseJointSet::new();
let mut multibody_joint_set = MultibodyJointSet::new();
let mut ccd_solver = CCDSolver::new();
let physics_hooks = ();
let event_handler = ();
physics_pipeline.step(
&gravity,
&integration_parameters,
&mut island_manager,
&mut broad_phase,
&mut narrow_phase,
&mut rigid_body_set,
&mut collider_set,
&mut impulse_joint_set,
&mut multibody_joint_set,
&mut ccd_solver,
None,
&physics_hooks,
&event_handler,
);
// Test RigidBodyChanges::POSITION and disable
{
let ball_body = &mut rigid_body_set[ball_body_handle];
// Also, change the translation and rotation to different values
ball_body.set_translation(vector![1.0, 1.0], true);
ball_body.set_rotation(nalgebra::UnitComplex::new(1.0), true);
ball_body.set_enabled(false);
}
physics_pipeline.step(
&gravity,
&integration_parameters,
&mut island_manager,
&mut broad_phase,
&mut narrow_phase,
&mut rigid_body_set,
&mut collider_set,
&mut impulse_joint_set,
&mut multibody_joint_set,
&mut ccd_solver,
None,
&physics_hooks,
&event_handler,
);
// Test RigidBodyChanges::POSITION and enable
{
let ball_body = &mut rigid_body_set[ball_body_handle];
// Also, change the translation and rotation to different values
ball_body.set_translation(vector![0.0, 0.0], true);
ball_body.set_rotation(nalgebra::UnitComplex::new(0.0), true);
ball_body.set_enabled(true);
}
physics_pipeline.step(
&gravity,
&integration_parameters,
&mut island_manager,
&mut broad_phase,
&mut narrow_phase,
&mut rigid_body_set,
&mut collider_set,
&mut impulse_joint_set,
&mut multibody_joint_set,
&mut ccd_solver,
None,
&physics_hooks,
&event_handler,
);
}
}

View File

@@ -4,6 +4,7 @@ use crate::dynamics::{
};
use crate::geometry::{
ColliderChanges, ColliderEnabled, ColliderHandle, ColliderPosition, ColliderSet,
ModifiedColliders,
};
pub(crate) fn handle_user_changes_to_colliders(
@@ -48,7 +49,7 @@ pub(crate) fn handle_user_changes_to_rigid_bodies(
impulse_joints: &mut ImpulseJointSet,
_multibody_joints: &mut MultibodyJointSet, // FIXME: propagate disabled state to multibodies
modified_bodies: &[RigidBodyHandle],
modified_colliders: &mut Vec<ColliderHandle>,
modified_colliders: &mut ModifiedColliders,
) {
enum FinalAction {
UpdateActiveKinematicSetId(usize),
@@ -150,12 +151,8 @@ pub(crate) fn handle_user_changes_to_rigid_bodies(
// here because that would modify the `modified_colliders` inside of the `ColliderSet`
// instead of the one passed to this method.
let co = colliders.index_mut_internal(*handle);
if !co.changes.contains(ColliderChanges::MODIFIED) {
modified_colliders.push(*handle);
}
co.changes |=
ColliderChanges::MODIFIED | ColliderChanges::PARENT_EFFECTIVE_DOMINANCE;
modified_colliders.push_once(*handle, co);
co.changes |= ColliderChanges::PARENT_EFFECTIVE_DOMINANCE;
}
}
@@ -166,9 +163,7 @@ pub(crate) fn handle_user_changes_to_rigid_bodies(
// here because that would modify the `modified_colliders` inside of the `ColliderSet`
// instead of the one passed to this method.
let co = colliders.index_mut_internal(*handle);
if !co.changes.contains(ColliderChanges::MODIFIED) {
modified_colliders.push(*handle);
}
modified_colliders.push_once(*handle, co);
if rb.enabled && co.flags.enabled == ColliderEnabled::DisabledByParent {
co.flags.enabled = ColliderEnabled::Enabled;
@@ -176,7 +171,7 @@ pub(crate) fn handle_user_changes_to_rigid_bodies(
co.flags.enabled = ColliderEnabled::DisabledByParent;
}
co.changes |= ColliderChanges::MODIFIED | ColliderChanges::ENABLED_OR_DISABLED;
co.changes |= ColliderChanges::ENABLED_OR_DISABLED;
}
// Propagate the rigid-bodys enabled/disable status to its attached impulse joints.