From ffc00068b5ce90e75b55985e9b238813f7652d87 Mon Sep 17 00:00:00 2001 From: Joona Aalto Date: Sat, 17 Feb 2024 14:06:08 +0200 Subject: [PATCH] Add `ColliderBackendPlugin` and support generic colliders (#311) # Objective An improved version of #302. Currently, collider logic is split across several plugins. The `PreparePlugin` initializes them and manages mass properties and collider parents, the `SyncPlugin` handles collider scale and transform propagation, the `BroadPhasePlugin` updates their AABBs and collects potential collision pairs, and the `NarrowPhasePlugin` uses them for actually computing contacts. Using `Collider` in so many places isn't ideal since it couples collision logic with other plugins, and it also makes it very challenging to implement custom collision backends nicely. It would be useful to handle collider-specific background work in a single plugin and to support custom colliders through generics. ## Solution - Add a `ColliderBackendPlugin`. It handles collider initialization, transform propagation, scaling, parents, mass property updates, AABB updates, and more. - Add the `AnyCollider` and `ScalableCollider` traits. Implementing these for a component makes it possible to use a completely custom collider component by passing the type to `ColliderBackendPlugin` and `NarrowPhasePlugin` through generics. There is a new `custom_collider` example that demonstrates this. To avoid leaking too many implementation details related to system ordering, I also added more system sets like the `BroadPhaseSet`, `NarrowPhaseSet` and `SyncSet` enums. Creating a custom collision backend is now straightforward. After implementing `AnyCollider` and `ScalableCollider` for a component, simply add the `CollisionBackendPlugin` and `NarrowPhasePlugin` with that type: ```rust .add_plugins(( DefaultPlugins, PhysicsPlugins::default(), ColliderBackendPlugin::::default(), NarrowPhasePlugin::::default(), )) ``` --- ## Changelog ### Added - `CollisionBackendPlugin` - `AnyCollider` trait - `ScalableCollider` trait - `scale_by` method for colliders - `Collider::swept_aabb` - `ColliderAabb::new` - `ColliderAabb::merged` - `BroadPhaseSet` system sets - `NarrowPhaseSet` system sets - `SyncSet` system sets - `custom_collider` example ### Changed - Moved `collider` module from `components` to `collision` module - `NarrowPhasePlugin` takes a generic type implementing `AnyCollider` - Moved collider systems from `PreparePlugin`, `SyncPlugin` and `SleepingPlugin` to `ColliderBackendPlugin` - Collider logic from `upate_mass_properties` is in a new `update_collider_mass_properties` system ## Migration Guide - Replace `NarrowPhasePlugin` with `NarrowPhasePlugin::` if you add it manually - Replace `Collider::compute_aabb` with `Collider::aabb` --- crates/bevy_xpbd_2d/Cargo.toml | 4 + .../bevy_xpbd_2d/examples/custom_collider.rs | 228 +++++ src/components/mass_properties.rs | 74 +- src/components/mod.rs | 4 +- src/components/rotation.rs | 2 +- src/components/world_queries.rs | 2 +- src/lib.rs | 2 +- src/plugins/collision/broad_phase.rs | 149 +-- .../collision}/collider.rs | 174 +++- src/plugins/collision/collider_backend.rs | 884 ++++++++++++++++++ src/plugins/collision/mod.rs | 4 + src/plugins/collision/narrow_phase.rs | 132 ++- src/plugins/mod.rs | 11 +- src/plugins/prepare.rs | 479 ++-------- src/plugins/sleeping.rs | 37 +- src/plugins/spatial_query/mod.rs | 2 +- src/plugins/sync.rs | 346 ++----- 17 files changed, 1550 insertions(+), 984 deletions(-) create mode 100644 crates/bevy_xpbd_2d/examples/custom_collider.rs rename src/{components => plugins/collision}/collider.rs (91%) create mode 100644 src/plugins/collision/collider_backend.rs diff --git a/crates/bevy_xpbd_2d/Cargo.toml b/crates/bevy_xpbd_2d/Cargo.toml index 823523d5..6336d02f 100644 --- a/crates/bevy_xpbd_2d/Cargo.toml +++ b/crates/bevy_xpbd_2d/Cargo.toml @@ -70,6 +70,10 @@ required-features = ["2d"] name = "collision_layers" required-features = ["2d"] +[[example]] +name = "custom_collider" +required-features = ["2d"] + [[example]] name = "fixed_joint_2d" required-features = ["2d"] diff --git a/crates/bevy_xpbd_2d/examples/custom_collider.rs b/crates/bevy_xpbd_2d/examples/custom_collider.rs new file mode 100644 index 00000000..0f5f5f52 --- /dev/null +++ b/crates/bevy_xpbd_2d/examples/custom_collider.rs @@ -0,0 +1,228 @@ +//! An example demonstrating how to make a custom collider and use it for collision detection. + +use bevy::{prelude::*, sprite::MaterialMesh2dBundle}; +use bevy_xpbd_2d::{math::*, prelude::*, PhysicsSchedule, PhysicsStepSet}; +use examples_common_2d::XpbdExamplePlugin; + +fn main() { + App::new() + .add_plugins(( + DefaultPlugins, + XpbdExamplePlugin, + // Add collider backend for our custom collider. + // This handles things like initializing and updating required components + // and managing collider hierarchies. + ColliderBackendPlugin::::default(), + // Enable collision detection for our custom collider. + NarrowPhasePlugin::::default(), + )) + .insert_resource(ClearColor(Color::rgb(0.01, 0.01, 0.025))) + .insert_resource(Gravity::ZERO) + .add_systems(Startup, setup) + .add_systems( + PhysicsSchedule, + (center_gravity, rotate).before(PhysicsStepSet::BroadPhase), + ) + .run(); +} + +/// A basic collider with a circle shape. Only supports uniform scaling. +#[derive(Component)] +struct CircleCollider { + /// The radius of the circle collider. This may be scaled by the `Transform` scale. + radius: Scalar, + /// The radius of the circle collider without `Transform` scale applied. + unscaled_radius: Scalar, + /// The scaling factor, determined by `Transform` scale. + scale: Scalar, +} + +impl CircleCollider { + fn new(radius: Scalar) -> Self { + Self { + radius, + unscaled_radius: radius, + scale: 1.0, + } + } +} + +impl AnyCollider for CircleCollider { + fn aabb(&self, position: Vector, _rotation: impl Into) -> ColliderAabb { + ColliderAabb::new(position, Vector::splat(self.radius)) + } + + fn mass_properties(&self, density: Scalar) -> ColliderMassProperties { + // In 2D, the Z length is assumed to be 1.0, so volume = area + let volume = bevy_xpbd_2d::math::PI * self.radius.powi(2); + let mass = density * volume; + let inertia = self.radius.powi(2) / 2.0; + + ColliderMassProperties { + mass: Mass(mass), + inverse_mass: InverseMass(mass.recip()), + inertia: Inertia(inertia), + inverse_inertia: InverseInertia(inertia.recip()), + center_of_mass: CenterOfMass::default(), + } + } + + // This is the actual collision detection part. + // It compute all contacts between two colliders at the given positions. + fn contact_manifolds( + &self, + other: &Self, + position1: Vector, + rotation1: impl Into, + position2: Vector, + rotation2: impl Into, + prediction_distance: Scalar, + ) -> Vec { + let rotation1: Rotation = rotation1.into(); + let rotation2: Rotation = rotation2.into(); + + let inv_rotation1 = rotation1.inverse(); + let delta_pos = inv_rotation1.rotate(position2 - position1); + let delta_rot = inv_rotation1.mul(rotation2); + + let distance_squared = delta_pos.length_squared(); + let sum_radius = self.radius + other.radius; + + if distance_squared < (sum_radius + prediction_distance).powi(2) { + let normal1 = if distance_squared != 0.0 { + delta_pos.normalize_or_zero() + } else { + Vector::X + }; + let normal2 = delta_rot.inverse().rotate(-normal1); + let point1 = normal1 * self.radius; + let point2 = normal2 * other.radius; + + vec![ContactManifold { + index: 0, + normal1, + normal2, + contacts: vec![ContactData { + index: 0, + point1, + point2, + normal1, + normal2, + penetration: sum_radius - distance_squared.sqrt(), + // Impulses are computed by the constraint solver + normal_impulse: 0.0, + tangent_impulse: 0.0, + }], + }] + } else { + vec![] + } + } +} + +// Note: This circle collider only supports uniform scaling. +impl ScalableCollider for CircleCollider { + fn scale(&self) -> Vector { + Vector::splat(self.scale) + } + + fn set_scale(&mut self, scale: Vector, _detail: u32) { + // For non-unifprm scaling, this would need to be converted to an ellipse collider or a convex hull. + self.scale = scale.max_element(); + self.radius = self.unscaled_radius * scale.max_element(); + } +} + +/// A marker component for the rotating body at the center. +#[derive(Component)] +struct CenterBody; + +fn setup( + mut commands: Commands, + mut materials: ResMut>, + mut meshes: ResMut>, +) { + commands.spawn(Camera2dBundle::default()); + + let center_radius = 200.0; + let particle_radius = 5.0; + + let red = materials.add(Color::rgb(0.9, 0.3, 0.3).into()); + let blue = materials.add(Color::rgb(0.1, 0.6, 1.0).into()); + let particle_mesh = meshes.add(shape::Circle::new(particle_radius).into()); + + // Spawn rotating body at the center. + commands + .spawn(( + MaterialMesh2dBundle { + mesh: meshes.add(shape::Circle::new(center_radius).into()).into(), + material: materials.add(Color::rgb(0.7, 0.7, 0.8).into()).clone(), + ..default() + }, + RigidBody::Kinematic, + CircleCollider::new(center_radius.adjust_precision()), + CenterBody, + )) + .with_children(|c| { + // Spawn obstacles along the perimeter of the rotating body, like the teeth of a cog. + let count = 8; + let angle_step = std::f32::consts::TAU / count as f32; + for i in 0..count { + let pos = Quat::from_rotation_z(i as f32 * angle_step) * Vec3::Y * center_radius; + c.spawn(( + MaterialMesh2dBundle { + mesh: particle_mesh.clone().into(), + material: red.clone(), + transform: Transform::from_translation(pos).with_scale(Vec3::ONE * 5.0), + ..default() + }, + CircleCollider::new(particle_radius.adjust_precision()), + )); + } + }); + + let x_count = 10; + let y_count = 30; + + // Spawm grid of particles. These will be pulled towards the rotating body. + for x in -x_count / 2..x_count / 2 { + for y in -y_count / 2..y_count / 2 { + commands.spawn(( + MaterialMesh2dBundle { + mesh: particle_mesh.clone().into(), + material: blue.clone(), + transform: Transform::from_xyz( + x as f32 * 3.0 * particle_radius - 350.0, + y as f32 * 3.0 * particle_radius, + 0.0, + ), + ..default() + }, + RigidBody::Dynamic, + CircleCollider::new(particle_radius.adjust_precision()), + LinearDamping(0.4), + )); + } + } +} + +/// Pulls all particles towards the center. +fn center_gravity( + mut particles: Query<(&Transform, &mut LinearVelocity), Without>, + time: Res