Integration parameters
Various aspects of the physics simulation can be tuned by modifying the fields of the IntegrationParametersR3IntegrationParameters
Several parameters are expressed in a normalized form, i.e., their value is implicitly multiplied by
length_unit before being used. Therefore they don't need to be adjusted
when the world is not measured in meters.
Setting the integration parameters
The integration parameters of a physics context are stored in the
RapierContextSimulation::integration_parameters field of its RapierContextSimulation component. The integration
parameters of the default physics context can be given to the RapierPhysicsPlugin directly, together with its
configuration and the optimization strategy of its broad-phase, by
initializing it with RapierContextInitialization::InitializeDefaultRapierContext. The IntegrationParameters
structure (as well as the FrictionModel enum in 3D) is part of the prelude of bevy_rapier:
- Example 2D
- Example 3D
.add_plugins(
RapierPhysicsPlugin::<NoUserData>::default().with_custom_initialization(
RapierContextInitialization::InitializeDefaultRapierContext {
integration_parameters: IntegrationParameters {
// 100 pixels make one meter.
length_unit: 100.0,
num_solver_iterations: 8,
..default()
},
rapier_configuration: RapierConfiguration {
gravity: Vec2::new(0.0, -372.0),
..RapierConfiguration::new(100.0)
},
broad_phase_optimization_strategy:
BroadPhaseOptimizationStrategy::SubtreeOptimizer,
},
),
)
.add_plugins(
RapierPhysicsPlugin::<NoUserData>::default().with_custom_initialization(
RapierContextInitialization::InitializeDefaultRapierContext {
integration_parameters: IntegrationParameters {
num_solver_iterations: 8,
..default()
},
rapier_configuration: RapierConfiguration {
gravity: Vec3::new(0.0, -3.72, 0.0),
..RapierConfiguration::new(1.0)
},
broad_phase_optimization_strategy:
BroadPhaseOptimizationStrategy::SubtreeOptimizer,
},
),
)
The broad_phase_optimization_strategy selects how the BVH of the broad-phase is kept efficient while the colliders
move: BroadPhaseOptimizationStrategy::SubtreeOptimizer (the default) optimizes different sub-trees at each
timestep, whereas BroadPhaseOptimizationStrategy::None disables this incremental optimization (which is only useful
for debugging). It can only be selected when the physics context is created, e.g., with
RapierContextSimulation::with_broad_phase_optimization_strategy for a context you spawn yourself.
The integration parameters can then be modified at any time through the RapierContextSimulation component:
fn modify_integration_parameters(
mut contexts: Query<&mut RapierContextSimulation, With<DefaultRapierContext>>,
) -> Result {
let mut simulation = contexts.single_mut()?;
simulation.integration_parameters.num_solver_iterations = 12;
simulation.integration_parameters.warmstart_joints = true;
Ok(())
}
Setting the integration parameters
The integration parameters are owned by the world. They can be read
all at once with r3IntegrationParameters, which returns a copy of them as an R3IntegrationParameters structure,
and modified all at once by giving such a structure to r3SetIntegrationParameters. The
r3DefaultIntegrationParameters function gives the default values of every parameter, which are also the initial
values of the parameters of a new world:
- Example 2D
- Example 3D
/* Copy the integration parameters of the world, modify them, then apply them. */
R2IntegrationParameters params = r2IntegrationParameters(world);
params.numSolverIterations = 12;
params.warmstartJoints = 1;
r2SetIntegrationParameters(world, ¶ms);
/* Copy the integration parameters of the world, modify them, then apply them. */
R3IntegrationParameters params = r3IntegrationParameters(world);
params.numSolverIterations = 12;
params.warmstartJoints = 1;
r3SetIntegrationParameters(world, ¶ms);
Each parameter can also be read and modified on its own, with a dedicated pair of functions:
- Example 2D
- Example 3D
/* Each parameter can also be read or modified on its own. */
r2SetTimeStep(world, 1.0 / 120.0);
r2SetNumSolverIterations(world, 8);
r2SetWarmstartJoints(world, 1);
printf("dt = %f, %zu solver iterations\n", (double)r2TimeStep(world), r2NumSolverIterations(world));
/* Each parameter can also be read or modified on its own. */
r3SetTimeStep(world, 1.0 / 120.0);
r3SetNumSolverIterations(world, 8);
r3SetWarmstartJoints(world, 1);
printf("dt = %f, %zu solver iterations\n", (double)r3TimeStep(world), r3NumSolverIterations(world));
The parameters are validated when they are modified: an invalid value (e.g. a negative dt, or zero solver iterations) is rejected with the
R3_INVALID_ARGUMENT error, and leaves the parameters of the world unchanged. The rest of this page names the
parameters as the Rust version of Rapier does. Here are their C names:
| Parameter | Field of R3IntegrationParameters | Getter and setter |
|---|---|---|
dt | dt | r3TimeStep, r3SetTimeStep |
length_unit | lengthUnit | r3LengthUnit, r3SetLengthUnit |
max_ccd_substeps | maxCcdSubsteps | r3MaxCcdSubsteps, r3SetMaxCcdSubsteps |
min_ccd_dt | minCcdDt | r3MinCcdDt, r3SetMinCcdDt |
num_solver_iterations | numSolverIterations | r3NumSolverIterations, r3SetNumSolverIterations |
num_internal_pgs_iterations | numInternalPgsIterations | r3NumInternalPgsIterations, r3SetNumInternalPgsIterations |
num_internal_stabilization_iterations | numInternalStabilizationIterations | r3NumInternalStabilizationIterations, r3SetNumInternalStabilizationIterations |
warmstart_coefficient | warmstartCoefficient | r3WarmstartCoefficient, r3SetWarmstartCoefficient |
warmstart_joints | warmstartJoints | r3WarmstartJoints, r3SetWarmstartJoints |
friction_in_bias_pass | frictionInBiasPass | r3FrictionInBiasPass, r3SetFrictionInBiasPass |
friction_model (3D only) | frictionModel | r3FrictionModel, r3SetFrictionModel |
contact_softness | contactSoftness | r3ContactSoftness, r3SetContactSoftness |
static_contact_softness | staticContactSoftness | r3StaticContactSoftness, r3SetStaticContactSoftness |
normalized_prediction_distance | normalizedPredictionDistance | r3NormalizedPredictionDistance, r3SetNormalizedPredictionDistance |
normalized_allowed_linear_error | normalizedAllowedLinearError | r3NormalizedAllowedLinearError, r3SetNormalizedAllowedLinearError |
normalized_max_corrective_velocity | normalizedMaxCorrectiveVelocity | r3NormalizedMaxCorrectiveVelocity, r3SetNormalizedMaxCorrectiveVelocity |
normalized_max_linear_velocity | normalizedMaxLinearVelocity | r3NormalizedMaxLinearVelocity, r3SetNormalizedMaxLinearVelocity |
contact_clustering | contactClustering | r3ContactClustering, r3SetContactClustering |
contact_recycling | contactRecycling | r3ContactRecycling, r3SetContactRecycling |
normalized_contact_recycle_distance | normalizedContactRecycleDistance | r3NormalizedContactRecycleDistance, r3SetNormalizedContactRecycleDistance |
soft_bodies | softBodies | See the soft-body settings |
Setting the integration parameters
The integration parameters are owned by the physics world, and
exposed as its PhysicsWorld.integration_parameters property. This property always gives the same
IntegrationParameters object, so the parameters are modified in place by setting its properties, which have the
same names as the parameters described in this page. Assigning an IntegrationParameters to the property of the world
copies all its parameters at once, e.g., the default values given by IntegrationParameters():
# The integration parameters of the world are modified in place: the property always gives
# the same object.
params = world.integration_parameters
params.dt = 1.0 / 120.0
params.num_solver_iterations = 8
params.warmstart_joints = True
print(f"dt = {params.dt}, {world.integration_parameters.num_solver_iterations} solver iterations")
# They can also be replaced all at once, e.g., to reset them to their default values.
world.integration_parameters = rp.IntegrationParameters()
The contact softnesses (contact_softness and static_contact_softness) are given as copies: modifying the object
read from one of these properties has no effect until it is assigned back to the property. On the other hand, the
soft_bodies settings are a live view of the parameters, like the IntegrationParameters of the world itself.
Time-stepping
dt
The timestep length used for each update of the physics engine. This is the time by which the physics simulation will be advanced. The default is seconds. This typically corresponds to a refresh rate of 60Hz. Smaller timesteps yield better accuracy. Large timesteps increase the negative effect of some approximations (linearization of various parts of the equations of motion) and may result in missed collisions (because of collisions that may occur in-between timesteps for fast-moving objects).
The dt parameter is overwritten by the plugin before each update of the physics according to the TimestepMode
resource, which also selects the number of timesteps executed by each run of the physics systems:
TimestepMode::Variable { max_dt, time_scale, substeps }(the default, withmax_dt,time_scale, andsubsteps) advances the simulation by the time elapsed since the last frame multiplied bytime_scale, without exceedingmax_dt. This timestep is subdivided intosubstepstimesteps of equal length. No timestep is executed if the elapsed time is zero (e.g. on the first frame).TimestepMode::Fixed { dt, substeps }advances the simulation bydt, insubstepstimesteps of equal length, each time the physics systems run. This is the recommended mode when the physics runs in theFixedUpdateschedule.TimestepMode::Interpolated { dt, time_scale, substeps }executes as many updates of lengthdtas needed for the simulated time to keep up with the real time (possibly none), each of them advancing the simulation bydtmultiplied bytime_scaleinsubstepstimesteps. The rigid-bodies with aTransformInterpolationcomponent have theirTransforminterpolated between the last two timesteps so their motion looks smooth whatever the frame rate.
// Advance the simulation by exactly 1/60 seconds (in two substeps of 1/120 seconds) at
// each update of the schedule running the physics.
.insert_resource(TimestepMode::Fixed {
dt: 1.0 / 60.0,
substeps: 2,
})
The TimestepMode::Variable mode depends on the frame rate of your application, therefore it will not give the same
simulation results twice. Use TimestepMode::Fixed (or TimestepMode::Interpolated) if you need
determinism.
Each call to r3Step advances the simulation by exactly dt. If the physics must follow the real time while the
frame rate of your application varies, it is recommended to keep dt constant and to call r3Step as many times as
needed at each frame, i.e., to accumulate the time elapsed since the last frame, and to execute one timestep each time
this accumulated time exceeds dt. Changing dt at each frame instead makes the simulation depend on the frame rate,
therefore it will not give the same simulation results twice (see determinism).
Each call to PhysicsWorld.step advances the simulation by exactly dt. If the physics must follow the real time
while the frame rate of your application varies, it is recommended to keep dt constant and to call
PhysicsWorld.step as many times as needed at each frame, i.e., to accumulate the time elapsed since the last frame
(e.g. measured with time.perf_counter), and to execute one timestep each time this accumulated time exceeds dt.
Changing dt at each frame instead makes the simulation depend on the frame rate, therefore it will not give the same
simulation results twice (see determinism).
length_unit
The number of your own length units that make one meter. The default is , i.e., the simulation is measured in meters. Rapier is tuned for human-scale objects measured in meters, therefore a simulation measured in centimeters should set this to , and a 2D game where a typical object is 100 pixels tall should set it to as well. This scales the normalized parameters as well as various internal tolerances, and is the recommended alternative to re-tuning every threshold by hand. Learn more about this in the common mistakes page.
The length unit of the default physics context is set with
RapierPhysicsPlugin::with_length_unit, or with RapierPhysicsPlugin::pixels_per_meter in 2D. Note that it also
scales the default gravity of the physics context and the lengths drawn by the debug-renderer.
Note that the length unit doesn't scale the gravity, nor any of the values you give to the engine: they must be expressed in your own units as well, e.g., a simulation measured in centimeters needs a gravity of along the axis:
- Example 2D
- Example 3D
/* The simulation is measured in centimeters. */
r2SetLengthUnit(world, 100.0);
/* The gravity isn't scaled by the length unit: it must be given in centimeters too. */
r2SetGravity(world, r2Vector(0.0, -981.0));
/* The simulation is measured in centimeters. */
r3SetLengthUnit(world, 100.0);
/* The gravity isn't scaled by the length unit: it must be given in centimeters too. */
r3SetGravity(world, r3Vector(0.0, -981.0, 0.0));
Note that the length unit doesn't scale the gravity, nor any of the values you give to the engine: they must be expressed in your own units as well, e.g., a simulation measured in centimeters needs a gravity of along the axis:
# The simulation is measured in centimeters.
world.integration_parameters.length_unit = 100.0
# The gravity isn't scaled by the length unit: it must be given in centimeters too.
world.gravity = (0.0, -981.0, 0.0)
max_ccd_substeps
The maximum number of CCD substeps performed during one timestep. The default is . This is also the global switch of CCD: setting it to disables every form of CCD for this world, including the sweeping of the fast dynamic bodies against the fixed colliders.
min_ccd_dt
When CCD with multiple substeps is enabled, the timestep is subdivided into smaller pieces. This timestep subdivision
won't generate timestep lengths smaller than min_ccd_dt. The default is seconds.
Setting this to a large value will reduce the opportunity to performing CCD substepping. Setting this to a very small value may lead to numerical instabilities.
Constraints solver
num_solver_iterations
The number of iterations, aka. substeps, run by the constraints solver. The default is . Higher values give more accurate and more stable simulations, at the cost of performance: to is a reasonable range for demanding scenes (tall stacks, machinery with stiff joints), whereas or may be enough if performance matters more than accuracy. Note that a single rigid-body can be given additional iterations of its own, as described in the rigid-body solver settings section.
num_internal_pgs_iterations
The number of internal Projected Gauss-Seidel iterations run at each solver iteration. The default is .
num_internal_stabilization_iterations
The number of stabilization iterations run at each solver iteration. The default is . These are the iterations solving the constraints without their depenetration forces. This is what prevents the energy introduced by the position correction from remaining in the simulation.
warmstart_coefficient
Each cached impulse is multiplied by this coefficient in when it is re-used to initialize the constraints solver. The default is : it allows the convergence of the solver even when the number of iterations is small.
warmstart_joints
If enabled, the impulse-joint constraints are warm-started like the contacts are, i.e., the impulses accumulated by the
previous timestep are re-applied at the beginning of each substep instead of restarting from zero. The default is
falseFalse
friction_in_bias_pass
If enabled, friction is solved during the biased pass of each substep as well as during the unbiased one. The default
is falseFalse
friction_model (3D only)
The kind of friction constraints solved by the engine. The SimplifiedR3_FRICTION_MODEL_SIMPLIFIEDCoulombR3_FRICTION_MODEL_COULOMB
The kind of friction constraints solved by the engine, given by the FrictionModel enum. The
FrictionModel.SIMPLIFIED model (the default) solves one Coulomb constraint per group
of four contacts, plus one purely rotational "twist" constraint eliminating the angular motion in the tangent plane of
the manifold. The FrictionModel.COULOMB model solves one Coulomb constraint per contact point instead: it is more
mechanically correct but more expensive.
# Solve one Coulomb friction constraint per contact point.
world.integration_parameters.friction_model = rp.FrictionModel.COULOMB
/* Solve one Coulomb friction constraint per contact point. */
r3SetFrictionModel(world, R3_FRICTION_MODEL_COULOMB);
Contacts
contact_softness and static_contact_softness
The softness of the contact constraints, given as a natural frequency (in Hz) and a damping ratio instead of a stiffness, so it doesn't depend on the masses of the colliding bodies. Softer constraints make the contacts more compliant (meaning that they allow more penetrations when pressed on), whereas harder constraints feel more rigid and correct, but might introduce jitters if too rigid.
Both are given as an R3SpringCoefficients structure, with its natural_frequency and damping_ratio fields. The
static_contact_softness applies to the contacts where one side is a fixed rigid-body, and is stiffer than
contact_softness by default so the bodies are held firmly against the static walls and floors:
- Example 2D
- Example 3D
/* Contacts against fixed rigid-bodies are stiffer by default: make them as soft as the others. */
R2SpringCoefficients softness = r2ContactSoftness(world);
printf("%f Hz, damping ratio %f\n", (double)softness.natural_frequency, (double)softness.damping_ratio);
r2SetStaticContactSoftness(world, softness);
/* Contacts against fixed rigid-bodies are stiffer by default: make them as soft as the others. */
R3SpringCoefficients softness = r3ContactSoftness(world);
printf("%f Hz, damping ratio %f\n", (double)softness.natural_frequency, (double)softness.damping_ratio);
r3SetStaticContactSoftness(world, softness);
Both are given as a SpringCoefficients, with its natural_frequency and damping_ratio properties. The
static_contact_softness applies to the contacts where one side is a fixed rigid-body, and is stiffer than
contact_softness by default so the bodies are held firmly against the static walls and floors. Keep in mind that
these properties give copies of the coefficients, which must be assigned back once modified:
params = world.integration_parameters
# The softness is given as a copy: modify it, then assign it back.
softness = params.contact_softness
print(f"{softness.natural_frequency} Hz, damping ratio {softness.damping_ratio}")
# Contacts against fixed rigid-bodies are stiffer by default: make them as soft as the others.
params.static_contact_softness = softness
normalized_prediction_distance
The maximal distance separating two objects that will generate predictive (aka. speculative) contacts. The default is , i.e., four times the geometric slop. Generating contacts before the objects actually touch is what allows the solver to stop them exactly at the surface instead of letting them interpenetrate first.
normalized_allowed_linear_error
The geometric slop distance. The default is .
normalized_max_corrective_velocity
The maximum speed at which the solver is allowed to push penetrating objects apart. The default is . Capping this velocity is what keeps a deep penetration from being resolved explosively.
normalized_max_linear_velocity
The maximum linear velocity a rigid-body may have after each substep. The default is . This velocity cap helps with stability and CCD effectiveness.
contact_clustering
If enabled, the contact manifolds of a collider pair that share (nearly) the same normal are merged into a single
cluster manifold before the constraints are generated (default: trueTrue
When contact clustering applies, the contacts and impulses seen by the solver must be read from
ContactPair::solver_clusters instead of ContactPair::manifolds. See the
contact graph section.
When contact clustering applies, the impulses of the contact points of ContactPair.manifolds (which are the points
of the geometric contact manifolds) don't reflect the impulses applied by the solver. Read the impulses from the totals
of the ContactPair instead (e.g. ContactPair.total_impulse() and ContactPair.total_impulse_magnitude()), which
account for the clusters. See the contact graph section.
When contact clustering applies, the impulses of the contact points given by r3ContactPoints (which are the
points of the geometric contact manifolds) don't reflect the impulses applied by the solver. Read the impulses from
the totals of the R3ContactPair instead (e.g. total_impulse and max_impulse), which account for the clusters.
See the contact graph section.
contact_recycling and normalized_contact_recycle_distance
If enabled, a contact pair which relative position moved less than the recycle distance since its last full update
keeps its existing contact points instead of recomputing them (default: trueTrue
Soft-bodies
soft_bodies
The settings shared by every soft-body of the world. They are detailed in the
soft-body settings section.RapierContextSimulation::integration_parameters field like the other integration
parameters.softBodies field of the R3IntegrationParameters like the other integration parameters, or with their dedicated functions, e.g., r3SoftBodiesSetMaxExtraSubsteps.soft_bodies property of the IntegrationParameters as a live view, so they are modified in place like the other integration parameters (its copy method gives a detached copy).