-
-
Notifications
You must be signed in to change notification settings - Fork 108
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Rework contact solver and collision detection, implement speculative collision #385
Conversation
The KCC collision logic was reworked to handle slope snapping, sliding, and speculative collision properly after the narrow phase changes.
The contact constraint generation needs to be kept separate from collision detection for now.
It's causing panics, and frankly, pretty useless :P
.add_plugins(( | ||
DefaultPlugins, | ||
ExampleCommonPlugin, | ||
PhysicsPlugins::default().with_length_unit(15.0), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think a potential new user will be a bit confused by this line.
This goes for the other instances of with_length_unit
in the other examples as well. Is there a reason why the examples don't use the default unit length?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These examples have a 2D camera with a default projection, so the objects use pixels for their dimensions and are quite large in terms of physics units if the length unit was the default 1.0
. This leads to the issues with overly soft contacts and incorrectly tuned tolerances, which can be seen at the end of the "New Solver Results" section (the two screenshots).
Note that bevy_rapier also has this, but in 2D it calls the method pixels_per_meter
, even though internally the property is still called length_unit
. And it also has a with_length_unit
method, so it just has two differently named methods that essentially configure the same thing.
A quick survey on what names different engines use for this property:
- Rapier:
length_unit
(or in 2D, also thepixels_per_meter
method) - Box2D:
b2_lengthUnitsPerMeter
- PhysX:
PxTolerancesScale.length
I agree though that the method usage should probably be commented in the examples, or the method name itself could be more self-explanatory. It's just hard to come up with an approachable name that conveys the purpose, but does not imply that it actually scales velocities or forces in any way.
For example, if there was a physics_scale
property and it was set to 10.0
(let's imagine this is 2D), a user could reasonably assume that Collider::rectangle(1.0, 1.0)
would get scaled to a 10x10 pixel collider. Or you could assume that you could use LinearVelocity::X
, and have the body move at the speed of 10 pixels per second. But this is not what length_unit
does, as it just scales the internal length-based tolerances and thresholds to match the expected scale. The nuance is difficult to convey through the name of the property.
For these examples though, another option would be to just scale the camera projection so that physics objects don't have to be so large in terms of physics units and you don't need a custom length unit. This would technically be more ideal for physics in other ways too, although I don't know what the game dev best practices are for 2D games when it comes to using pixels vs. other units for scenes.
(sorry for the long response lol)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For now, I added a comment briefly explaining the length unit to each example that sets it. I also improved the length unit docs overall
if *ent1 < *ent2 { | ||
broad_collision_pairs.push((*ent1, *ent2)); | ||
} else { | ||
broad_collision_pairs.push((*ent2, *ent1)); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Too bad minmax
is nightly only :/
Co-authored-by: Jan Hohenheim <[email protected]>
Addressed most of the comments. A few other changes:
|
# Objective #385 added speculative collision, a form of Continuous Collision Detection (CCD) with the goal of preventing tunneling. However, it isn't perfect; speculative collision can sometimes cause ghost collisions, and it can miss collisions against objects spinning at very high speeds. Another form of CCD is swept CCD, which 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. Both forms of CCD are valuable and should exist. They can also be used together to complement each other! ## Solution Add a `CcdPlugin` that performs sweep-based CCD for rigid bodies that have the `SweptCcd` component. ```rust commands.spawn(( RigidBody::Dynamic, Collider::circle(0.5), // Enable swept CCD for this rigid body. SweptCcd::default(), )); ``` Two sweep modes are supported: - If `SweptCcd::mode` is `SweepMode::Linear`, only translational motion is considered. - If `SweptCcd::mode` is `SweepMode::NonLinear`, both translational and rotational motion are considered. This is more expensive. By default, the mode is `SweepMode::NonLinear`, but it can be specified using associated constants `SweptCcd::Linear` or `SweptCcd::NonLinear`, or by using the `SweptCcd::new_with_mode` constructor. It may not be desirable to have CCD enabled for all collisions. To enable it based on the relative velocities of the bodies involved, velocity thresholds can be set: ```rust SweptCcd::NonLinear.with_velocity_threshold(linear, angular) ``` Additionally, swept CCD can be disabled for collisions against dynamic bodies: ```rust SweptCcd::NonLinear.include_dynamic(false) ``` Below you can see the new `ccd` example and the different forms of CCD in action! https://github.com/Jondolf/bevy_xpbd/assets/57632562/2a27d92b-85aa-404e-b258-dcf4f914b12e As you can see, all the different modes and combinations behave differently, and have their own unique characteristics and trade-offs. In this spinning example, sweep-based CCD clearly has time loss, especially with speculative collision disabled. Non-linear swept CCD is also the most reliable however, as it doesn't let any of the projectiles tunnel through. Linear swept CCD on the other hand struggles a lot in this case since it doesn't take rotational motion into account. Of course, this is a very extreme and relatively niche example. For most objects in most games, speculative collision should be enough, and swept CCD can be used as a safety net when it isn't. ### Implementation The implementation works as follows: - Store broad phase intersections for entities with `SweptCcd` in an `AabbIntersections` component. - In `solve_swept_ccd`, iterate through the AABB intersections for each entity with `SweptCcd`, and perform shape casts to find the minimum time of impact. - If `SweptCcd::mode` is `SweepMode::Linear`, only translational motion is considered. - If `SweptCcd::mode` is `SweepMode::NonLinear`, both translational and rotational motion are considered. This is more expensive. - Integrate the positions of the bodies for the duration of the minimum time of impact to move them into a touching state. - Normal collision detection and collision response will handle the collision during the next frame. A number of physics engines were used as inspiration for the implementation and terminology, primarily Box2D V3, Bepu, and Jolt. Unlike Rapier, I did not implement substepping for the time of impact solver, because it is more expensive and complex, and likely much more difficult to parallelize in the future. I believe Box2D V3 is opting for a similar approach for similar reasons.
Objective
Note: This is for the
avian
branch, which will later be pushed tomain
.Bevy XPBD currently uses Extended Position-Based Dynamics for its solver. While it has worked quite well, it does have several issues:
Additionally, XPBD even has some potential legal ambiguities, see #346.
Aside from XPBD itself, there are also several other issues with the current collisions and rigid body dynamics:
These are big issues, and I don't currently see XPBD as the best option for the contact solver going forward. Extensive rewrites and fixes are needed.
Solution
This PR consists of three major parts:
They all relate to each other, and are covered in detail below.
Substepped Impulse-Based Solver With Soft Constraints
The contact solver has been rewritten to use TGS Soft, a substepped impulse-based solver using soft constraints. The choice was largely motivated by the wonderful Erin Catto's Solver2D experiments, where TGS Soft was deemed as quite a clear winner.
Box2D V3 was used as the primary inspiration for the core implementation of the new solver. Engines such as Rapier and Bepu also use a very similar approach.
Terminology
Please refer to the Solver2D post for a more complete overview of what TGS and soft constraints are, how they work, and how they relate to other approaches.
Solver Overview
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.
BroadCollisionPairs
.Collisions
.ContactConstraint
is generated for each contact manifold, and added toContactConstraints
.SubstepCount
times.AccumulatedTranslation
.Refer to the code for implementation details. The contact logic and constraints are quite heavily commented and should hopefully be relatively easy to follow.
New Solver Results
Collisions 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, and the pyramid quickly 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.
2024-06-24.22-01-47.mp4
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.2024-06-24.21-58-54.mp4
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 very strongly and explode through the walls. This is typically very undesirable for games.
2024-06-24.22-20-27.mp4
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.
2024-06-24.22-22-06.mp4
An important thing to note is that the new solver uses several tolerances and thresholds that are length-based. The old solver also had some tolerances, but now they are much more important for stability.
Without any tuning, a 2D game using pixels as length units 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.0, with no tuning whatsoever:
The contacts are too soft. To fix this, there is a new
PhysicsLengthUnit
resource, which 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 objects or velocities in any way.PhysicsLengthUnit
can be easily set when addingPhysicsPlugins
for an app:And with that, we get the stability and behavior we expect:
Collision Detection Refactor
The narrow phase and the
PostProcessCollisions
schedule are now run in thePhysicsSchedule
instead of theSubstepSchedule
. This greatly improves performance, and also makes collision events and reacting to collisions less awkward and footgunny: previously, the contact data was always from the last substep, but at that point, the contact is often already mostly resolved.Moving narrow phase collision detection out of the substepping loop is possible by simply storing contact data in local space and computing the updated separation distance at each substep using the current poses of the bodies. This way, the contact data is (approximately) accurate even with the bodies moving relative to each other within substeps.
A few other improvements have been made as well:
NarrowPhase
system parameter.Speculative Collision
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:
Moving the narrow phase out of the substepping loop has the unfortunate consequence that it increases the risk of tunneling as collisions are not computed as frequently. A solution for this is needed before we commit to the narrow phase change.
One of the primary solutions to tunneling is Continuous Collision Detection. There are two common forms:
Sweep-based CCD can be much more expensive and more complex, especially with substepping and non-linear sweeps. Speculative collision on the other hand is very cheap while still being quite robust, although in rare cases it can miss collisions or cause ghost collisions.
The new solver implements speculative collision, which is enabled for all bodies by default. In my experiments, this is efficient, improves stability, and eliminates almost all tunneling except when caused by contact softness. This approach seems to also be taken by both Bepu and Box2D.
Below is a high-level overview of how speculative collision is currently implemented.
From a user's point of view, this happens completely in the background. However, if desired, the speculative margin can be configured for an entity using the
SpeculativeMargin
component, or even globally usingdefault_speculative_margin
inNarrowPhaseConfig
.Below is an example of balls being shot at high speeds at thin walls and spinning objects, with just a single substep to eliminate the effect of substepping. The simulation is paused and stepped manually a few times to closer see the behavior.
Old: Almost all of the balls simply pass through the thin geometry. With more substeps, this tunneling could been reduced, but never removed completely. Increasing the substep count would also hurt performance.
2024-06-25.17-34-10.mp4
New: The balls never tunnel straight through the walls, and often even hit the spinning objects. You can still see many balls phasing through the walls, but this is mostly due to contact softness, in cases where a body hits another body hard enough to force it through the wall.
2024-06-25.17-30-44.mp4
Of course, bodies getting pushed through the ground by other bodies is still an issue. This could be reduced in the future by solving contacts against static objects after dynamic-dynamic contacts, giving them higher priority and stiffness. This is follow-up material however, and cases where this is an issue should be quite rare in games.
Sweep-based CCD will also be added as an option in a follow-up. I already have an implementation ready locally.
Performance Results
Comparing this branch to the
avian
branch (which still has the old solver), there is a roughly 4-5x performance improvement for collision-heavy scenes, with the difference growing with the number of collisions and the number of substeps.The benchmarks below use 8 substeps.
With just a single substep, the difference is smaller, but this branch is still faster, up to 2x.
Other Changes
This is such a big rework that it is unfortunately almost impossible to cover every change. Below are some noteworthy changes however.
Sleeping Rework
PhysicsChangeTicks
component. This is used to detect what component changes are made by the user outside of physics schedules, and what changes are made by the physics engine. This way, we can ignore changes made by the physics engine and have more control over when bodies are woken up.Integrator Rework
IntegrationSet::Velocity
andIntegrationSet::Position
. This is needed for the new solver.par_iter_mut
.Scheduling Changes
PhysicsStepSet
now hasFirst
andLast
variants, so users can easily schedule systems before or after physics in thePhysicsSchedule
.PostProcessCollisions
schedule are now run inPhysicsStepSet::NarrowPhase
instead ofSubstepSet::NarrowPhase
.IntegrationSet::Velocity
andIntegrationSet::Position
instead ofSubstepSet::Integrate
.SubstepSet
has been removed.PhysicsStepSet::Solver
.SolverSet
.SolverSet::Substep
.SubstepSolverSet
.New Configuration Options
NarrowPhaseConfig
has newdefault_speculative_margin
andcontact_tolerance
properties.SolverConfig
resource can be used for tuning collisions.PhysicsLengthUnit
resource can be used as a scaling factor for internal length-based tolrances.linear
property ofSleepThreshold
.Examples
XpbdExamplePlugin
has been renamed toExampleCommonPlugin
, and it no longer addsPhysicsPlugins
automatically.PhysicsLengthUnit
configured.one_way_platform
example's logic has been modified to account forPostProcessCollisions
no longer running in theSubstepSchedule
.PhysicsSchedule
without jitter or stability issues.Miscallaneous
ColliderAabb
now has thegrow
andshrink
methods.ContactData
now stores feature IDs for contact matching, which is needed for warm starting.ContactData
propertyindex
has been removed.Migration Guide
New Contact Solver
The contact solver has been rewritten. In practice, this has the following effects:
However:
SolverConfig
.PhysicsLengthUnit
to get the best stability and behavior.The
PhysicsLengthUnit
can be thought of a pixels-per-meter scaling factor for the engine's internal length-based tolerances and thresholds, such as the maximum speed at which overlap is resolved, or the speed threshold for allowing bodies to sleep. It does not scale actual physics objects or their velocities.To configure the
PhysicsLengthUnit
, you can insert it as a resource, or simply set it while addingPhysicsPlugins
:Custom Constraints
Custom constraints using XPBD are currently still possible. However, the traits and systems are now located in the
dynamics::solver::xpbd
module, and users should runsolve_constraints
inSubstepSolverSet::SolveUserConstraints
instead ofSubstepSet::SolveUserConstraints
.Scheduling Changes
Several scheduling internals have been changed. For example:
PostProcessCollisions
schedule are now run inPhysicsStepSet::NarrowPhase
instead ofSubstepSet::NarrowPhase
.IntegrationSet::Velocity
andIntegrationSet::Position
instead ofSubstepSet::Integrate
.SubstepSet
has been removed.PhysicsStepSet::Solver
.SolverSet
.SolverSet::Substep
.SubstepSolverSet
.Systems running in
PostProcessCollisions
may need to be modified to account for it being moved outside of the substepping loop.