Skip to main content

Joints

One of the most appealing features of a physics engine is to simulate articulations. Articulations, aka. joints, allow the restriction of the motion of one body relative to another body. For example, one well-known joint is the ball-in-socket joint also known as the spherical joint: it allows one object to rotate freely with regard to the other but not to translate. This is typically used to simulate shoulders of a ragdoll.

Basic concepts​

Joints can be modeled in various ways but let's talk about the concept of Degrees Of Freedom (DOF) first. In 3D, a rigid-body is capable of translating along the 3 coordinates axes x\mathbf{x}, y\mathbf{y} and z\mathbf{z}, and to rotate along those three axes as well. Therefore, a rigid-body is said to have 3 translational DOF and 3 rotational DOF. We can also say a 3D rigid-body has a total of 6 DOF. The 2D case is similar but with less possibilities of movements: a 2D rigid-body has 2 translational DOF and only 1 rotational DOF (which forms a total of 3 DOF). The number of relative DOF of a body wrt. another body is the number of possible relative translations and rotations.

The goal of a joint is to reduce the number of DOF a body has. For example, the aforementioned spherical joint removes all relative translations between two bodies. Therefore, it allows only the 3 rotational DOF in 3D simulations or the 1 rotational DOF in 2D simulations. Other joints exist allowing other combinations of relative DOF. Note that because there are less possible motions in 2D, some joints are only defined in 3D. This is illustrated by empty cells in the following table for joints that are not defined in 2D:

JointAllowed DOF in 2DAllowed DOF in 3DRapier support
Fixed jointNoneNoneYes
Free jointAllAllThrough GenericJointa generic joint
Prismatic joint1 Translation1 TranslationYes
Revolute joint1 Rotation1 RotationYes
Spherical joint1 Rotation3 RotationsYes
Cartesian joint2 Translations3 TranslationsThrough GenericJointa generic joint
Planar joint2 Translations + 1 RotationThrough GenericJointa generic joint
Cylindrical joint1 Translation + 1 Rotation (along the same axis)Through GenericJointa generic joint
Pin-slot joint1 Translation + 1 Rotation (along different axes)Through GenericJointa generic joint
Rectangular joint2 TranslationsThrough GenericJointa generic joint
Universal joint2 RotationsThrough GenericJointa generic joint

Joints must be inserted into the physics world either as an impulse joint with PhysicsWorld::insert_impulse_joint, or as a multibody joint with PhysicsWorld::insert_multibody_joint (which store them into the PhysicsWorld::impulse_joints and PhysicsWorld::multibody_joints sets respectively). The difference between those is explained in next section.

info

In 2D, bevy_rapier also exposes the pin-slot joint (aka. groove joint) with the dedicated PinSlotJoint type (built with PinSlotJointBuilder). It allows the relative rotation, and the relative translation along one axis.

A joint is created by adding either an ImpulseJoint or a MultibodyJoint component to an entity. The difference between those is explained in next section. The first rigid-body attached by the joint is the one of the entity given as the parent of the joint, and the second rigid-body is the one of the entity containing the joint component (or of its parent entity). Similarly to the attachment of multiple colliders to a single rigid-body, multiple impulse joints can be attached to the same rigid-body by adding them to child entities:

/* Attach two joints to the same rigid-body using child entities. */
let body1 = commands.spawn(RigidBody::Fixed).id();
let body2 = commands.spawn(RigidBody::Fixed).id();
commands
.spawn((RigidBody::Dynamic, Collider::cuboid(0.5, 0.5)))
.with_children(|children| {
// The second rigid-body of these joints is the one of the parent entity.
let joint1 = RevoluteJointBuilder::new().local_anchor2(Vec2::new(-1.0, 0.0));
let joint2 = RevoluteJointBuilder::new().local_anchor2(Vec2::new(1.0, 0.0));
children.spawn(ImpulseJoint::new(body1, joint1));
children.spawn(ImpulseJoint::new(body2, joint2));
});

Joints must be inserted into the physics world either as an impulse joint with World.createImpulseJoint, or as a multibody joint with World.createMultibodyJoint (which store them into the World.impulseJoints and World.multibodyJoints sets respectively). The difference between those is explained in next section.

A joint is described by an R3JointDesc which must be initialized by one of the constructors of the joint types detailed in this page: r3FixedJointDesc, r3SphericalJointDesc (3D only), r3RevoluteJointDesc, and r3PrismaticJointDesc. A few other joint types have their own constructors too: r3RopeJointDesc, r3SpringJointDesc, and r2PinSlotJointDesc (2D only), and any other combination of free DOF can be described by a generic joint. Then any of the fields of the description can be modified before the joint is inserted into the world either as an impulse joint with r3InsertImpulseJoint, or as a multibody joint with r3InsertMultibodyJoint, given the handles of the two rigid-bodies it attaches. They return an R3ImpulseJointHandle and an R3MultibodyJointHandle respectively. The difference between those is explained in next section.

As the Python bindings are 3D only, only the 3D column of this table is relevant. A joint is created by a builder obtained from the static builder method of its class (e.g., FixedJoint.builder(), or RevoluteJoint.builder(axis=...)), each method of the builder returning a new builder with the corresponding property set, so they can be chained. Joints must be inserted into the physics world either as an impulse joint with world.impulse_joints.insert, or as a multibody joint with world.multibody_joints.insert, given the handles of the two rigid-bodies it attaches (these methods accept the builder directly, as well as the joint returned by its build method). They return an ImpulseJointHandle and a MultibodyJointHandle respectively. The difference between those is explained in next section. Note that removing a rigid-body with PhysicsWorld.remove_body also removes all the joints attached to it.

Fixed joint​

A fixed joint ensures that two rigid-bodies don't move relative to each other. Fixed joints are characterized by one local frame (represented by a PoseR3Pose) on each rigid-body. The fixed-joint makes these frames coincide in world-space.

A fixed joint ensures that two rigid-bodies don't move relative to each other. Fixed joints are characterized by one local frame (represented by an Isometry3) on each rigid-body. The fixed-joint makes these frames coincide in world-space.

info

Attaching multiple colliders to a single rigid-body will have the same effect as using one rigid-body per collider and attaching them with a fixed joint. However the multi-collider approach will be much more efficient and numerically efficient than the joint approach. So a fixed-joint should only be used when the multi-collider doesn't fit your use-case (for example if you want to read the force applied by the joint in order to break it dynamically).

// NOTE: setting the local anchors sets the translation part of the local frames.
let joint = FixedJointBuilder::new()
.local_anchor1(Vector::new(0.0, 1.0))
.local_anchor2(Vector::new(0.0, -3.0));
world.insert_impulse_joint(body_handle1, body_handle2, joint);
let joint = FixedJointBuilder::new().local_anchor1(Vec2::new(0.0, -20.0));
commands
.spawn(RigidBody::Dynamic)
.insert(Collider::cuboid(5f32, 5f32))
.insert(ImpulseJoint::new(parent_entity, joint));
let params = RAPIER.JointData.fixed({ x: 0.0, y: 0.0 }, 0.0, { x: 0.0, y: -2.0 }, 0.0);
let joint = world.createImpulseJoint(params, body1, body2, true);

The local frames are the localFrame1 and localFrame2 fields of the joint description, expressed in the local-space of the first and second rigid-body respectively. Their translation parts are the local anchors of the joint, which can also be set with r3JointDesc_SetLocalAnchor1 and r3JointDesc_SetLocalAnchor2:

// NOTE: the local anchors are the translation parts of the local frames.
R2JointDesc joint = r2FixedJointDesc();
joint.localFrame1.translation = r2Vector(0.0, 1.0);
joint.localFrame2.translation = r2Vector(0.0, -3.0);
r2InsertImpulseJoint(body_handle1, body_handle2, &joint);

The local frames are set with the local_frame1 and local_frame2 methods of the builder, expressed in the local-space of the first and second rigid-body respectively. Their translation parts are the local anchors of the joint, which can also be set with local_anchor1 and local_anchor2:

# NOTE: setting the local anchors sets the translation part of the local frames.
joint = (
rp.FixedJoint.builder()
.local_anchor1((0.0, 1.0, 0.0))
.local_anchor2((0.0, -3.0, 0.0))
)
world.impulse_joints.insert(body_handle1, body_handle2, joint)

Spherical joint​

The spherical joint ensures that two points on the local-spaces of two rigid-bodies always coincide (it prevents any relative translational motion at this points). This is typically used to simulate ragdolls arms, pendulums, etc. They are characterized by one local anchor on each rigid-body. Each anchor represents the location of the points that need to coincide on the local-space of each rigid-body.

let joint = SphericalJointBuilder::new()
.local_anchor1(Vector::new(0.0, 0.0, 1.0))
.local_anchor2(Vector::new(0.0, 0.0, -3.0));
world.insert_impulse_joint(body_handle1, body_handle2, joint);
let joint = SphericalJointBuilder::new()
.local_anchor1(Vec3::new(0.0, 0.0, 1.0))
.local_anchor2(Vec3::new(0.0, 0.0, -3.0));
commands
.spawn(RigidBody::Dynamic)
.insert(Collider::cuboid(0.5, 0.5, 0.5))
.insert(ImpulseJoint::new(parent_entity, joint));
let params = RAPIER.JointData.spherical({ x: 0.0, y: 0.0, z: 1.0 }, { x: 0.0, y: 0.0, z: -3.0 });
let joint = world.createImpulseJoint(params, body1, body2, true);
R3JointDesc joint = r3SphericalJointDesc();
joint.localFrame1.translation = r3Vector(0.0, 0.0, 1.0);
joint.localFrame2.translation = r3Vector(0.0, 0.0, -3.0);
r3InsertImpulseJoint(body_handle1, body_handle2, &joint);
joint = (
rp.SphericalJoint.builder()
.local_anchor1((0.0, 0.0, 1.0))
.local_anchor2((0.0, 0.0, -3.0))
)
world.impulse_joints.insert(body_handle1, body_handle2, joint)
note

In 2D, revolute joints and spherical joints are the same thing. Therefore, there is no SphericalJoint typer2SphericalJointDesc constructor in 2D (use the RevoluteJoint typer2RevoluteJointDesc constructor instead).

Revolute joint​

The revolute joint prevents any relative movement between two rigid-bodies, except for relative rotations along one axis. This is typically used to simulate wheels, fans, etc. They are characterized by one local anchor as well as one local axis on each rigid-body.

let joint = RevoluteJointBuilder::new()
.local_anchor1(Vector::new(0.0, 1.0))
.local_anchor2(Vector::new(0.0, -3.0));
world.insert_impulse_joint(body_handle1, body_handle2, joint);
let joint = RevoluteJointBuilder::new()
.local_anchor1(Vec2::new(0.0, 1.0))
.local_anchor2(Vec2::new(0.0, -5.0));
commands
.spawn(RigidBody::Dynamic)
.insert(Collider::cuboid(5f32, 5f32))
.insert(ImpulseJoint::new(parent_entity, joint));
let params = RAPIER.JointData.revolute({ x: 0.0, y: 1.0 }, { x: 0.0, y: -3.0 });
let joint = world.createImpulseJoint(params, body1, body2, true);

In 3D, the local axis is given to r3RevoluteJointDesc: it sets the rotation of both local frames so that their x\mathbf{x} axis is aligned with this axis. Different axes on each rigid-body can then be set with r3JointDesc_SetLocalAxis1 and r3JointDesc_SetLocalAxis2. In 2D, the rotation axis is implicit, so r2RevoluteJointDesc takes no argument:

R2JointDesc joint = r2RevoluteJointDesc();
joint.localFrame1.translation = r2Vector(0.0, 1.0);
joint.localFrame2.translation = r2Vector(0.0, -3.0);
r2InsertImpulseJoint(body_handle1, body_handle2, &joint);

The local axis is given to RevoluteJoint.builder (it is required since the bindings are 3D only): it sets the rotation of both local frames so that their x\mathbf{x} axis is aligned with this axis. Different axes on each rigid-body can then be set with the local_axis1 and local_axis2 methods of the builder. The current angle of the joint can be computed from the rotations of its two rigid-bodies with RevoluteJoint.angle:

x = (1.0, 0.0, 0.0)
joint = (
rp.RevoluteJoint.builder(axis=x)
.local_anchor1((0.0, 0.0, 1.0))
.local_anchor2((0.0, 0.0, -3.0))
)
world.impulse_joints.insert(body_handle1, body_handle2, joint)

Prismatic joint​

The prismatic joint prevents any relative movement between two rigid-bodies, except for relative translations along one axis. It is characterized by one local anchor as well as one local axis on each rigid-body. In 3D, an optional local tangent axis can be specified for each rigid-body. If a local tangent axis equal to zero is specified, it will be computed automatically. Setting this tangent axis lets you control the fixed relative orientation of the rigid-bodies.The local axis is the x\mathbf{x} axis of the local frames. In 3D, the rotation of the local frames around this axis lets you control the fixed relative orientation of the rigid-bodies.

The prismatic joint supports the application of joint limits. This will restrict the relative distance between the rigid-body anchors (along the free joint axis) to remain in the specified range PrismaticJoint::limitsset with r3JointDesc_SetLimits. The signed distance is computed as (anchor2 - anchor1).dot(axis1).

The prismatic joint prevents any relative movement between two rigid-bodies, except for relative translations along one axis. It is characterized by one local anchor as well as one local axis on each rigid-body. The local axis given to PrismaticJoint.builder is the x\mathbf{x} axis of both local frames, and different axes on each rigid-body can be set with the local_axis1 and local_axis2 methods of the builder. The rotation of the local frames around this axis is computed automatically: use a generic joint with explicit local frames if you need to control the fixed relative orientation of the rigid-bodies.

The prismatic joint supports the application of joint limits. This will restrict the relative distance between the rigid-body anchors (along the free joint axis) to remain in the range set with the limits(min, max) method of the builder (or with PrismaticJoint.set_limits). The signed distance is computed as (anchor2 - anchor1).dot(axis1).

let x = Vector::X;
let mut joint = PrismaticJointBuilder::new(x)
.local_anchor1(Vector::new(0.0, 1.0))
.local_anchor2(Vector::new(0.0, -3.0))
.limits([-2.0, 5.0]);
world.insert_impulse_joint(body_handle1, body_handle2, joint);
let joint = PrismaticJointBuilder::new(Vec2::X)
.local_anchor1(Vec2::new(0.0, 1.0))
.local_anchor2(Vec2::new(0.0, -3.0))
.limits([-2.0, 5.0]);
commands
.spawn(RigidBody::Dynamic)
.insert(Collider::cuboid(5f32, 5f32))
.insert(ImpulseJoint::new(parent_entity, joint));
let x = { x: 1.0, y: 0.0 };
let params = RAPIER.JointData.prismatic({ x: 0.0, y: 0.0 }, x, { x: 0.0, y: -3.0 });
params.limitsEnabled = true;
params.limits = [-2.0, 5.0];
let joint = world.createImpulseJoint(params, body1, body2, true);

The limits are set along one of the axes of the joint, identified by its index: R3_AXIS_LIN_X, R3_AXIS_LIN_Y, and R3_AXIS_LIN_Z for the translations, then R3_AXIS_ANG_X, R3_AXIS_ANG_Y, and R3_AXIS_ANG_Z for the rotations (in 2D, the only rotation is R2_AXIS_ANG_X, i.e., the rotation around z\mathbf{z}). For a prismatic joint, the free axis is R3_AXIS_LIN_X. The function r3JointDesc_SetLimits also enables the limits of this axis in the limitAxes bitmask of the description. Limits apply to any free axis of any joint: angular limits of a revolute joint are set on its R3_AXIS_ANG_X axis (in radians).

R2Vector x = r2Vector(1.0, 0.0);
R2JointDesc joint = r2PrismaticJointDesc(x);
joint.localFrame1.translation = r2Vector(0.0, 1.0);
joint.localFrame2.translation = r2Vector(0.0, -3.0);
// The free axis of a prismatic joint is the X axis of its local frames.
r2JointDesc_SetLimits(&joint, R2_AXIS_LIN_X, -2.0, 5.0);
r2InsertImpulseJoint(body_handle1, body_handle2, &joint);
x = (1.0, 0.0, 0.0)
joint = (
rp.PrismaticJoint.builder(axis=x)
.local_anchor1((0.0, 0.0, 1.0))
.local_anchor2((0.0, 0.0, -3.0))
.limits(-2.0, 5.0)
)
world.impulse_joints.insert(body_handle1, body_handle2, joint)

Limits apply to any free axis of any joint: the angular limits of a revolute joint are set the same way (in radians). The joints with several free axes (the spherical joint and the generic joints) take the axis the limits apply to as their first argument: JointAxis.LIN_X, JointAxis.LIN_Y, and JointAxis.LIN_Z for the translations, then JointAxis.ANG_X, JointAxis.ANG_Y, and JointAxis.ANG_Z for the rotations (the free axis of a prismatic joint being JointAxis.LIN_X, and the one of a revolute joint being JointAxis.ANG_X). The limits of an axis can be read with the limits method of the joint, which returns a JointLimits (or None if they are not enabled).

Joint motors​

Spherical, revolute, and prismatic joints support joint motors.

Motors allow you to make the linked rigid-bodies move relative to one another, along the free degrees of freedom left by the joint, as if a motor was pushing them. The joint motor is simulated with a PD controller (Proportional Derivative controller) where you can set a target relative velocity along the free degrees of freedom as well as a target position. The stiffness of the PD controller controls the strength of the force that will be applied to make the bodies reach the target relative positions along the free DOFs. The damping of the PD controller controls the strength of the force that will be applied to make the bodies reach the target relative velocities along the free DOFs. All joints supporting motors have the following methods:The motor of each free axis is configured by the following functions (where axis is the index of the axis, e.g., R3_AXIS_LIN_X):

  • configure_motor_position(target_pos, stiffness, damping) configureMotorPosition(targetPos, stiffness, damping) r3JointDesc_SetMotorPosition(desc, axis, target_pos, stiffness, damping)set_motor_position(target_pos, stiffness, damping): this tells the joint motor that we want the relative position of the rigid-bodies along the free DOFs to be equal to target_pos target_pos target_postarget_pos, and that the relative velocity when it reaches that position should be zero.
  • configure_motor_velocity(target_vel, damping)configureMotorVelocity(targetVel, damping)r3JointDesc_SetMotorVelocity(desc, axis, target_vel, damping)set_motor_velocity(target_vel, factor): this tells the joint motor that we want the relative relative velocity of the rigid-bodies along the free DOFs to be equal to target_veltargeVeltarget_veltarget_vel. There is no restriction on the relative position along the free DOF.
  • configure_motor(target_pos, target_vel, stiffness, damping)configureMotor(targetPos, targetVel, stiffness, damping)r3JointDesc_SetMotor(desc, axis, target_pos, target_vel, stiffness, damping)set_motor(target_pos, target_vel, stiffness, damping): this lets you control all the parameters of the motor's spring-like equation. This should be used only if the other configuration methods are not flexible enough.
  • configure_motor_model(model)configureMotorModel(model)r3JointDesc_SetMotorModel(desc, axis, model)set_motor_model(model): this selects the mathematical model for the motor's controller. All the available models use a spring-like equation. See the API documentation of MotorModel for details.With R3_MOTOR_ACCELERATION_BASED (the default), the stiffness and damping are scaled by the mass With MotorModel.ACCELERATION_BASED (the default), the stiffness and damping are scaled by the mass of the rigid-bodies (which makes them easier to tune). With MotorModel.FORCE_BASED, they produce absolute forces. of the rigid-bodies (which makes them easier to tune). With R3_MOTOR_FORCE_BASED, they produce absolute forces.

It is also possible to configure the maximum impulse applied by the motor with r3JointDesc_SetMotorMaxForce with set_motor_max_force: this limits the maximum force/torque the motor is able to deliver. The following examples show the configuration of the joint motor for a prismatic joint:

let x = Vector::X;
let mut joint = PrismaticJointBuilder::new(x)
.local_anchor1(Vector::new(0.0, 1.0))
.local_anchor2(Vector::new(0.0, -3.0))
.motor_velocity(1.0, 0.5);
world.insert_impulse_joint(body_handle1, body_handle2, joint);
let joint = PrismaticJointBuilder::new(Vec2::X)
.local_anchor1(Vec2::new(0.0, 1.0))
.local_anchor2(Vec2::new(0.0, -3.0))
.motor_velocity(1.0, 1.0);
commands
.spawn(RigidBody::Dynamic)
.insert(Collider::cuboid(5f32, 5f32))
.insert(ImpulseJoint::new(parent_entity, joint));
let x = { x: 1.0, y: 0.0 };
let params = RAPIER.JointData.prismatic({ x: 0.0, y: 0.0 }, { x: 0.0, y: -3.0 }, x);
let joint = world.createImpulseJoint(params, body1, body2, true);
(joint as RAPIER.PrismaticImpulseJoint).configureMotorVelocity(1.0, 0.5);
R2Vector x = r2Vector(1.0, 0.0);
R2JointDesc joint = r2PrismaticJointDesc(x);
joint.localFrame1.translation = r2Vector(0.0, 1.0);
joint.localFrame2.translation = r2Vector(0.0, -3.0);
r2JointDesc_SetMotorVelocity(&joint, R2_AXIS_LIN_X, 1.0, 0.5);
R2ImpulseJointHandle joint_handle = r2InsertImpulseJoint(body_handle1, body_handle2, &joint);

Each of these functions also enables the motor of the given axis in the motorAxes bitmask of the description.

x = (1.0, 0.0, 0.0)
joint = (
rp.PrismaticJoint.builder(axis=x)
.local_anchor1((0.0, 0.0, 1.0))
.local_anchor2((0.0, 0.0, -3.0))
.motor_velocity(1.0, 0.5)
)
joint_handle = world.impulse_joints.insert(body_handle1, body_handle2, joint)

The builders have the same methods as the joints, without the set_ prefix (e.g., motor_velocity instead of set_motor_velocity). Like for the limits, the spherical joint and the generic joints take the axis of the motor as their first argument, e.g., set_motor_velocity(JointAxis.ANG_X, target_vel, factor). The current configuration of a motor can be read with the motor method of the joint, which returns a JointMotor (or None if the motor isn't enabled).

Modifying joints​

After its creation, an impulse joint can be modified through its ImpulseJoint component:

  • Modifying ImpulseJoint::parent re-attaches the joint to the rigid-body of the new parent entity.
  • Adding the ImpulseJointDisabled component disables the joint: it stays attached to its rigid-bodies but is ignored by the constraints solver until this component is removed.
  • The impulses applied by the joint during the last simulation step can be read from its ImpulseJointImpulses component (automatically added alongside the ImpulseJoint). This can be used to break a joint dynamically, by removing its ImpulseJoint component once these impulses exceed some threshold.
/* Re-attach the joints to another rigid-body inside of a system. */
fn reattach_joints(
mut joints: Query<&mut ImpulseJoint>,
new_parent: Query<Entity, With<NewParent>>,
) {
let Ok(new_parent) = new_parent.single() else {
return;
};
for mut joint in joints.iter_mut() {
joint.parent = new_parent;
}
}

/* Disable the joints inside of a system. */
fn disable_joints(mut commands: Commands, joints: Query<Entity, With<ImpulseJoint>>) {
for entity in joints.iter() {
// Removing this component enables the joint again.
commands.entity(entity).insert(ImpulseJointDisabled);
}
}

/* Break the joints applying a large impulse inside of a system. */
fn break_joints(mut commands: Commands, joints: Query<(Entity, &ImpulseJointImpulses)>) {
for (entity, impulses) in joints.iter() {
// The impulse applied by the joint along its locked translations during the last step.
if impulses.linear.length() > 100.0 {
commands.entity(entity).remove::<ImpulseJoint>();
}
}
}

Generic joints​

All the joint constructors only initialize the fields of the same R3JointDesc structure, so any other joint can be described by modifying these fields directly. The main one is the lockedAxes bitmask: it contains one bit per axis of the joint (1 << R3_AXIS_LIN_X for the translation along x\mathbf{x}, etc.), each set bit removing the corresponding relative DOF. For example, lockedAxes is R3_JOINT_FIXED_AXES for a fixed joint, and R3_JOINT_REVOLUTE_AXES for a revolute joint. A description initialized by r3DefaultJointDesc locks nothing, which is a Free joint until its lockedAxes are set. The following fields of the description can be modified for any joint:

  • lockedAxes, limitAxes, and motorAxes: the bitmasks of the locked axes, and of the axes with enabled limits and motors. The limits and motors themselves are stored in the limits and motors arrays (one entry per axis).
  • contactsEnabled: whether the colliders of the two rigid-bodies attached by the joint can collide with each other.
  • softness: the natural frequency and damping ratio of the joint constraints. Lowering them makes the joint springy instead of rigid. Note that this has no effect on the locked axes of a multibody joint, which can't be violated.
  • enabled: whether the joint is enabled. A disabled joint stays attached to its rigid-bodies but is ignored by the constraints solver.
  • userData: 128 bits of data freely available to the application.
// A cartesian joint: only the rotation is locked.
R2JointDesc joint = r2DefaultJointDesc();
joint.lockedAxes = 1 << R2_AXIS_ANG_X;
joint.localFrame1.translation = r2Vector(0.0, 1.0);
joint.localFrame2.translation = r2Vector(0.0, -3.0);
// Limit the relative translation along the X axis.
r2JointDesc_SetLimits(&joint, R2_AXIS_LIN_X, -2.0, 5.0);
// Allow contacts between the colliders of the two rigid-bodies.
// Default: 1
joint.contactsEnabled = 1;
// Make the locked axes springy instead of rigid.
// Default: a natural frequency of 1.0e6 Hz and a damping ratio of 1.0.
joint.softness.natural_frequency = 10.0;
joint.softness.damping_ratio = 1.0;
r2InsertImpulseJoint(body_handle1, body_handle2, &joint);

The coupledAxes bitmask changes the interpretation of the limits and motors of the coupled axes: instead of applying to each axis independently, they apply to the combined displacement along all the coupled linear axes (resp. angular axes). In this case, only the limit and motor of the first coupled linear axis (resp. angular axis) are used. This is, for example, how the rope joint limits the distance between its anchors:

// The relative translation along all the axes is free, but its length is limited to 2.0 (like a rope).
R2JointDesc joint = r2DefaultJointDesc();
joint.coupledAxes = (1 << R2_AXIS_LIN_X) | (1 << R2_AXIS_LIN_Y);
// Only the limits of the first coupled axis are used.
r2JointDesc_SetLimits(&joint, R2_AXIS_LIN_X, 0.0, 2.0);
r2InsertImpulseJoint(body_handle1, body_handle2, &joint);

The rope joint and the spring joint are both built this way, and have their own constructors. In 2D, the pin-slot joint (aka. groove joint) is also available: it allows the relative rotation, and the relative translation along one axis:

// The distance between the anchors can't exceed 2.0.
R2JointDesc rope = r2RopeJointDesc(2.0);
r2InsertImpulseJoint(body_handle1, body_handle2, &rope);
// A spring with a rest length of 2.0, a stiffness of 10.0, and a damping of 0.5.
R2JointDesc spring = r2SpringJointDesc(2.0, 10.0, 0.5);
r2InsertImpulseJoint(body_handle1, body_handle2, &spring);
// A pin-slot joint: free rotation, and free translation along the X axis.
R2JointDesc pin_slot = r2PinSlotJointDesc(r2Vector(1.0, 0.0));
r2InsertImpulseJoint(body_handle1, body_handle2, &pin_slot);

Modifying joints​

After its insertion, the configuration of an impulse joint can be read with r3ImpulseJoint_Desc, and replaced as a whole with r3ImpulseJoint_SetDesc. Each setter of R3JointDesc also has an r3ImpulseJoint_ counterpart modifying the inserted joint directly, e.g., r3ImpulseJoint_SetMotorVelocity, or r3ImpulseJoint_SetLimits. The last argument of these functions indicates whether the rigid-bodies attached by the joint must be woken up. The rigid-bodies attached by an impulse joint are given by r3ImpulseJoint_Bodies, its user data by r3ImpulseJoint_UserData, and a joint is removed with r3RemoveImpulseJoint (resp. r3RemoveMultibodyJoint for a multibody joint). The number of joints of the world is given by r3ImpulseJointCount and r3MultibodyJointCount, and their handles can be listed with r3ImpulseJointHandles and r3MultibodyJointHandles.

// Change the motor of an existing joint (the last argument wakes up its rigid-bodies).
r2ImpulseJoint_SetMotorVelocity(joint_handle, R2_AXIS_LIN_X, 2.0, 0.5, 1);
// Read a copy of the whole joint description, modify it, then apply it back.
R2JointDesc desc = r2ImpulseJoint_Desc(joint_handle);
desc.contactsEnabled = 0;
r2ImpulseJoint_SetDesc(joint_handle, &desc, 1);
// Disable the joint: it stays attached to its rigid-bodies but is ignored by the solver.
r2ImpulseJoint_SetEnabled(joint_handle, 0, 1);
// The rigid-bodies attached by the joint.
R2JointBodies bodies = r2ImpulseJoint_Bodies(joint_handle);
// Remove the joint, waking up its rigid-bodies.
r2RemoveImpulseJoint(joint_handle, 1);

The impulses applied by an impulse joint during the last simulation step are given by r3ImpulseJoint_Impulses: the linear and angular impulses applied along the locked axes, as well as the impulses applied by the limit and the motor of each axis (the limits and motors arrays). They are expressed along the axes of the joint frame, and are zero before the first simulation step of the joint. This can be used to break a joint dynamically, by removing it once these impulses exceed some threshold:

const R2Real max_impulse = 10.0;
r2Step(world, NULL, NULL);
// The impulses applied by the joint during the last step.
R2JointImpulses impulses = r2ImpulseJoint_Impulses(joint_handle);
// Break the joint if it has to pull its rigid-bodies too strongly to keep them together.
if (r2VectorLength(impulses.linear) > max_impulse) {
r2RemoveImpulseJoint(joint_handle, 1);
}

Generic joints​

All the joints of this page are specializations of the GenericJoint class (given by their data property), so any other combination of free DOF can be described by a generic joint. Its builder is obtained with GenericJoint.builder(locked_axes=...), where locked_axes is a JointAxesMask with one flag per axis of the joint (JointAxesMask.LIN_X for the translation along x\mathbf{x}, etc.) combined with the | operator, each flag removing the corresponding relative DOF. For example, the locked axes of a fixed joint are JointAxesMask.LOCKED_FIXED_AXES, and the ones of a revolute joint are JointAxesMask.LOCKED_REVOLUTE_AXES. By default, nothing is locked, which is a Free joint. On top of the local frames, limits, and motors, the following properties can be set by the builder of any joint, and read or modified afterward on its GenericJoint:

  • contacts_enabled: whether the colliders of the two rigid-bodies attached by the joint can collide with each other.
  • softness: the natural frequency and damping ratio of the joint constraints (as a SpringCoefficients). Lowering them makes the joint springy instead of rigid. Note that this has no effect on the locked axes of a multibody joint, which can't be violated.
  • user_data: an integer (of up to 128 bits) freely available to the application.

The axes with enabled limits and motors are given by the GenericJoint.limit_axes and GenericJoint.motor_axes masks.

# A cylindrical joint: only the translation along, and the rotation around, the X axis are free.
locked_axes = rp.JointAxesMask.LIN_Y | rp.JointAxesMask.LIN_Z | rp.JointAxesMask.ANG_Y | rp.JointAxesMask.ANG_Z
# Make the locked axes springy instead of rigid.
# Default: a natural frequency of 1.0e6 Hz and a damping ratio of 1.0.
softness = rp.SpringCoefficients(natural_frequency=10.0, damping_ratio=1.0)
joint = (
rp.GenericJoint.builder(locked_axes=locked_axes)
.local_anchor1((0.0, 0.0, 1.0))
.local_anchor2((0.0, 0.0, -3.0))
# Limit the relative translation along the X axis.
.limits(rp.JointAxis.LIN_X, -2.0, 5.0)
# Allow contacts between the colliders of the two rigid-bodies.
# Default: True
.contacts_enabled(True)
.softness(softness)
# An integer (up to 128 bits) freely available to the application.
# Default: 0
.user_data(42)
)
world.impulse_joints.insert(body_handle1, body_handle2, joint)

The coupled_axes mask changes the interpretation of the limits and motors of the coupled axes: instead of applying to each axis independently, they apply to the combined displacement along all the coupled linear axes (resp. angular axes). In this case, only the limit and motor of the first coupled linear axis (resp. angular axis) are used. This is, for example, how the rope joint limits the distance between its anchors:

# The relative translation along all the axes is free, but its length is limited to 2.0 (like a rope).
joint = (
rp.GenericJoint.builder()
.coupled_axes(rp.JointAxesMask.LIN_AXES)
# Only the limits of the first coupled axis are used.
.limits(rp.JointAxis.LIN_X, 0.0, 2.0)
)
world.impulse_joints.insert(body_handle1, body_handle2, joint)

The rope joint and the spring joint are both built this way, and have their own classes: RopeJoint and SpringJoint. The model of the spring can be selected with the spring_model method of its builder (with the same MotorModel values as the joint motors):

# The distance between the anchors can't exceed 2.0.
rope = rp.RopeJoint.builder(max_distance=2.0)
world.impulse_joints.insert(body_handle1, body_handle2, rope)
# A spring with a rest length of 2.0, a stiffness of 10.0, and a damping of 0.5.
spring = rp.SpringJoint.builder(rest_length=2.0, stiffness=10.0, damping=0.5)
world.impulse_joints.insert(body_handle1, body_handle2, spring)

Modifying joints​

After its insertion, an impulse joint is accessed with world.impulse_joints[handle] (or with world.impulse_joints.get(handle), which returns None instead of raising an InvalidHandle exception if the handle is invalid). This returns an ImpulseJoint, which is a live view of the joint: its data property is the GenericJoint of the joint, and modifying it (or assigning a whole new GenericJoint to it) modifies the joint inserted into the world, and wakes up its rigid-bodies. Setting GenericJoint.set_enabled(False) disables the joint: it stays attached to its rigid-bodies but is ignored by the constraints solver. The rigid-bodies attached by the joint are given by ImpulseJoint.body1 and ImpulseJoint.body2, and can be replaced with world.impulse_joints.set_bodies. Finally, a joint is removed with world.impulse_joints.remove (resp. world.multibody_joints.remove for a multibody joint). Iterating through world.impulse_joints yields the handle and the view of each impulse joint of the world, and the handles of the joints attached to a rigid-body are given by world.impulse_joints.attached_joints:

# A live view of the joint: modifying it modifies the joint inserted into the world (and wakes up its rigid-bodies).
joint = world.impulse_joints[joint_handle]
# Change the motor of the joint.
joint.data.set_motor_velocity(rp.JointAxis.LIN_X, 2.0, 0.5)
# Forbid contacts between the colliders of its rigid-bodies.
joint.data.contacts_enabled = False
# Disable the joint: it stays attached to its rigid-bodies but is ignored by the solver.
joint.data.set_enabled(False)
# The rigid-bodies attached by the joint.
print("The joint attaches", joint.body1, "and", joint.body2)
# Attach the joint to other rigid-bodies (its handle doesn't change).
world.impulse_joints.set_bodies(joint_handle, body_handle2, body_handle1)
# Iterate through all the impulse joints of the world.
for handle, joint in world.impulse_joints:
print(handle, "attaches", joint.body1, "and", joint.body2)
# The handles of all the impulse joints attached to a rigid-body.
attached_joints = list(world.impulse_joints.attached_joints(body_handle1))
# Remove the joint, waking up its rigid-bodies.
world.impulse_joints.remove(joint_handle)

The impulses applied by an impulse joint during the last simulation step are given by ImpulseJoint.impulses (a list of 6 values: the 3 linear impulses, then the 3 angular impulses). This can be used to break a joint dynamically, by removing it once these impulses exceed some threshold:

max_impulse = 10.0
world.step()
# The impulses applied by the joint during the last step: 3 linear components, then 3 angular components.
impulses = world.impulse_joints[joint_handle].impulses
# Break the joint if it has to pull its rigid-bodies too strongly to keep them together.
if math.hypot(*impulses[:3]) > max_impulse:
world.impulse_joints.remove(joint_handle)