Solver Bodies#735
Merged
Merged
Conversation
Removal is not yet implemented
Also reworked `get_pair_unchecked` and renamed it to `get_pair_unchecked_mut`.
Joints now store base positional data like anchors from before the solver step, and compute updated values during substeps using position and rotation deltas. Lots of clean-up and documentation work is still needed.
…elocity`, `PreSolveAngularVelocity`
Jondolf
added a commit
that referenced
this pull request
May 15, 2025
# Objective #420 changed Avian to use implicit Euler integration for solving gyroscopic torque. This was done for stability, as the semi-implicit version was prone to blowing up, because it extrapolated angular velocity and brought energy into the system. However, that implementation of gyroscopic torque is fairly expensive, often being more than half of the total cost of velocity integration. It is also computed even for shapes with isotropic inertia tensors (e.g. spheres and regular solids) that don't experience gyroscopic torque. It would be nice to make this have less overhead. Additionally, #735 broke gyroscopic motion, which needs to be fixed. ## Solution 1. Switch gyroscopic torque back to an approach that is closer to semi-implicit Euler integration, but clamp the magnitude of the angular momentum to remain the same. This prevents energy from being introduced to the system and seems to be sufficiently accurate for game physics. This idea was inspired by Jolt's [`ApplyGyroscopicForceInternal`](https://github.com/jrouwe/JoltPhysics/blob/d497df2b9b0fa9aaf41295e1406079c23148232d/Jolt/Physics/Body/MotionProperties.inl#L102). 2. Skip computation of gyroscopic torque for shapes with isotropic inertia tensors. This makes e.g. spheres, cubes, and other regular solids faster. > [!NOTE] > Do regular solids really have isotropic inertia tensors? Yes! > See [Moments of inertia of Archimedean solids](https://web.archive.org/web/20211003110410/https://www.ipgp.fr/sites/default/files/archimedeani_200115.pdf) by Frédéric Perrier: "The condition of two axes of threefold or higher rotational symmetry crossing at the center of gravity is sufficient to produce an isotropic tensor of inertia. The MI around any axis passing by the center of gravity are then identical." ## Results This is a 3D test scene with a bunch of cube stacks. The cubes have isotropic inertia tensors. - **Left**: Before any optimizations. - **Middle**: With the faster gyroscopic torque solver. - **Right**: With the faster gyroscopic torque solver, but ignoring computation for isotropic tensors (all dynamic bodies in this scene).  Velocity integration went down from 0.24 ms to 0.18 ms with the new solver, and still down to 0.13 ms ignoring unnecessary gyroscopic computations. Nice! In the future, we could also consider making gyroscopic motion opt-in with a `GyroscopicMotion` component if we wanted to optimize this further for non-isotropic cases. ## Bonus: `gyroscopic_motion` Example I added a new `gyroscopic_motion` example to demonstrate the Dzhanibekov effect and test the conservation of angular momentum. https://github.com/user-attachments/assets/9ef9e335-feb9-4685-8f12-2f34ba442937
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Objective
Our current solver is highly bottlenecked by expensive ECS queries and sub-optimal cache performance. Each constraint currently looks up
RigidBodyQueryItemstructs for the involved bodies, which holds 16 different properties/components, many of which are optional. This results in a ton of overhead in a very hot loop.We need a more efficient and densely packed representation for body data required by the solver. This will also make vectorization and eventual SIMD constraints much more viable to implement.
Solution
Add
SolverBodyandSolverBodyInertiastructs designed to improve memory locality and performance. They are initialized with body data before the solver, and the results are written back after the solver. In 2D, the types look like this:Note
In 3D,
SolverBodyis currently 56 bytes, but we can look into a more optimized representation later on.SolverBodyInertiais also 44 bytes because of the 3x3 inertia tensor, but if/when we add aSymmetricMat3type with 6 floats, it'll be 32 bytes.Only awake dynamic bodies and kinematic bodies have an associated solver body. Static bodies and sleeping dynamic bodies do not move, so they instead use a "dummy state" with
SolverBody::default(). However, this raises a problem: if the solver doesn't have access to the position or rotation of static bodies, how can we compute constraint anchors? There's two options:Avian uses Option 1, because:
As a result, joints now also have a
preparestep before the substepping loop where they prepare some base offsets for the solver.This approach was largely inspired by Box2D v3, with some small modifications, like how inertial properties are stored. Box2D stores them in constraints, which leads to duplicating some data, whereas I store them per-body.
Storing Solver Bodies
For storing these solver bodies, I investigated two approaches:
SolverBodiesresource withVecs for the body data, and use aSolverBodyIndexcomponent to map rigid body entities to them.SolverBodyas a component on the rigid body entity itself.Option 1 gives us a bit more control and ends up being slightly faster especially for random access, since it's just indexing a
Vec. However, Option 2 has way less complexity and bookkeeping, and integrates better with the ECS (ex: no need for double-lookups when both ECS data and the solver body is needed, easier parallel iteration, integration with query filters...), so I opted to use that for the initial PR. We will also benefit from any improvements to Bevy'sQuery::getperformance here.You can see how the two approaches differ in the Results section at the end.
Changes to Constraints
As mentioned before, joints now need a
preparestep to prepare base translational and rotational offsets and the relative dominance of the bodies (we may be able to abstract dominance out somehow). This is done once per timestep, before substepping. Joints then compute updated offsets and other positional data based on the pre-computed base data and thedelta_positionanddelta_rotation.Contact constraints have undergone similar changes, but we can now also get rid of the
local_anchor1andlocal_anchor2, as we can now apply thedelta_rotationto the world-spaceanchor1andanchor2.We also have way less branching, and don't check whether bodies are dynamic or not before applying constraint impulses. If they aren't, the mass will be infinite anyway, and the impulse won't do anything.
Joint Refactor
Aside from changes required for solver bodies, I have also refactored joints in many other ways.
entitiesmethod now uses a separateEntityConstrainttype to allow reusable constraints that don't store entities.PointConstraintandFixedAngleConstraintto share more constraint logic between different joint types.with_compliancehave been moved out of theJointtrait.with_point_complianceandwith_swing_compliance.xpbd::solve_constraintis nowxpbd::solve_xpbd_joint, and only supports two entities (make your own system if you need more).Other Notable Changes
The following components have been removed and are no longer stored for rigid bodies:
AccumulatedTranslationPreSolveAccumulatedTranslationPreSolveLinearVelocityPreSolveAngularVelocityPreSolveRotationPreviousRotationThat's a lot of unnecessary data gone! Some of them were already redundant, and others were made unnecessary by the changes in this PR.
Instead, we now require two new components:
PreSolveDeltaPositionPreSolveDeltaRotationThese are needed for XPBD velocity projection, but can be removed once we switch joints to be impulse-based.
There are frankly so many small internal changes that I can't really cover them all in detail, so I suggest looking at the diff for more details. For the most part, the changes are internal, and won't directly affect the average user (beyond the improved performance :D).
Results
The results are quite remarkable. In the
pyramid_2dexample, using a base of 100 boxes, the results look like the following with 6 substeps and theparallelfeature enabled.mainbranch, no solver bodies.SolverBodiesresource, mapping entities to them with aSolverBodyIndexcomponent.SolverBodycomponent.That is nearly a 3x performance improvement in this collision-heavy scene. Crazy!
Using
SolverBodyas a component is slightly more expensive here, but for now, I think it's better to start with that to reduce complexity. We can always re-assess this later.Future Work
There's still a lot more we can do to optimize things further. Some ideas include:
SolverBody.SymmetricMat3type (I have this mostly implemented).SolverBodylayout is designed to be quite optimal for this, at least in 2D.Migration Guide
Avian's solver now uses special
SolverBodyandSolverBodyInertiacomponents instead of separate components likePosition,Rotation,AccumulatedTranslation,LinearVelocity, andAngularVelocity. They are initialized for awake dynamic and kinematic bodies in the substepping loop, and the results are written back to the user-facing components at the end. This also means that if you are running custom logic inside the substepping loop, you should generally useSolverBodyinstead of the separate components.Solver bodies were added to drastically improve the performance of the solver, but it also means significant changes to internals.
Removed Components and Properties
The following components have been removed and are no longer stored for rigid bodies:
AccumulatedTranslationPreSolveAccumulatedTranslation(now calledPreSolveDeltaPosition)PreSolveLinearVelocityPreSolveAngularVelocityPreSolveRotationPreviousRotationA new
PreSolveDeltaRotationcomponent has also been added.The
current_positionhelper onRigidBodyQueryItemandColliderQueryItemhas been removed.The
local_anchor1andlocal_anchor2properties onContactConstraintPointhave been removed.Joints
with_compliancehave been moved out of theJointtrait.with_point_complianceandwith_swing_compliance.entitiesmethod now uses a separateEntityConstrainttrait to allow reusable constraints that don't store entities.XpbdConstrainttrait now has apreparestep to prepare base translational and rotational offsets and any other pre-step data.XpbdConstraint::solveand many related methods now useSolverBodyandSolverBodyInertiastructs. Positional corrections are applied to thedelta_positionanddelta_rotationproperties. Updated positional information can be computed based on the pre-step data and these deltas.PointConstraintandFixedAngleConstraintto share more constraint logic between different joint types.xpbd::solve_constraintis nowxpbd::solve_xpbd_joint, and only supports two entities (make your own system if you need more).Other
SolverSet::ApplyTranslationhas been renamed toSolverSet::Finalize.