Bevy XPBD is an ECS-based 2D and 3D physics engine for the Bevy game engine. You can check out the GitHub repository and the introductory post for more details.

Note: Version 0.3 didn’t get its own blog post, but you can find the release notes and changelog here.

Bevy XPBD 0.4 has now been released on crates.io, featuring several new features, bug fixes, and quality of life improvements. Here are some highlights:

The migration guide and a more complete changelog can be found on GitHub.

Generic Colliders

In previous versions, collider-specific backend logic was split across several plugins. The PreparePlugin initialized colliders and managed mass properties and collider parents, the SyncPlugin handled collider scale and transform propagation, and the BroadPhasePlugin updated their AABBs. This coupled “collider logic” with “rigid body logic” and made it very difficult to implement custom collision backends without having to rewrite a large chunk of the engine.

Bevy XPBD 0.4 moves this logic into a generic ColliderBackendPlugin. It handles all of the collider setup and updates for any type that implements the AnyCollider and (for now) ScalableCollider traits. The narrow phase has also been refactored to be generic over the collider type.

It is now very straightforward to implement completely custom collider backends. After implementing the traits, you simply need to add the ColliderBackendPlugin and NarrowPhasePlugin:

app.add_plugins((
    DefaultPlugins,
    PhysicsPlugins::default(),
    ColliderBackendPlugin::<MyCollider>::default(),
    NarrowPhasePlugin::<MyCollider>::default(),
));

This will also make experimentation with changing the built-in collision backend from Parry to something else much easier. Note that spatial queries are still reliant on Collider however.

Below is the video you saw as the cover of this post. It is the custom_collider example, using only a custom-made CircleCollider and zero Parry!

Circle colliders colliding with a rotating object

Some new sets like the BroadPhaseSet, NarrowPhaseSet, and SyncSet system sets have also been introduced as a part of the refactor to make internal system ordering cleaner.

Parry and Nalgebra Are Optional

Bevy XPBD currently uses Parry for collision detection. It uses Nalgebra for math unlike Bevy which uses Glam. For an official physics integration some day in the future, Bevy doesn’t want duplicate math libraries in its dependency tree.

Generic colliders paved the way for collider agnosticism, but it doesn’t get rid of Parry or Nalgebra if Collider still exists. To address this, Bevy XPBD 0.4 adds three new feature flags: default-collider, parry-f32, and parry-f64, for the f32 and f64 versions of Parry respectively.

By default, the f32 version of Collider is enabled, but by disabling default features, you can get rid of Parry and Nalgebra completely.

[dependencies]
# This has no default `Collider`, Parry, or Nalgebra
bevy_xpbd_3d = { version = "0.4", default-features = false, features = ["3d", "f32"] }

This helps reduce binary size and complexity for projects that don’t need collision detection or have a custom collision backend.

Access Contact Impulses

Having access to contact impulses can be very useful for several tasks, like implementing destructable objects or determining how much damage a hit should deal. This is now exposed in ContactData accessed from Collision events or the Collisions resource.

Impulses are stored instead of forces for a few reasons:

Computing the corresponding force is simple however, as you just need to divide by the (substep) delta time. For this, the contact types also have helpers like normal_force and tangent_force.

Note that impulses in Collision events are currently only from the last substep.

Debug Render Contacts

Previously, only contact points could be debug rendered. Now, it is also supported for contact normals, which by default vary in length based on the impulse magnitude.

Debug rendering contacts

Layer Rework

Before Bevy XPBD 0.4, CollisionLayers had “groups” and “masks”. Groups indicated which layers a collider is a part of, and masks indicated which layers a collider can interact with.

However, both groups and masks are bitmasks, so the “masks” name is a bit ambiguous. In addition, “groups” and “layers” sound almost synonymous, but masks also use layers (a layer corresponds to one bit). CollisionLayers also had tons of API duplication with methods like add_group, add_groups, add_mask, add_masks, and so on.

Bevy XPBD 0.4 reduces this duplication and ambiguity by reworking layers in three major ways:

LayerMasks are versatile: you can create them from bitmasks, enums that implement PhysicsLayer, or even arrays of layers. CollisionLayers constructors now take impl Into<LayerMask>, so you can use whatever approach you prefer:

let layers1 = CollisionLayers::new(0b00010, 0b0111);
let layers2 = CollisionLayers::new(GameLayer::Player, [GameLayer::Enemy, GameLayer::Ground]);
let layers3 = CollisionLayers::new(LayerMask(0b0001), LayerMask::ALL);

Modifying layers is now done by modifying the memberships or filters directly:

layers.memberships.remove(GameLayer::Environment);
layers.filters.add([GameLayer::Environment, GameLayer::Tree]);

// Bitwise ops also work since we're accessing the bitmasks/layermasks directly.
layers.memberships |= GameLayer::Player; // You could also use a bitmask like 0b0010.

The methods mutate in-place, which fixes the footgun where the old methods like add_group looked like they were in-place when in reality they were not.

Bevy 0.13 Support

Bevy XPBD 0.4 supports the latest version of Bevy. 0.13 contains lots of additions that are highly useful for physics and collision detection, and several of these are already in use in Bevy XPBD.

Colliders from Primitives

Bevy 0.13 added support for first-party geometric primitives. Bevy XPBD 0.4 supports creating colliders from most of them using the new IntoCollider trait.

// Both work
let circle = Circle::new(0.5).collider();
let circle = Collider::from(Circle::new(0.5);

All primitives support collider creation, except for ConicalFrustum and Torus, which will get support in a future release. Some new collider shapes are also supported in 2D: Ellipse and RegularPolygon.

// Both work
let ellipse = Collider::ellipse(1.0, 0.5);
let ellipse = Collider::from(Ellipse::new(1.0, 0.5));

// Both work
let hexagon = Collider::regular_polygon(0.5, 6);
let hexagon = Collider::from(RegularPolygon::new(0.5, 6));

To align better with Bevy’s naming, the ball constructor is now circle in 2D and sphere in 3D, while cuboid is rectangle in 2D and remains as cuboid in 3D.

Direction Types for Spatial Queries

Bevy 0.13 added the Direction2d and Direction3d types to provide a type-level guarantee that unit vectors remain normalized. These are used in several Bevy APIs already, like the new Ray2d and Ray3d types.

Bevy XPBD 0.4 changes spatial query APIs to use the new direction types. This is more explicit, and it guarantees that the input and output are expected; previously, you could use unnormalized direction vectors, which were actually treated as a kind of velocity, affecting the time of impact. This could often be unexpected for users.

// Before
let caster = RayCaster::new(Vec3::ZERO, Vec3::X);

// After
let caster = RayCaster::new(Vec3::ZERO, Direction3d::X);

This applies to RayCaster, ShapeCaster, SpatialQuery methods like cast_ray, and many other methods that use directions.

You can also create RayCasters from Bevy’s new ray types:

// `RayCaster::from` also works
let caster = RayCaster::from_ray(Ray3d::new(Vec3::ZERO, Vec3::X));

In the future, we will most likely also start using Bevy’s Ray2d and Ray3d types in more APIs. It was left out for now, because it’d require some larger API changes and have several design questions, and it might not work as well for f64 precision.

PhysicsGizmos Gizmo Config Group

The PhysicsDebugConfig resource and PhysicsDebugRenderer system parameter have been removed in favor of the new PhysicsGizmos gizmo configuration group. With this change, the gizmo configuration of Bevy XPBD is finally separate from the global configuration, so third party plugins and custom configurations won’t unexpectedly impact physics debug rendering.

Before:

fn main() {
    App::new()
        .add_plugins((
            DefaultPlugins,
            PhysicsPlugins::default(),
            PhysicsDebugPlugin::default(),
        ))
        // Configure physics debug rendering
        .insert_resource(PhysicsDebugConfig {
            aabb_color: Some(Color::WHITE),
            ..default()
        })
        .run();
}

After:

fn main() {
    App::new()
        .add_plugins((
            DefaultPlugins,
            PhysicsPlugins::default(),
            PhysicsDebugPlugin::default(),
        ))
        // Configure physics debug rendering
        .insert_gizmo_group(
            PhysicsGizmos {
                aabb_color: Some(Color::WHITE),
                ..default()
            },
            GizmoConfig::default(),
        )
        .run();
}

What’s Next?

I have a lot of experiments and exciting plans for physics in Bevy. I recently gave a talk where I touched on the topic in the first ever (unofficial) Bevy game dev meetup; go check it out along with everyone else’s amazing talks!

(And excuse my poor microphone; this was also my first ever talk like this so I had zero experience)

Now, let’s dive into some of my experiments and plans in a bit more detail. I might end up writing dedicated blog posts for these later on if the results end up being interesting enough.

Glam-Based Collision Experiments

Currently, both bevy_rapier and bevy_xpbd use Parry for collision detection. As I briefly mentioned earlier, Parry uses Nalgebra instead of Glam, and Bevy doesn’t really want duplicate math libraries because it’d be confusing, add complexity, and increase binary size and compile times. Bevy wants Glam-based collision detection.

I see two paths for this:

  1. Fork Parry to make a version that supports Glam.
  2. Build a collision detection library from scratch.

These are quite massive endeavours, especially the latter one. However, I think they’re both valuable in their own ways, so I have started experimenting with both approaches.

My Parry fork is currently called Barry (Bevy + Parry), and I have already managed to completely replace Nalgebra with bevy_math, which uses Glam. The next step is to fix bugs introduced in the process and to continue making it feel more native by using Bevy’s built-in primitive shapes and bounding volumes.

Note that bevy_math does not depend on Bevy itself, so Barry would still be a general-purpose collision detection library that the whole Rust ecosystem could use, just like Parry. It’s very much a work in progress, but it looks promising!

My completely custom collision detection library on the other hand is called bevy_peck (because birds hit things with their beaks by pecking). It’s in extremely early stages, but I’m close to having working GJK and EPA implemented, which would provide the core foundation for collision detection.

Even if bevy_peck doesn’t become a complete and usable library, it is at least a great learning opportunity for me, and a way to freely experiment with different APIs and approaches. As I add more functionality like ray casting and point queries for primitive shapes, we could also consider upstreaming them into Bevy itself. Doing this piece by piece might be a better approach than eventually upstreaming an entire collision detection library all at once.

Solver Experiments

Extended Position-Based Dynamics, or XPBD for short, is a great simulation method with generally good stability. It has worked great for Bevy XPBD, but it does have its flaws.

Erin Catto, the author of the incredible Box2D physics engine and a legend in the physics simulation scene, recently made a project called Solver2D to compare different solvers and their unique characteristics. His video shows the results, and the article goes into more detail on the actual implementations.

Erin found that a solver that he calls “TGS Soft” consistently performs the best, and it is likely to be used for Box2D v3.0. TGS Soft is an impulse-based solver that uses XPBD-like substepping combined with soft constraints, which apply intuitive spring-like damping to constraint responses. This solver is also extremely close if not identical to the solver that Rapier recently switched to, with great success.

I have started experimenting with a TGS Soft solver of my own, and am quite close to having it working properly. I will experiment with it more and compare against XPBD myself, and if the results have TGS Soft as the winner, we might end up switching solvers. It would probably require a rebrand though :P

Ultimately, the ideal approach would be to be solver-agnostic so that you could simply switch the solver by changing a plugin. XPBD might also still be great for things like cloth and soft bodies, so it could be worth keeping it around even if we use another approach for contacts.

Performance Optimization

Let’s face it: Bevy XPBD is not performant enough. It can do quite well when entities are relatively sparse and not constantly colliding, but as soon as you have even a thousand bodies consistently in contact, the simulation can become sluggish.

There are currently several potential bottlenecks causing this. Here is a relatively comprehensive list, in no particular order in terms of significance:

This is a lot, but I believe most of the issues should definitely be resolvable.

Firstly, implementing a better broad phase and moving the narrow phase out of the substepping loop could give a very significant performance boost. Especially narrow phase collision detection can be very expensive, and running it at every substep is unreasonable.

One challenge with having the narrow phase outside of the substepping loop is that we could have more tunneling issues (fast-moving bodies go through objects), which is typically counteracted with Continuous Collision Detection. CCD is also another heavily requested feature, so it might be time to finally try and implement it.

Simulation islands could also provide a very meaningful performance improvement, especially for larger game worlds. I haven’t tried how this would work in an ECS context yet, but I imagine it’s possible, and it’s just something that we’ll have to try.

Conclusion

While we will still be adding new features like various improvements to joints and maybe more options for collision filtering, I believe it’s time to start focusing a bit more on improving the core foundational pieces of Bevy XPBD rather than continuing to pile on more and more features.

To summarize, we need:

In the short term, implementing CCD and attempting to move the narrow phase outside of the substepping loop will most likely be my top priorities, as they are somewhat related and could have a significant impact. In the background, I will also be slowly building Glam-based collision detection and experimenting with alternative solvers, primarily TGS soft. For the solver experiments, it will also be very important to develop better examples like the samples in Erin Catto’s Solver2D.

However, I am a human, and unfortunately have some responsibilities that I need to take care of first. Starting today, I am taking a step back from active development for about a month to focus on school. I’ll be back in full force at the end of March though, and from that point onwards, I should be free to work actively on Bevy and physics for quite a long time.

Huge thanks again to everyone for all of the support! Somehow, Bevy XPBD has reached 840 stars already, and even its #crate-help topic on the Bevy Discord has surpassed 10,000 message (a rather arbitrary milestone that somehow feels meaningful). These numbers are crazy to me considering we’re only at version 0.4.

For now, I’ll be focusing on studies for a bit, but I look forward to returning to all of these cool experiments and active maintenance as soon as I can.