From 4ca32a9546beca788104041134f0afbe96c5e871 Mon Sep 17 00:00:00 2001 From: Crozet Sébastien Date: Tue, 23 Feb 2021 15:43:43 +0100 Subject: Add one-way platform + conveyor belt demos. --- examples2d/one_way_platforms2.rs | 143 ++++++++++++++++++++++++++++++++++++++ examples3d/one_way_platforms3.rs | 143 ++++++++++++++++++++++++++++++++++++++ src/geometry/pair_filter.rs | 138 ------------------------------------ src/pipeline/physics_hooks.rs | 146 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 432 insertions(+), 138 deletions(-) create mode 100644 examples2d/one_way_platforms2.rs create mode 100644 examples3d/one_way_platforms3.rs delete mode 100644 src/geometry/pair_filter.rs create mode 100644 src/pipeline/physics_hooks.rs diff --git a/examples2d/one_way_platforms2.rs b/examples2d/one_way_platforms2.rs new file mode 100644 index 0000000..c7f58e8 --- /dev/null +++ b/examples2d/one_way_platforms2.rs @@ -0,0 +1,143 @@ +use na::{Point2, Vector2}; +use rapier2d::dynamics::{JointSet, RigidBodyBuilder, RigidBodySet}; +use rapier2d::geometry::{ColliderBuilder, ColliderHandle, ColliderSet}; +use rapier2d::pipeline::{ContactModificationContext, PhysicsHooks, PhysicsHooksFlags}; +use rapier_testbed2d::Testbed; + +struct OneWayPlatformHook { + platform1: ColliderHandle, + platform2: ColliderHandle, +} + +impl PhysicsHooks for OneWayPlatformHook { + fn active_hooks(&self) -> PhysicsHooksFlags { + PhysicsHooksFlags::MODIFY_SOLVER_CONTACTS + } + + fn modify_solver_contacts(&self, context: &mut ContactModificationContext) { + // The allowed normal for the first platform is its local +y axis, and the + // allowed normal for the second platform is its local -y axis. + // + // Now we have to be careful because the `manifold.local_n1` normal points + // toward the outside of the shape of `context.co1`. So we need to flip the + // allowed normal direction if the platform is in `context.collider_handle2`. + // + // Therefore: + // - If context.collider_handle1 == self.platform1 then the allowed normal is +y. + // - If context.collider_handle2 == self.platform1 then the allowed normal is -y. + // - If context.collider_handle1 == self.platform2 then its allowed normal +y needs to be flipped to -y. + // - If context.collider_handle2 == self.platform2 then the allowed normal -y needs to be flipped to +y. + let mut allowed_local_n1 = Vector2::zeros(); + + if context.collider_handle1 == self.platform1 { + allowed_local_n1 = Vector2::y(); + } else if context.collider_handle2 == self.platform1 { + // Flip the allowed direction. + allowed_local_n1 = -Vector2::y(); + } + + if context.collider_handle1 == self.platform2 { + allowed_local_n1 = -Vector2::y(); + } else if context.collider_handle2 == self.platform2 { + // Flip the allowed direction. + allowed_local_n1 = -Vector2::y(); + } + + // Call the helper function that simulates one-way platforms. + context.update_as_oneway_platform(&allowed_local_n1, 0.1); + + // Set the surface velocity of the accepted contacts. + let surface_velocity = if context.collider_handle1 == self.platform1 + || context.collider_handle2 == self.platform2 + { + -12.0 + } else { + 12.0 + }; + + for contact in context.solver_contacts.iter_mut() { + contact.surface_velocity.x = surface_velocity; + } + } +} + +pub fn init_world(testbed: &mut Testbed) { + /* + * World + */ + let mut bodies = RigidBodySet::new(); + let mut colliders = ColliderSet::new(); + let joints = JointSet::new(); + + /* + * Ground + */ + let rigid_body = RigidBodyBuilder::new_static().build(); + let handle = bodies.insert(rigid_body); + + let collider = ColliderBuilder::cuboid(25.0, 0.5) + .translation(30.0, 2.0) + .modify_contacts(true) + .build(); + let platform1 = colliders.insert(collider, handle, &mut bodies); + let collider = ColliderBuilder::cuboid(25.0, 0.5) + .translation(-30.0, -2.0) + .modify_contacts(true) + .build(); + let platform2 = colliders.insert(collider, handle, &mut bodies); + + /* + * Setup the one-way platform hook. + */ + let physics_hooks = OneWayPlatformHook { + platform1, + platform2, + }; + + /* + * Spawn cubes at regular intervals and apply a custom gravity + * depending on their position. + */ + testbed.add_callback(move |mut window, mut graphics, physics, _, run_state| { + if run_state.timestep_id % 50 == 0 && physics.bodies.len() <= 7 { + // Spawn a new cube. + let collider = ColliderBuilder::cuboid(1.5, 2.0).build(); + let body = RigidBodyBuilder::new_dynamic() + .translation(20.0, 10.0) + .build(); + let handle = physics.bodies.insert(body); + physics + .colliders + .insert(collider, handle, &mut physics.bodies); + + if let (Some(graphics), Some(window)) = (&mut graphics, &mut window) { + graphics.add(window, handle, &physics.bodies, &physics.colliders); + } + } + + physics.bodies.foreach_active_dynamic_body_mut(|_, body| { + if body.position().translation.y > 1.0 { + body.set_gravity_scale(1.0, false); + } else if body.position().translation.y < -1.0 { + body.set_gravity_scale(-1.0, false); + } + }); + }); + + /* + * Set up the testbed. + */ + testbed.set_world_with_params( + bodies, + colliders, + joints, + Vector2::new(0.0, -9.81), + physics_hooks, + ); + testbed.look_at(Point2::new(0.0, 0.0), 20.0); +} + +fn main() { + let testbed = Testbed::from_builders(0, vec![("Boxes", init_world)]); + testbed.run() +} diff --git a/examples3d/one_way_platforms3.rs b/examples3d/one_way_platforms3.rs new file mode 100644 index 0000000..b3182a4 --- /dev/null +++ b/examples3d/one_way_platforms3.rs @@ -0,0 +1,143 @@ +use na::{Point3, Vector3}; +use rapier3d::dynamics::{JointSet, RigidBodyBuilder, RigidBodySet}; +use rapier3d::geometry::{ColliderBuilder, ColliderHandle, ColliderSet}; +use rapier3d::pipeline::{ContactModificationContext, PhysicsHooks, PhysicsHooksFlags}; +use rapier_testbed3d::Testbed; + +struct OneWayPlatformHook { + platform1: ColliderHandle, + platform2: ColliderHandle, +} + +impl PhysicsHooks for OneWayPlatformHook { + fn active_hooks(&self) -> PhysicsHooksFlags { + PhysicsHooksFlags::MODIFY_SOLVER_CONTACTS + } + + fn modify_solver_contacts(&self, context: &mut ContactModificationContext) { + // The allowed normal for the first platform is its local +y axis, and the + // allowed normal for the second platform is its local -y axis. + // + // Now we have to be careful because the `manifold.local_n1` normal points + // toward the outside of the shape of `context.co1`. So we need to flip the + // allowed normal direction if the platform is in `context.collider_handle2`. + // + // Therefore: + // - If context.collider_handle1 == self.platform1 then the allowed normal is +y. + // - If context.collider_handle2 == self.platform1 then the allowed normal is -y. + // - If context.collider_handle1 == self.platform2 then its allowed normal +y needs to be flipped to -y. + // - If context.collider_handle2 == self.platform2 then the allowed normal -y needs to be flipped to +y. + let mut allowed_local_n1 = Vector3::zeros(); + + if context.collider_handle1 == self.platform1 { + allowed_local_n1 = Vector3::y(); + } else if context.collider_handle2 == self.platform1 { + // Flip the allowed direction. + allowed_local_n1 = -Vector3::y(); + } + + if context.collider_handle1 == self.platform2 { + allowed_local_n1 = -Vector3::y(); + } else if context.collider_handle2 == self.platform2 { + // Flip the allowed direction. + allowed_local_n1 = -Vector3::y(); + } + + // Call the helper function that simulates one-way platforms. + context.update_as_oneway_platform(&allowed_local_n1, 0.1); + + // Set the surface velocity of the accepted contacts. + let surface_velocity = if context.collider_handle1 == self.platform1 + || context.collider_handle2 == self.platform2 + { + -12.0 + } else { + 12.0 + }; + + for contact in context.solver_contacts.iter_mut() { + contact.surface_velocity.z = surface_velocity; + } + } +} + +pub fn init_world(testbed: &mut Testbed) { + /* + * World + */ + let mut bodies = RigidBodySet::new(); + let mut colliders = ColliderSet::new(); + let joints = JointSet::new(); + + /* + * Ground + */ + let rigid_body = RigidBodyBuilder::new_static().build(); + let handle = bodies.insert(rigid_body); + + let collider = ColliderBuilder::cuboid(9.0, 0.5, 25.0) + .translation(0.0, 2.0, 30.0) + .modify_contacts(true) + .build(); + let platform1 = colliders.insert(collider, handle, &mut bodies); + let collider = ColliderBuilder::cuboid(9.0, 0.5, 25.0) + .translation(0.0, -2.0, -30.0) + .modify_contacts(true) + .build(); + let platform2 = colliders.insert(collider, handle, &mut bodies); + + /* + * Setup the one-way platform hook. + */ + let physics_hooks = OneWayPlatformHook { + platform1, + platform2, + }; + + /* + * Spawn cubes at regular intervals and apply a custom gravity + * depending on their position. + */ + testbed.add_callback(move |mut window, mut graphics, physics, _, run_state| { + if run_state.timestep_id % 50 == 0 && physics.bodies.len() <= 7 { + // Spawn a new cube. + let collider = ColliderBuilder::cuboid(1.0, 2.0, 1.5).build(); + let body = RigidBodyBuilder::new_dynamic() + .translation(0.0, 6.0, 20.0) + .build(); + let handle = physics.bodies.insert(body); + physics + .colliders + .insert(collider, handle, &mut physics.bodies); + + if let (Some(graphics), Some(window)) = (&mut graphics, &mut window) { + graphics.add(window, handle, &physics.bodies, &physics.colliders); + } + } + + physics.bodies.foreach_active_dynamic_body_mut(|_, body| { + if body.position().translation.y > 1.0 { + body.set_gravity_scale(1.0, false); + } else if body.position().translation.y < -1.0 { + body.set_gravity_scale(-1.0, false); + } + }); + }); + + /* + * Set up the testbed. + */ + testbed.set_world_with_params( + bodies, + colliders, + joints, + Vector3::new(0.0, -9.81, 0.0), + physics_hooks, + ); + testbed.look_at(Point3::new(-100.0, 0.0, 0.0), Point3::origin()); +} + +fn main() { + let testbed = Testbed::from_builders(0, vec![("Boxes", init_world)]); + testbed.run() +} diff --git a/src/geometry/pair_filter.rs b/src/geometry/pair_filter.rs deleted file mode 100644 index a30145e..0000000 --- a/src/geometry/pair_filter.rs +++ /dev/null @@ -1,138 +0,0 @@ -use crate::dynamics::RigidBody; -use crate::geometry::{Collider, ColliderHandle, ContactManifold, SolverContact, SolverFlags}; - -/// Context given to custom collision filters to filter-out collisions. -pub struct PairFilterContext<'a> { - /// The first collider involved in the potential collision. - pub rigid_body1: &'a RigidBody, - /// The first collider involved in the potential collision. - pub rigid_body2: &'a RigidBody, - /// The first collider involved in the potential collision. - pub collider_handle1: ColliderHandle, - /// The first collider involved in the potential collision. - pub collider_handle2: ColliderHandle, - /// The first collider involved in the potential collision. - pub collider1: &'a Collider, - /// The first collider involved in the potential collision. - pub collider2: &'a Collider, -} - -pub struct ContactModificationContext<'a> { - /// The first collider involved in the potential collision. - pub rigid_body1: &'a RigidBody, - /// The first collider involved in the potential collision. - pub rigid_body2: &'a RigidBody, - /// The first collider involved in the potential collision. - pub collider_handle1: ColliderHandle, - /// The first collider involved in the potential collision. - pub collider_handle2: ColliderHandle, - /// The first collider involved in the potential collision. - pub collider1: &'a Collider, - /// The first collider involved in the potential collision. - pub collider2: &'a Collider, - /// The contact manifold. - pub manifold: &'a ContactManifold, - /// The solver contacts that can be modified. - pub solver_contacts: &'a mut Vec, - /// User-defined data attached to the manifold. - // NOTE: we keep this a &'a mut u32 to emphasize the - // fact that this can be modified. - pub user_data: &'a mut u32, -} - -bitflags::bitflags! { - #[cfg_attr(feature = "serde-serialize", derive(Serialize, Deserialize))] - /// Flags affecting the behavior of the constraints solver for a given contact manifold. - pub struct PhysicsHooksFlags: u32 { - /// If set, Rapier will call `PhysicsHooks::filter_contact_pair` whenever relevant. - const FILTER_CONTACT_PAIR = 0b0001; - /// If set, Rapier will call `PhysicsHooks::filter_intersection_pair` whenever relevant. - const FILTER_INTERSECTION_PAIR = 0b0010; - /// If set, Rapier will call `PhysicsHooks::modify_solver_contact` whenever relevant. - const MODIFY_SOLVER_CONTACTS = 0b0100; - } -} - -/// User-defined functions called by the physics engines during one timestep in order to customize its behavior. -pub trait PhysicsHooks: Send + Sync { - /// The sets of hooks that must be taken into account. - fn active_hooks(&self) -> PhysicsHooksFlags; - - /// Applies the contact pair filter. - /// - /// User-defined filter for potential contact pairs detected by the broad-phase. - /// This can be used to apply custom logic in order to decide whether two colliders - /// should have their contact computed by the narrow-phase, and if these contact - /// should be solved by the constraints solver - /// - /// Note that using a contact pair filter will replace the default contact filtering - /// which consists of preventing contact computation between two non-dynamic bodies. - /// - /// This filtering method is called after taking into account the colliders collision groups. - /// - /// If this returns `None`, then the narrow-phase will ignore this contact pair and - /// not compute any contact manifolds for it. - /// If this returns `Some`, then the narrow-phase will compute contact manifolds for - /// this pair of colliders, and configure them with the returned solver flags. For - /// example, if this returns `Some(SolverFlags::COMPUTE_IMPULSES)` then the contacts - /// will be taken into account by the constraints solver. If this returns - /// `Some(SolverFlags::empty())` then the constraints solver will ignore these - /// contacts. - fn filter_contact_pair(&self, context: &PairFilterContext) -> Option; - - /// Applies the intersection pair filter. - /// - /// User-defined filter for potential intersection pairs detected by the broad-phase. - /// - /// This can be used to apply custom logic in order to decide whether two colliders - /// should have their intersection computed by the narrow-phase. - /// - /// Note that using an intersection pair filter will replace the default intersection filtering - /// which consists of preventing intersection computation between two non-dynamic bodies. - /// - /// This filtering method is called after taking into account the colliders collision groups. - /// - /// If this returns `false`, then the narrow-phase will ignore this pair and - /// not compute any intersection information for it. - /// If this return `true` then the narrow-phase will compute intersection - /// information for this pair. - fn filter_intersection_pair(&self, context: &PairFilterContext) -> bool; - - /// Modifies the set of contacts seen by the constraints solver. - /// - /// By default, the content of `solver_contacts` is computed from `manifold.points`. - /// This method will be called on each contact manifold which have the flag `SolverFlags::MODIFY_CONTACTS` set. - /// This method can be used to modify the set of solver contacts seen by the constraints solver: contacts - /// can be removed and modified. - /// - /// Note that if all the contacts have to be ignored by the constraint solver, you may simply - /// do `context.solver_contacts.clear()`. - /// - /// Modifying the solver contacts allow you to achieve various effects, including: - /// - Simulating conveyor belts by setting the `surface_velocity` of a solver contact. - /// - Simulating shapes with multiply materials by modifying the friction and restitution - /// coefficient depending of the features in contacts. - /// - Simulating one-way platforms depending on the contact normal. - /// - /// Each contact manifold is given a `u32` user-defined data that is persistent between - /// timesteps (as long as the contact manifold exists). This user-defined data is initialized - /// as 0 and can be modified in `context.user_data`. - fn modify_solver_contacts(&self, context: &mut ContactModificationContext); -} - -impl PhysicsHooks for () { - /// The sets of hooks that must be taken into account. - fn active_hooks(&self) -> PhysicsHooksFlags { - PhysicsHooksFlags::empty() - } - - fn filter_contact_pair(&self, _: &PairFilterContext) -> Option { - None - } - - fn filter_intersection_pair(&self, _: &PairFilterContext) -> bool { - false - } - - fn modify_solver_contacts(&self, _: &mut ContactModificationContext) {} -} diff --git a/src/pipeline/physics_hooks.rs b/src/pipeline/physics_hooks.rs new file mode 100644 index 0000000..12b658f --- /dev/null +++ b/src/pipeline/physics_hooks.rs @@ -0,0 +1,146 @@ +use crate::dynamics::RigidBody; +use crate::geometry::{Collider, ColliderHandle, ContactManifold, SolverContact, SolverFlags}; + +/// Context given to custom collision filters to filter-out collisions. +pub struct PairFilterContext<'a> { + /// The first collider involved in the potential collision. + pub rigid_body1: &'a RigidBody, + /// The first collider involved in the potential collision. + pub rigid_body2: &'a RigidBody, + /// The first collider involved in the potential collision. + pub collider_handle1: ColliderHandle, + /// The first collider involved in the potential collision. + pub collider_handle2: ColliderHandle, + /// The first collider involved in the potential collision. + pub collider1: &'a Collider, + /// The first collider involved in the potential collision. + pub collider2: &'a Collider, +} + +pub struct ContactModificationContext<'a> { + /// The first collider involved in the potential collision. + pub rigid_body1: &'a RigidBody, + /// The first collider involved in the potential collision. + pub rigid_body2: &'a RigidBody, + /// The first collider involved in the potential collision. + pub collider_handle1: ColliderHandle, + /// The first collider involved in the potential collision. + pub collider_handle2: ColliderHandle, + /// The first collider involved in the potential collision. + pub collider1: &'a Collider, + /// The first collider involved in the potential collision. + pub collider2: &'a Collider, + /// The contact manifold. + pub manifold: &'a ContactManifold, + /// The solver contacts that can be modified. + pub solver_contacts: &'a mut Vec, + /// User-defined data attached to the manifold. + // NOTE: we keep this a &'a mut u32 to emphasize the + // fact that this can be modified. + pub user_data: &'a mut u32, +} + +bitflags::bitflags! { + #[cfg_attr(feature = "serde-serialize", derive(Serialize, Deserialize))] + /// Flags affecting the behavior of the constraints solver for a given contact manifold. + pub struct PhysicsHooksFlags: u32 { + /// If set, Rapier will call `PhysicsHooks::filter_contact_pair` whenever relevant. + const FILTER_CONTACT_PAIR = 0b0001; + /// If set, Rapier will call `PhysicsHooks::filter_intersection_pair` whenever relevant. + const FILTER_INTERSECTION_PAIR = 0b0010; + /// If set, Rapier will call `PhysicsHooks::modify_solver_contact` whenever relevant. + const MODIFY_SOLVER_CONTACTS = 0b0100; + } +} + +/// User-defined functions called by the physics engines during one timestep in order to customize its behavior. +pub trait PhysicsHooks: Send + Sync { + /// The sets of hooks that must be taken into account. + fn active_hooks(&self) -> PhysicsHooksFlags; + + /// Applies the contact pair filter. + /// + /// Note that this method will only be called if `self.active_hooks()` + /// contains the `PhysicsHooksFlags::FILTER_CONTACT_PAIR` flags. + /// + /// User-defined filter for potential contact pairs detected by the broad-phase. + /// This can be used to apply custom logic in order to decide whether two colliders + /// should have their contact computed by the narrow-phase, and if these contact + /// should be solved by the constraints solver + /// + /// Note that using a contact pair filter will replace the default contact filtering + /// which consists of preventing contact computation between two non-dynamic bodies. + /// + /// This filtering method is called after taking into account the colliders collision groups. + /// + /// If this returns `None`, then the narrow-phase will ignore this contact pair and + /// not compute any contact manifolds for it. + /// If this returns `Some`, then the narrow-phase will compute contact manifolds for + /// this pair of colliders, and configure them with the returned solver flags. For + /// example, if this returns `Some(SolverFlags::COMPUTE_IMPULSES)` then the contacts + /// will be taken into account by the constraints solver. If this returns + /// `Some(SolverFlags::empty())` then the constraints solver will ignore these + /// contacts. + fn filter_contact_pair(&self, context: &PairFilterContext) -> Option; + + /// Applies the intersection pair filter. + /// + /// Note that this method will only be called if `self.active_hooks()` + /// contains the `PhysicsHooksFlags::FILTER_INTERSECTION_PAIR` flags. + /// + /// User-defined filter for potential intersection pairs detected by the broad-phase. + /// + /// This can be used to apply custom logic in order to decide whether two colliders + /// should have their intersection computed by the narrow-phase. + /// + /// Note that using an intersection pair filter will replace the default intersection filtering + /// which consists of preventing intersection computation between two non-dynamic bodies. + /// + /// This filtering method is called after taking into account the colliders collision groups. + /// + /// If this returns `false`, then the narrow-phase will ignore this pair and + /// not compute any intersection information for it. + /// If this return `true` then the narrow-phase will compute intersection + /// information for this pair. + fn filter_intersection_pair(&self, context: &PairFilterContext) -> bool; + + /// Modifies the set of contacts seen by the constraints solver. + /// + /// Note that this method will only be called if `self.active_hooks()` + /// contains the `PhysicsHooksFlags::MODIFY_SOLVER_CONTACTS` flags. + /// + /// By default, the content of `solver_contacts` is computed from `manifold.points`. + /// This method will be called on each contact manifold which have the flag `SolverFlags::MODIFY_CONTACTS` set. + /// This method can be used to modify the set of solver contacts seen by the constraints solver: contacts + /// can be removed and modified. + /// + /// Note that if all the contacts have to be ignored by the constraint solver, you may simply + /// do `context.solver_contacts.clear()`. + /// + /// Modifying the solver contacts allow you to achieve various effects, including: + /// - Simulating conveyor belts by setting the `surface_velocity` of a solver contact. + /// - Simulating shapes with multiply materials by modifying the friction and restitution + /// coefficient depending of the features in contacts. + /// - Simulating one-way platforms depending on the contact normal. + /// + /// Each contact manifold is given a `u32` user-defined data that is persistent between + /// timesteps (as long as the contact manifold exists). This user-defined data is initialized + /// as 0 and can be modified in `context.user_data`. + fn modify_solver_contacts(&self, context: &mut ContactModificationContext); +} + +impl PhysicsHooks for () { + fn active_hooks(&self) -> PhysicsHooksFlags { + PhysicsHooksFlags::empty() + } + + fn filter_contact_pair(&self, _: &PairFilterContext) -> Option { + None + } + + fn filter_intersection_pair(&self, _: &PairFilterContext) -> bool { + false + } + + fn modify_solver_contacts(&self, _: &mut ContactModificationContext) {} +} -- cgit