Expand description
Extended Position-Based Dynamics (XPBD) constraint functionality.
XPBD is a simulation method that solves constraints at the position-level. Avian currently uses it for joints, while contacts use an impulse-based approach.
This module contains traits and systems for XPBD functionality.
The actual joint implementations are in dynamics::solver::joints
,
but it is also possible to create your own constraints.
The following section has an overview of what exactly constraints are, how they work with XPBD, and how you can define your own constraints.
§Constraints
Constraints are a way to model physical relationships between entities. They are an integral part of physics simulation, and they can be used for things like contact resolution, joints, soft bodies, and much more.
At its core, a constraint is just a rule that is enforced by moving the participating entities in a way that satisfies that rule. For example, a distance constraint is satisfied when the distance between two entities is equal to the desired distance.
Most constraints in Avian are modeled as seperate entities with a component that implements XpbdConstraint
.
They contain a solve
method that receives the states of the participating entities as parameters.
You can find more details on how to use each constraint by taking a look at their documentation.
Below are the currently implemented XPBD-based constraints.
Avian’s ContactConstraint
is impulse-based instead.
§Custom constraints
In Avian, you can easily create your own XPBD constraints using the same APIs that the engine uses for its own constraints.
First, create a struct and implement the XpbdConstraint
trait, giving the number of participating entities using generics.
It should look similar to this:
use avian3d::{prelude::*, dynamics::solver::xpbd::XpbdConstraint};
use bevy::{ecs::entity::{EntityMapper, MapEntities}, prelude::*};
struct CustomConstraint {
entity1: Entity,
entity2: Entity,
lagrange: f32,
}
#[cfg(feature = "f32")]
impl XpbdConstraint<2> for CustomConstraint {
fn entities(&self) -> [Entity; 2] {
[self.entity1, self.entity2]
}
fn clear_lagrange_multipliers(&mut self) {
self.lagrange = 0.0;
}
fn solve(&mut self, bodies: [&mut RigidBodyQueryItem; 2], dt: f32) {
// Constraint solving logic goes here
}
}
impl MapEntities for CustomConstraint {
fn map_entities<M: EntityMapper>(&mut self, entity_mapper: &mut M) {
self.entity1 = entity_mapper.map_entity(self.entity1);
self.entity2 = entity_mapper.map_entity(self.entity2);
}
}
Take a look at XpbdConstraint::solve
and the constraint theory to learn more about what to put in solve
.
Next, we need to add a system that solves the constraint during each run of the solver.
If your constraint is a component like Avian’s joints, you can use the generic solve_constraint
system that handles some of the background work for you.
Add the solve_constraint::<YourConstraint, ENTITY_COUNT>
system to the substepping schedule’s
SubstepSolverSet::SolveUserConstraints
system set.
It should look like this:
// Get substep schedule
let substeps = app
.get_schedule_mut(SubstepSchedule)
.expect("add SubstepSchedule first");
// Add custom constraint
substeps.add_systems(
solve_constraint::<CustomConstraint, 2>
.in_set(SubstepSolverSet::SolveUserConstraints),
);
Now, just spawn an instance of the constraint, give it the participating entities, and the constraint should be getting
solved automatically according to the solve
method!
You can find a working example of a custom constraint here.
§Theory
In this section, you can learn some of the theory behind how position-based constraints work. Understanding the theory and maths isn’t important for using constraints, but it can be useful if you want to create your own constraints.
Note: In the following theory, primarily the word “particle” is used, but the same logic applies to normal rigid bodies as well. However, unlike particles, rigid bodies can also have angular quantities such as rotation and angular inertia, so constraints can also affect their orientation. This is explained in more detail at the end.
§Constraint functions
At the mathematical level, each constraint has a constraint function C(x)
that takes the state
of the particles as parameters and outputs a scalar value. The goal of the constraint is to move the particles
in a way that the output satisfies a constraint equation.
For equality constraints the equation takes the form C(x) = 0
. In other words, the constraint tries to
minimize the value of C(x)
to be as close to zero as possible. When the equation is true, the constraint is satisfied.
For a distance constraint, the constraint function would be C(x) = distance - rest_distance
,
because this would be zero when the distance is equal to the desired rest distance.
For inequality constraints the equation instead takes the form C(x) >= 0
. These constraints are only applied
when C(x) < 0
, which is useful for things like static friction and joint limits.
§Constraint gradients
To know what directions the particles should be moved towards, constraints compute a constraint gradient ▽C(x)
for each particle. It is a vector that points in the direction in which the constraint function value C
increases the most.
The length of the gradient indicates how much C
changes when moving the particle by one unit. This is often equal to one.
In a case where two particles are being constrained by a distance constraint, and the particles are outside of the rest distance, the gradient vector would point away from the other particle, because it would increase the distance even further.
§Lagrange multipliers
In the context of constraints, a Lagrange multiplier λ
corresponds to the signed magnitude of the constraint force.
It is a scalar value that is the same for all of the constraint’s participating particles, and it is used for computing
the correction that the constraint should apply to the particles along the gradients.
In XPBD, the Lagrange multiplier update Δλ
during a substep is computed by dividing the opposite of C
by the sum of the products of the inverse masses and squared gradient lengths plus an additional compliance term:
Δλ = -C / (sum(w_i * |▽C_i|^2) + α / h^2)
where w_i
is the inverse mass of particle i
, |▽C_i|
is the length of the gradient vector for particle i
,
α
is the constraint’s compliance (inverse of stiffness) and h
is the substep size. Using α = 0
corresponds to infinite stiffness.
The minus sign is there because the gradients point in the direction in which C
increases the most,
and we instead want to minimize C
.
Note that if the gradients are normalized, as is often the case, the squared gradient lengths can be omitted from the calculation.
§Solving constraints
Once we have computed the Lagrange multiplier λ
, we can compute the positional correction for a given particle
as the product of the Lagrange multiplier and the particle’s inverse mass and gradient vector:
Δx_i = Δλ * w_i * ▽C_i
In other words, we typically move the particle along the gradient by Δλ
proportional to the particle’s inverse mass.
§Rigid body constraints
Unlike particles, rigid bodies also have angular quantities like rotation, angular velocity and angular inertia. In addition, constraints can be applied at specific points in the body, like contact positions or joint attachment positions, which also affects the orientation.
When the constraint is not applied at the center of mass, the inverse mass in the computation of Δλ
must
be replaced with a generalized inverse mass that is essentially the effective mass when applying the constraint
at some specified position.
For a positional constraint applied at position r_i
, the generalized inverse mass computation for body i
looks like this:
w_i = 1 / m_i + (r_i x ▽C_i)^T * I_i^-1 * (r_i x ▽C_i)
where m_i
is the mass of body i
, I_i^-1
is the inverse inertia tensor, and ^T
refers to the
transpose of a vector. Note that the value of the inertia tensor depends on the orientation of the body, so it should be
recomputed each time the constraint is solved.
For an angular constraint where the gradient vector is the rotation axis, the generalized inverse mass computation instead looks like this:
w_i = ▽C_i^T * I_i^-1 * ▽C_i
Once we have computed the Lagrange multiplier update, we can apply the positional correction as shown in the previous section.
However, angular constraints are handled differently. If the constraint function’s value is the rotation angle and the gradient vector is the rotation axis, we can compute the angular correction for a given body like this:
Δq_i = 0.5 * [I_i^-1 * (r_i x (Δλ * ▽C_i)), 0] * q_i
where q_i
is the rotation of body i
and r_i
is a vector pointing from the body’s center of mass to some
attachment position.
Traits§
- An angular constraint applies an angular correction around a given axis.
- A positional constraint applies a positional correction with a given direction and magnitude at the local contact points
r1
andr2
. - A trait for all XPBD constraints.
Functions§
- Iterates through the XPBD constraints of a given type and solves them. Sleeping bodies are woken up when active bodies interact with them in a constraint.