Skip to content

Commit

Permalink
Add ColliderBackendPlugin and support generic colliders (#311)
Browse files Browse the repository at this point in the history
# 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::<CircleCollider>::default(),
    NarrowPhasePlugin::<CircleCollider>::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::<Collider>` if you add it manually
- Replace `Collider::compute_aabb` with `Collider::aabb`
  • Loading branch information
Jondolf authored Feb 17, 2024
1 parent 715b5b0 commit ffc0006
Show file tree
Hide file tree
Showing 17 changed files with 1,550 additions and 984 deletions.
4 changes: 4 additions & 0 deletions crates/bevy_xpbd_2d/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
Expand Down
228 changes: 228 additions & 0 deletions crates/bevy_xpbd_2d/examples/custom_collider.rs
Original file line number Diff line number Diff line change
@@ -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::<CircleCollider>::default(),
// Enable collision detection for our custom collider.
NarrowPhasePlugin::<CircleCollider>::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<Rotation>) -> 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<Rotation>,
position2: Vector,
rotation2: impl Into<Rotation>,
prediction_distance: Scalar,
) -> Vec<ContactManifold> {
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<Assets<ColorMaterial>>,
mut meshes: ResMut<Assets<Mesh>>,
) {
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<CenterBody>>,
time: Res<Time>,
) {
let delta_seconds = time.delta_seconds_f64().adjust_precision();
for (transform, mut lin_vel) in &mut particles {
let pos_delta = transform.translation.truncate().adjust_precision();
let dir = -pos_delta.normalize_or_zero();
lin_vel.0 += 800.0 * delta_seconds * dir;
}
}

/// Rotates the center body periodically clockwise and counterclockwise.
fn rotate(mut query: Query<&mut AngularVelocity, With<CenterBody>>, time: Res<Time>) {
let sin = 3.0 * time.elapsed_seconds_f64().adjust_precision().sin();
for mut ang_vel in &mut query {
ang_vel.0 = sin;
}
}
74 changes: 9 additions & 65 deletions src/components/mass_properties.rs
Original file line number Diff line number Diff line change
Expand Up @@ -309,7 +309,7 @@ impl Default for ColliderDensity {
///
/// fn print_collider_masses(query: Query<&ColliderMassProperties>) {
/// for mass_props in &query {
/// println!("{}", mass_props.mass());
/// println!("{}", mass_props.mass.0);
/// }
/// }
/// ```
Expand All @@ -318,15 +318,15 @@ impl Default for ColliderDensity {
#[reflect(Component)]
pub struct ColliderMassProperties {
/// Mass given by collider.
pub(crate) mass: Mass,
pub mass: Mass,
/// Inverse mass given by collider.
pub(crate) inverse_mass: InverseMass,
pub inverse_mass: InverseMass,
/// Inertia given by collider.
pub(crate) inertia: Inertia,
pub inertia: Inertia,
/// Inverse inertia given by collider.
pub(crate) inverse_inertia: InverseInertia,
pub inverse_inertia: InverseInertia,
/// Local center of mass given by collider.
pub(crate) center_of_mass: CenterOfMass,
pub center_of_mass: CenterOfMass,
}

impl ColliderMassProperties {
Expand All @@ -339,68 +339,12 @@ impl ColliderMassProperties {
center_of_mass: CenterOfMass::ZERO,
};

/// Computes mass properties from a given [`Collider`] and density.
/// Computes mass properties from a given collider and density.
///
/// Because [`ColliderMassProperties`] is read-only, adding this as a component manually
/// has no effect. The mass properties will be recomputed using the [`ColliderDensity`].
pub fn new(collider: &Collider, density: Scalar) -> Self {
let props = collider.shape_scaled().mass_properties(density);

Self {
mass: Mass(props.mass()),
inverse_mass: InverseMass(props.inv_mass),

#[cfg(feature = "2d")]
inertia: Inertia(props.principal_inertia()),
#[cfg(feature = "3d")]
inertia: Inertia(props.reconstruct_inertia_matrix().into()),

#[cfg(feature = "2d")]
inverse_inertia: InverseInertia(1.0 / props.principal_inertia()),
#[cfg(feature = "3d")]
inverse_inertia: InverseInertia(props.reconstruct_inverse_inertia_matrix().into()),

center_of_mass: CenterOfMass(props.local_com.into()),
}
}

/// Get the [mass](Mass) of the [`Collider`].
pub fn mass(&self) -> Scalar {
self.mass.0
}

/// Get the [inverse mass](InverseMass) of the [`Collider`].
pub fn inverse_mass(&self) -> Scalar {
self.inverse_mass.0
}

/// Get the [inerta](Inertia) of the [`Collider`].
#[cfg(feature = "2d")]
pub fn inertia(&self) -> Scalar {
self.inertia.0
}

/// Get the [inertia tensor](InverseInertia) of the [`Collider`].
#[cfg(feature = "3d")]
pub fn inertia(&self) -> Matrix3 {
self.inertia.0
}

/// Get the [inverse inertia](InverseInertia) of the [`Collider`].
#[cfg(feature = "2d")]
pub fn inverse_inertia(&self) -> Scalar {
self.inverse_inertia.0
}

/// Get the [inverse inertia](InverseInertia) of the [`Collider`].
#[cfg(feature = "3d")]
pub fn inverse_inertia(&self) -> Matrix3 {
self.inverse_inertia.0
}

/// Get the [local center of mass](CenterOfMass) of the [`Collider`].
pub fn center_of_mass(&self) -> Vector {
self.center_of_mass.0
pub fn new<C: AnyCollider>(collider: &C, density: Scalar) -> Self {
collider.mass_properties(density)
}
}

Expand Down
4 changes: 1 addition & 3 deletions src/components/mod.rs
Original file line number Diff line number Diff line change
@@ -1,14 +1,12 @@
//! Components used for rigid bodies, colliders and mass properties.
//! Commonly used components.
mod collider;
mod forces;
mod layers;
mod locked_axes;
mod mass_properties;
mod rotation;
mod world_queries;

pub use collider::*;
pub use forces::*;
pub use layers::*;
pub use locked_axes::*;
Expand Down
2 changes: 1 addition & 1 deletion src/components/rotation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@ impl Rotation {
self.as_radians().to_degrees()
}

/// Rotates the rotation by a given vector.
/// Rotates the given vector by `self`.
pub fn rotate(&self, vec: Vector) -> Vector {
Vector::new(
vec.x * self.cos() - vec.y * self.sin(),
Expand Down
2 changes: 1 addition & 1 deletion src/components/world_queries.rs
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ impl<'w> RigidBodyQueryItem<'w> {

#[derive(WorldQuery)]
#[world_query(mutable)]
pub(crate) struct MassPropertiesQuery {
pub struct MassPropertiesQuery {
pub mass: &'static mut Mass,
pub inverse_mass: &'static mut InverseMass,
pub inertia: &'static mut Inertia,
Expand Down
2 changes: 1 addition & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -536,7 +536,7 @@ pub mod prelude {
narrow_phase::NarrowPhaseConfig,
*,
},
prepare::*,
prepare::{init_transforms, update_mass_properties, PrepareConfig, PreparePlugin},
setup::*,
solver::solve_constraint,
spatial_query::*,
Expand Down
Loading

0 comments on commit ffc0006

Please sign in to comment.