I am happy to announce that Avian Physics 0.1 has been released!

Avian is an ECS-based 2D and 3D physics engine for Bevy, a refreshingly simple data-driven game engine built in Rust. Avian prioritizes ergonomics and modularity, with a focus on providing a native ECS-driven user experience.

It is the next evolution of my previous physics engine Bevy XPBD, with a completely rewritten contact solver, improved performance, a reworked structure, and numerous other improvements over its predecessor. This will be covered in detail later into the post.

Getting Started

If you are new to Rust or Bevy, consider first going through Bevy’s Quick Start guide.

Getting up to speed with Avian is very straightforward! First, add it as a dependency in Cargo.toml:

[dependencies]
avian2d = "0.1" # For 2D applications
avian3d = "0.1" # For 3D applications
bevy = "0.14"

Then, add PhysicsPlugins to your Bevy application:

use avian3d::prelude::*;
use bevy::prelude::*;

fn main() {
    App::new()
        .add_plugins((DefaultPlugins, PhysicsPlugins::default()))
        .run();
}

Now, you can use the numerous components and resources provided by Avian to add physics behavior to your entities. For example, turning an entity into a rigid body with a collider is as simple as adding the RigidBody and Collider components, and an initial velocity can be specified using LinearVelocity:

use avian3d::prelude::*;
use bevy::prelude::*;

fn setup(mut commands: Commands) {
    commands.spawn((
        RigidBody::Dynamic,
        Collider::sphere(0.5),
        LinearVelocity(Vec3::X),
    ));
}

To learn more, refer to the official documentation and check out the GitHub repository and the 2D and 3D examples.

Highlights

As stated earlier, Avian isn’t actually my first physics engine. Instead, it is the rebranded successor of my previous physics engine Bevy XPBD. The rest of this post will focus on the rebrand and what is new from Bevy XPBD 0.4.

Avian 0.1 has a ton of improvements, additions, and fixes over its predecessor. Highlights include:

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

This post is long, and there’s a lot of topics to cover. We will start by going over some background on what Bevy XPBD was and why a rebrand was deemed necessary, but if you want to skip over to the shiny new stuff instead, feel free to jump to the Solver Rework section.

Background on the Rebrand

Over two years ago, I started a project called Bevy XPBD, a physics engine based on Extended Position-Based Dynamics. I had been playing around with Bevy for a while and was interested in learning about physics simulation, so when I saw a tutorial series on XPBD physics in Bevy by Johan Helsing, I decided to follow along.

After finishing the tutorial, I continued slowly working on the project on the side and building out more functionality, until over a year later it was starting to get some recognition, including from Johan himself. I released Bevy XPBD 0.1 in June 2023, and from that point onwards, it has been my main project and area of interest.

As the project has grown, some things have become more evident to me:

  1. There is clearly high demand for a (native) physics integration for Bevy.
  2. There is interest in eventually adding official physics support to Bevy.
  3. Bevy XPBD in its old form would not have been fit for upstreaming.

Problems With Bevy XPBD

XPBD as a Simulation Method

From a simulation standpoint, our XPBD implementation has several issues:

Some of the problems could be issues with our implementation, but a lot of them are also inherent to the simulation method, at least when implemented as described in the original papers.

Collision and Dynamics

Aside from XPBD as a simulation method, Bevy XPBD also has several other large issues with collisions and rigid body dynamics:

Branding

I am not entirely content with Bevy XPBD in terms of branding either:

Patent?

Perhaps most importantly, from a legal standpoint, there were some potential issues with XPBD: it is technically patented by NVIDIA. This is something I only learned about quite late into the project.

As Bevy XPBD had already gained a lot of popularity in the ecosystem, Bevy’s maintainers reached out to share their concerns about the XPBD patent situation. This discussion convinced me to finally commit to the idea of rebranding the project and transitioning away from XPBD entirely. I announced the rebrand in the form of a rather long GitHub issue in early March this year.

It is important to note that soon after the announcement, we actually got a response from Miles Macklin, one of the inventors of XPBD, about whether there are issues with creating an open source game physics engine with XPBD:


I don’t believe there is any restriction here - you can already find many open source and commercial examples of XPBD implementations online.

This suggests that using XPBD should actually be fine. However, the wording is still slightly too vague to my liking, and of course we cannot be entirely certain, as NVIDIA still holds the patent. Either way, even for reasons unrelated to the patent, a rebrand and solver rework is warranted.

The new solver will be covered in detail in the Solver Rework section. Note that joints still use XPBD for now, as reimplementing them with another approach would’ve delayed the release by an unreasonable amount of time, but they may be reworked in the future as well.

Why Avian? 🪶

My criteria for the ideal name were:

I had dozens (or even hundreds!) of name ideas, most of which were already taken or only satisfied a few of these criteria. A bunch of community members also helped with great suggestions, thanks for all of them <3

After a long time trying to come up with a name, I landed on Avian, because it fits all of these criteria perfectly. It’s nice and short, not reserved by existing projects, relates to birds and flight, and overall just feels like a pretty natural name for a Bevy physics engine. A bevy is a flock of birds, and physics is what makes the bevy truly avian through the power of flight!

Why ditch the bevy_ prefix?
  • Maybe one day we find a way to use the engine without the Bevy core and the ECS?
  • A name like bevy_avian would sort of imply that it is a Bevy integration for an avian crate, not the core crate itself. Things could get confusing if someone else made an avian crate.
  • 3rd party crate names don’t have to be as long, and we could even have official sub-crates like avian_collision, avian_spatial, and avian_math (although in practice, these could just have their own names)
  • Several people stated they prefer names without the prefix. This is also not uncommon in the ecosystem: big-brain, big_space, lightyear, leafwing-input-manager (and other Leafwing crates) are all popular Bevy plugins with no bevy_ prefix.

After lots of iteration, design help from community members, and some finishing touches, we ended up with the following logo:

Avian banner logo

The logo went through several iterations. Click this open to see a few!

Here’s the initial mock-up design I made for the logo:

Logo iteration 1

Looks too much like a quill! What if we just… turn it upside down and refine it a little?

Logo iteration 2

That looks pretty nice! A bit bland though, so maybe we could just add some color and decorations?

Logo iteration 3

Hmm, the blue is nice but the wind doodles don’t really look like wind. Let’s try something different… What if we made the Bevy logo, but with feathers 🤔

Logo iteration 4

Interesting idea and looks like motion, but I’m not sold. Let’s return to the original idea.

Logo iteration 5

Now we’re getting somewhere! Pretty ripples masterfully drawn by @NiseVoid. With a bit more polish, we reach the final result:

Avian banner logo

Now that we’ve covered what Avian is and why the rebrand was done, let’s finally jump straight into all of the new shiny features and improvements!

Solver Rework #385

Author: @Jondolf (me)

The new contact solver uses TGS Soft, an impulse-based solver using substepping and soft constraints together with warm starting and relaxation.

Slow down, what do any of those words mean??
  • Impulse-based: Contacts, joints, and other interactions between bodies are handled by applying impulses that modify velocities. The old solver was position-based, and applied corrections by modifying positions directly.
  • Projected Gauss-Seidel (PGS): The classic iterative approach to solving constraints (contacts and joints) using the Gauss-Seidel numerical method. Reframed by Erin Catto as Sequential Impulses.
  • Temporal Gauss-Seidel (TGS): Same as PGS, but prefers substepping over iteration, running more simulation steps with smaller time steps rather than simply iteratively solving constraints. Substeps tend to be more effective than iterations, as shown in the Small Steps in Physics Simulation paper by Macklin et al.
  • Baumgarte stabilization: When solving contact constraints using impulses, boost the impulses using a bias to account for overlap and actually push the bodies apart.
  • Soft constraints: Similar to Baumgarte stabilization, but more stable and controlled. Based on the harmonic oscillator, soft constraints dampen constraint responses, and can be tuned intuitively with a frequency and damping ratio. The Avian docs have a more in-depth overview.
  • Warm starting: Store the constraint impulses from the previous frame and initialize the solver by applying them at the start of the current frame. This helps the solver converge on the solution faster, and is especially helpful when objects are coming to rest.
  • Relaxation: Baumgarte stabilization and soft constraints can add unwanted energy. Relaxation helps remove it by solving constraints a second time, but without a bias.

The choice was largely motivated by the wonderful Erin Catto’s Solver2D experiments, where TGS Soft was deemed as a clear winner. His video below showcases several different solvers quite well.

Erin Catto’s physics engine Box2D V3 (currently in beta) was used as the primary inspiration for the core implementation of the new solver. However, engines such as Rapier and Bepu also use a similar approach.

What does the solver implementation look like?

As stated earlier, contacts use TGS Soft. However, joints still currently use XPBD, so the new solver is actually a kind of hybrid solver. I do plan on transitioning joints away from XPBD in the future though.

Below is a high-level overview of the new structure of the solver.

  1. Broad phase collision detection collects potential collision pairs into BroadCollisionPairs.
  2. Narrow phase collision detection computes contacts for the pairs and adds them to Collisions.
  3. A ContactConstraint is generated for each contact manifold, and added to ContactConstraints.
  4. Substepping loop, running SubstepCount times.
    1. Integrate velocities, applying gravity and external forces.
    2. Warm start the solver.
    3. Solve velocity constraints with bias (soft constraints).
    4. Integrate positions, moving bodies based on their velocities.
    5. Relax velocities by solving constraints again, but without bias.
    6. Solve XPBD constraints (joints) and perform XPBD velocity projection.
  5. Perform swept CCD (if enabled).
  6. Apply restitution in a post-phase.
  7. Finalize positions by applying AccumulatedTranslation.
  8. Store contact impulses for next frame’s warm starting.

Refer to the code and PR for implementation details. The contact logic and constraints are quite heavily commented and should hopefully be relatively easy to follow (as far as physics engines go).

Results

Collisions now have significantly less drift than before, and performance is much better. Below is a pyramid with a base of 64 boxes, simulated with 4 substeps, with sleeping disabled.

Old: XPBD has substantial drift over time, and the pyramid collapses in on itself. This even happens with a much larger number of substeps, although to a slightly lesser degree. Performance is very poor, even with just 4 substeps.

New: With TGS Soft, the pyramid stays stable. There is a very small amount of drift over a long period of time, but even that can be mitigated by configuring the contact behavior through the SolverConfig and/or by adding more substeps. Performance is significantly better.

Impulses even out and stabilize much better with TGS Soft, even with deep overlap. In overlapping cases, the old XPBD implementation was significantly more explosive. Below is an example where colliders are dynamically enlarged to fill up a container.

Old: With XPBD, overlap causes explosions even before the shapes fill the container. Once they do fill the container, they jitter violently and explode through the walls. This is very undesirable for games.

New: Overlap is solved perfectly smoothly with no explosions. The contact impulses even out without jitter. With no space to go, the shapes prioritize stability over perfectly hard contacts that would cause explosiveness.

Rigid bodies could even be spawned at the same spot with deep overlap, and it would now get resolved more gently, whereas the old solver had the tendency to blast the bodies into oblivion.

An important thing to note is that the new solver uses several tolerances and thresholds that are length-based, such as the maximum speed at which bodies can be pushed apart, or the velocity threshold for sleeping. The old solver also had some tolerances, but now they are much more important for stability.

Without any tuning, a 2D game expressing object sizes in pixels might have collision issues, because the tolerances would be wrong for that application. For example, below is a scene with stacks of balls that have a radius of 5 pixels, with no tuning whatsoever:

No tuning

The contacts are too soft. To fix this, there is a new PhysicsLengthUnit resource that can be thought of as a kind of pixels-per-meter conversion factor. It is only for scaling the internal tolerances (and debug rendering gizmos!) however, and doesn’t scale colliders or velocities in any way.

PhysicsLengthUnit can be easily set when adding PhysicsPlugins for an app:

fn main() {
    App::new()
        .add_plugins((
            DefaultPlugins,
            // A 2D game with 10 pixels per meter
            PhysicsPlugins::default().with_length_unit(10.0),
        ))
        .run()
}

And with that, we get the stability and behavior we expect:

With the appropriate length unit

Instead of setting the length unit in 2D, you should also consider expressing all of your object sizes in meters and changing the camera projection to scale the visual output to the desired size. This has the added benefit that physical units and values will be more sensible, and the simulation may be even more stable.

In summary:

However:

The performance improvements are covered in the Performance Improvements section.

Collision Detection Rework #385

Author: @Jondolf (me)

As part of the solver rework, narrow phase collision detection has also been reworked.

What is a narrow phase?

Collision detection is typically split into two parts: a broad phase and a narrow phase.

  1. The broad phase performs cheap intersection tests to determine all of the potential intersection pairs. To speed things up, Axis-Aligned Bounding Boxes (AABBs) and an acceleration structure like a Bounding Volume Hierarchy (BVH) are often used.
  2. The narrow phase iterates through all of the potential intersection pairs and computes the actual contacts for each intersection. In Avian, the narrow phase also generates contact constraints, which are used by the contact solver.

Approximating Contacts for Substeps

Previously, the narrow phase was run at every substep. This was very bad for performance, as the narrow phase is one of the most expensive parts of the simulation.

If you think about it, running the narrow phase in the substepping loop makes sense: bodies can move and interact with each other at each substep, which can change the contact data. As costly as it is, we need to compute the contacts every time to get good results, right?

Well, not exactly. Two intersecting bodies are unlikely to move large distances relative to each other within a single frame. We can exploit this temporal coherence and approximate the current contact data at each substep with some simple vector algebra.

If we store the contact data in the local space of each body, we can compute the contact points at the current substep by simply transforming the local points by the current poses of the bodies:

// World-space contact points relative to center of mass
let anchor1 = rotation1 * local_anchor1;
let anchor2 = rotation2 * local_anchor2;

Updating contact data at substeps

Then, we can compute the current separation distance based on these points:

// Separation distance along contact normal at current substep
let separation = (position2 + anchor2 - position1 - anchor1).dot(normal);

Or alternatively, to improve precision when far from the world origin, using the position deltas accumulated so far during the frame:

// `anchor1` and `anchor2` computed and stored before substepping loop
let delta_anchor1 = delta_rotation1 * anchor1;
let delta_anchor2 = delta_rotation2 * anchor2;

// Change in separation distance relative to start of frame
let delta_separation =
    (delta_position2 + delta_anchor2 - delta_position2 - delta_anchor2).dot(normal);

// Initial separation computed and stored before substepping loop
let separation = initial_separation + delta_separation;

Of course, the contact data computed with this approach is approximate, but in practice it is accurate enough and definitely worth it for the performance gains. It makes substepping a viable approach for real-time physics.

The old solver actually used a similar approach already, but trying to move the narrow phase outside of the substepping loop with XPBD had its challenges and stability issues. One potential reason is that with XPBD, the relative positions change after every single constraint solve, not just once per substep, so there is more potential for error. With the new impulse-based solver, it was straightforward to implement.

The performance improvements are covered in the Performance Improvements section.

Other Changes and Fixes

Continuous Collision Detection (CCD)

Tunneling is a phenomenon where fast-moving small objects can pass through thin geometry such as walls due to collision detection being run at discrete time steps:

An object tunneling through a wall due to discrete timesteps

Moving the narrow phase out of the substepping loop as described earlier has the unfortunate consequence that it increases the risk of tunneling as collisions are not computed as frequently. Thus, a solution was needed.

One of the primary solutions to tunneling is Continuous Collision Detection. There are two common forms: speculative collision and sweep-based CCD. Luckily, Avian 0.1 supports both!

Speculative Collision #385

Author: @Jondolf (me)

Speculative collision is a form of Continuous Collision Detection where contacts are predicted before they happen. The contacts are only solved if the entities are expected to come into contact within the next frame.

To determine whether two bodies may come into contact, their AABBs are expanded based on their velocities. Additionally, a speculative margin is used to determine the maximum distance at which a collision pair can generate speculative contacts.

A visualization of speculative collision

The current “effective” speculative margin for a body is determined by its velocity clamped by the specified maximum bound. By default, the maximum bound is infinite, but it can be configured for all entities using the NarrowPhaseConfig resource, and for individual entities using the SpeculativeMargin component.

use avian3d::prelude::*;
use bevy::prelude::*;

fn setup(mut commands: Commands) {
    // Spawn a rigid body with a maximum bound for the speculative margin.
    commands.spawn((
        RigidBody::Dynamic,
        Collider::capsule(0.5, 2.0),
        SpeculativeMargin(2.0),
    ));
}

Speculative collisions are an efficient and generally robust approach to Continuous Collision Detection. They are enabled for all bodies by default, provided that the default naximum speculative margin is greater than zero.

How is the speculative collision response implemented?

The effective speculative margin lets us compute the closest points between the two separated bodies. These are used as approximations of the contact points.

When actually solving the contact (the normal part, no friction), softness parameters are only used if the contact is penetrating, meaning that the separation distance is negative. Otherwise, we know that the contact is speculative, and we can bias the impulse to cancel out the velocity that would cause penetration.

// Compute incremental impulse (before clamping).
let impulse = if separation > 0.0 {
    // Contact is speculative: Push back the part of the velocity that would cause penetration.
    -effective_mass * (normal_speed + separation / delta_secs)
} else {
    // Contact is penetrating: Handle it normally.
};

Caveats of Speculative Collision

Speculative contacts are approximations. They typically have good enough accuracy, but when bodies are moving past each other at high speeds, the prediction can sometimes fail and lead to ghost collisions. This happens because contact surfaces are treated like infinite planes from the point of view of the solver. Ghost collisions typically manifest as objects bumping into seemingly invisible walls.

An object hitting a ghost plane due to an inaccurate speculative contact

Ghost collisions can be mitigated by using a smaller SpeculativeMargin or a higher Time<Physics> timestep rate.

Another caveat of speculative collisions is that they can still occasionally miss contacts, especially for thin objects spinning at very high speeds. This is typically quite rare however, and speculative collision should work fine for the vast majority of cases.

Swept CCD #391

Author: @Jondolf (me)

Sweep-based Continuous Collision Detection sweeps colliders from their previous positions to the current ones, and moves the bodies to the time of impact if a hit was found.

This can be more reliable than speculative collision, and can act as a sort of safety net when you want to ensure that an object has no tunneling, at the cost of being more expensive and causing “time loss” where bodies appear to stop momentarily as they are brought back in time.

An object hitting a wall without tunneling thanks to sweep-based CCD

Two sweep modes are supported:

A comparison of linear and non-linear sweep-based CCD

To enable swept CCD for a rigid body, simply add the SweptCcd component and make sure that the CcdPlugin is enabled. The plugin is included in the PhysicsPlugins plugin group.

use avian3d::prelude::*;
use bevy::prelude::*;

fn setup(mut commands: Commands) {
    // Spawn a rigid body with swept CCD enabled.
    // `SweepMode::NonLinear` is used by default.
    commands.spawn((
        RigidBody::Dynamic,
        Collider::capsule(0.5, 2.0),
        SweptCcd::default(),
    ));
}

Comparison of CCD Approaches

Below, you can see the new ccd example with all of the different forms of CCD in action!

As you can see, all the different modes and combinations behave differently, and have their own unique characteristics and trade-offs.

Of course, this is a very extreme and relatively niche example, and each method can have their own pros and cons in different scenarios. For most objects in most games though, just speculative collision should be enough. For the cases where it isn’t, swept CCD can be used as a safety net.

Another thing to note is that CCD does not prevent bodies from being pushed through other objects due to contact softness. In the future, this could be mitigated to some extent by solving collisions between dynamic and static objects after dynamic-dynamic contacts, giving them higher priority and preventing objects from ever going through the walls or the ground.

Collision Margins #393

Author: @Jondolf (me)

Collisions for thin objects such as triangle meshes can sometimes have stability and performance issues, especially when dynamically colliding against other thin objects.

To allow users to mitigate these issues when necessary, a CollisionMargin component can now be added to any collider to force the solver to maintain an artificial separation distance between objects. It can be thought of as a kind of shell or skin that adds extra thickness to colliders for collisions, reducing the issues mentioned above.

// Add a collision margin around a triangle mesh collider.
commands.spawn((
    RigidBody::Dynamic,
    Collider::trimesh_from_mesh(&mesh),
    CollisionMargin(0.05),
));

Dynamic rigid bodies with trimesh colliders colliding against each other without a collision margin:

And with a collision margin:

(Note: The sticking issue could also be possibly reduced with trimesh pre-processing using the appropriate TrimeshFlags)

Collision margins should not be needed in most cases, and they are primarily useful specifically for collisions between triangle meshes or other extremely thin or hollow shapes. Primitive shapes and convex decomposition should be preferred over trimesh colliders for dynamic objects wherever possible.

Runtime Collider Constructors #378

Authors: @janhohenheim, @Jondolf (me)

The Collider component does not currently support Reflect. This can be a problem for people who would like to define collision shapes outside of their own code, such as in assets, or even in external tools like Blender using something like the Blender_bevy_components_workflow.

Previous releases had the AsyncCollider and AsyncSceneCollider components that could automatically generate colliders for meshes and scenes at runtime:

fn setup(mut commands: Commands, mut meshes: Assets<Mesh>, mut assets: ResMut<AssetServer>) {
    // Spawn a cube with a convex hull collider generated from the mesh at runtime.
    commands.spawn((
        AsyncCollider(ComputedCollider::ConvexHull),
        PbrBundle {
            mesh: meshes.add(Cuboid::default()),
            ..defaul)
    ,
    ));

    // Load a scene and automatically create trimesh colliders for the meshes.
    // `AsyncSceneCollider` will wait for the scene to be ready before inserting the colliders.
    let scene = assets.load("my_model.gltf#Scene0");
    commands.spawn((
        SceneBundle { scene: scene.clone(), ..default() },
        AsyncSceneCollider::new(Some(CoutedCollide:TriMesh)),
    ));
}

ComputedCollider was an enum for specifying what kind of collider is computed for each mesh:

pub enum ComputedCollider {
    TriMesh,
    TriMeshWithFlags(TriMeshFlags),
    ConvexHull,
    ConvexDecomposition(VHACDParameters),
}

However:

The limitations frequently required people to set up their own enum-based reflectable component that mirrors Collider, such as the following:

#[derive(Debug, Component, Reflect)
#[reflect(Component)]
enum ColliderMirror {
    Cuboid { /* */ },
    Sphere { /* */ },
    Trimesh,
    // ...
}

This was not ideal.

Luckily, Avian 0.1 reworks AsyncCollider, AsyncSceneCollider, and ComputedCollider to be much more general and useful.

ComputedCollider is now called ColliderConstructor, and it supports almost all shapes, apart from compound shapes (for now):

// Supports `Reflect`! (other attributes left out for brevity)
#[derive(Clone, Debug, PartialEq, Reflect, Component)]
pub enum ColliderConstructor {
    Sphere {
        radius: Scalar,
    },
    Cuboid {
        x_length: Scalar,
        y_length: Scalar,
        z_length: Scalar,
    },
    // ...other shapes
    // Note: TriMesh has been renamed to Trimesh for consistency with `Collider::trimesh`.
    TrimeshFromMesh,
    TrimeshFromMeshWithConfig(TrimeshFlags),
    ConvexDecompositionFromMesh,
    // Note: Previously VHACDParameters, which does not follow Rust's naming conventions.
    ConvexDecompositionFromMeshWithConfig(VhacdParameters),
    ConvexHullFromMesh,
}

It has also been turned into a component that replaces AsyncCollider, which previously was just a wrapper around ComputedCollider.

Now that shapes other than meshes are supported, ColliderConstructor can also be used in 2D!

Similarly, AsyncSceneCollider has been renamed to ColliderConstructorHierarchy. Aside from supporting Reflect and using ColliderConstructor, it has also been reworked to work for any hierarchies, not just scenes. This enables more powerful workflows, as Scene is no longer a requirement.

// Generate sphere colliders for all available descendants, ignoring the hierarchy root itself.
// If the constructor is a computed shape like `TrimeshFromMesh`, only descendants
// with a mesh will get a collider by default.
commands
    .spawn(ColliderConstructorHierarchy::new(ColliderConstructor::Sphere { radius: 0.5 }))
    .with_children(|parent| {
        // ...
    });

The old behavior is still preserved in that ColliderConstructorHierarchy waits for a scene to be loaded if the component is placed on one. This behavior requires the new bevy_scene feature, which is enabled by default.

Performance Improvements

Solver and Narrow Phase Rework #385

Author: @Jondolf (me)

Comparing the old solver and narrow phase to the new ones, both running on Bevy 0.14, there is roughly a 4-5x performance improvement for collision-heavy scenes, with the difference growing with the number of collisions and the number of substeps.

Using 8 substeps:

Benchmark comparing old and new solver with 8 substeps

With just a single substep, the difference is smaller, but the new solver is still faster, up to 2x.

Benchmark comparing old and new solver with 1 substep

Note that these are not very thorough benchmarks, and results can vary a bit based on the scene, but through extensively testing out the new solver myself in various samples, I can say that it is definitely significantly faster and can handle a lot more collisions, even when single-threaded.

Some reasons for the speedup are most likely:

Transform Propagation #377 and #380

Author: @Jondolf (me)

The engine has a lot of additional transform propagation to make sure that positions are kept up to date, GlobalTransform is kept in sync with the physics Position and Rotation components, and user changes to each of the components are accounted for. Not only that, but it also separately propagates ColliderTransform components for colliders. And some of the propagation is done at every substep.

This wouldn’t be that big of an issue if the systems only traversed through physics entities, but in previous releases, they had to traverse through basically everything. This resulted in awful performance for scenes with many non-physics entities, even if there were no physics entities in the world at all.

Avian 0.1 reworks all of the transform propagation systems to use custom implementations that only traverse trees with physics entities. This is done by marking ancestors of physics entities with an AncestorMarker<C: Component> component that indicates that some descendant of the entity has the given component. This can be used to skip traversing trees that have no effect on the simulation.

The result is significantly better performance for scenes with many non-physics entities.

Test scene: 12 substeps, 1 root entity, 100,000 child entities. All entities have just a SpatialBundle, and one child entity also has a Collider.

These were tested before the solver rework. Now, in Avian 0.1, I get up to 600 FPS (not entirely sure where that +110 FPS comes from). Nice!

Reworked Module Structure #370

Author: @Jondolf (me)

The old high-level module structure looked like this:

Old module structure

where plugins looked like this:

Plugins in old module structure

This structure wasn’t ideal.

It was fine in the beginning, but as the engine has grown, the lack of separation of concerns has really started harming the modularity of the engine and can make it harder to find and organize things.

Avian 0.1 has a new reworked structure without modules like plugins, components, or resources. Everything is separated by logical concerns.

New module structure

This helps with organization, discoverability, documentation, and nicer import paths. For example, everything related to rigid body dynamics is just in dynamics instead of being split across a dozen separate modules with no central place for a high-level overview.

The PhysicsSetupPlugin has also been split into a PhysicsSchedulePlugin and a PhysicsTypeRegistrationPlugin.

For user applications migrating from Bevy XPBD, most imports from the prelude should work like before, but explicit import paths may need to be changed.

API Consistency Improvements

Collider Constructor Methods

Author: @Jondolf (me)

The new constructors for Bevy’s Cylinder, Capsule, and Cone shapes take a radius first, and a height second.

Capsule::new(radius, height)

However, our collider constructors have used the opposite order:

Collider::capsule(height, radius)

The reason for this is that it is the order used by Parry (the collision detection library), and Bevy’s primitive shapes were added afterwards with a different ordering. This is a very unfortunate inconsistency and can easily lead to confusion.

To match Bevy’s APIs, the order of arguments has changed for some Collider constructors.

Applications migrating from Bevy XPBD will likely notice this as colliders having completely wrong dimensions before fixing the ordering. It is a very highly breaking change, but I believe it is important that we align ourselves with Bevy here sooner rather than later.

In addition, Collider::halfspace has been renamed to Collider::half_space for consistency with HalfSpace.

Rotation Component #370

Author: @Jondolf (me)

Avian has its own Rotation component for the global rotations of physics entities. It has been updated to match the API of Bevy’s new Rot2 type more closely.

The primary breaking changes are that rotate and mul have been deprecated in favor of Mul implementations, and the 2D from_radians and from_degrees have been renamed to just radians and degrees.

// Before
let rotation = Rotation::from_degrees(45.0);
assert_eq!(rotation.mul(rotation).rotate(Vec2::X), Vec2::Y);

// After
let rotation = Rotation::degrees(45.0);
assert_eq!(rotation * rotation * Vec2::X, Vec2::Y);

Add and Sub implementations have also been removed, as adding or subtracting quaternions in 3D is not quite equivalent to performing rotations, which can be a footgun, and having the 2D version function differently would also be inconsistent.

Sensor Mass Properties #381

Author: @Jondolf (me)

The Sensor component can be used to make a Collider behave like a trigger that detects collisions and sends events, but does not cause an actual collision response and apply forces on colliding bodies. This is often used to detect when something enters or leaves an area or is intersecting some shape.

In Bevy XPBD, colliders could contribute to the mass properties of rigid bodies and have an effect on the center of mass. This differs from most peoples’ expectations, and is also different from many existing engines such as Godot and Unity.

Avian 0.1 changes the behavior of sensors so that they no longer have an impact on the physics simulation at all, apart from sending collision events. To add mass for them yourself, you can add another collider that is not a sensor, or manually add mass properties with the MassPropertiesBundle or its components.

Other Changes and Fixes

What’s Next?

Avian 0.1 is a big step forward from Bevy XPBD, and I’m incredibly excited for the future.

I have lots of plans, including:

Simulation islands, geometric queries for Bevy’s primitives, and working towards Glam-based collision detection will likely be the next big tasks I will focus on, but there are numerous other smaller improvements I’d also like to implement. There’s always more to do!

On a more personal note: I have officially graduated from high school, and will start university in late August. There’s still a decent amount of time until then, but once it does start, I will most likely have less time for development. I will try my best though, and I am very invested in trying to improve the state of physics and collision detection in the Bevy ecosystem.

With that being said, I am going to take at least a few days off to recharge my batteries before starting work on new features again. Going through the rebrand and various rewrites has been quite a stressful undertaking, but I am glad that we did it and that everything turned out so well in the end.

As always, huge thanks to everyone for all of the incredible support! Without you, this project would not be where it is today.

Support Me

While Avian will always be free and permissively licensed, developing and maintaining it takes a lot of time and effort.

If you find my work valuable, it is now possible to support me through GitHub Sponsors. This is ultimately my hobby, but by supporting me you can help make it more sustainable.

Thank you ❤️