Joints
A joint constrains how two bodies may move relative to each other. There are nine, each with its own handle type and its own definition.
// A door that opens ninety degrees and swings shut behind you.
RevoluteJoint hinge = world.CreateRevoluteJoint(
RevoluteJointDefinition.Hinge(frame, door, hingePoint, Vector3.UnitY) with
{
LimitsEnabled = true,
LowerAngle = 0.0f,
UpperAngle = MathF.PI * 0.5f,
MotorEnabled = true,
MotorSpeed = -1.0f,
MaxMotorTorque = 50.0f,
});
The nine
| Joint | Leaves free | Built with | For |
|---|---|---|---|
Revolute |
one rotation axis | Hinge(a, b, anchor, axis) |
Doors, wheels, chains, ragdoll elbows |
Prismatic |
one translation axis | Slider(a, b, anchor, axis) |
Lifts, pistons, drawers |
Distance |
everything, at a fixed range | Between(a, b, anchorA, anchorB) |
Ropes, springs, struts |
Spherical |
all three rotations | BallAndSocket(a, b, anchor, axis) |
Shoulders, hips, pendulums |
Weld |
nothing | Weld(a, b, anchor) |
Rigid assemblies, breakable joins |
Wheel |
spin plus suspension travel | Suspension(chassis, wheel, anchor, axis) |
Vehicles |
Motor |
everything, while driving a target pose | MotorJointDefinition |
Followers, mouse dragging, active props |
Parallel |
everything but the frame's z axis | ParallelJointDefinition |
Keeping something upright without locking it |
Filter |
everything | Between(a, b) |
Two bodies that must not collide |
Each Create…Joint returns the specific handle, not a generic one, so
hinge.MotorSpeed compiles and hinge.MinLength does not. The shared members
are one hop away through hinge.AsJoint.
Use the factory methods
A joint needs a pair of local frames describing the same world pose from each body's point of view. Get that wrong and the joint starts out violated and snaps on the first step.
Hinge, Slider, Between, BallAndSocket, Weld and Suspension derive
that pair from a world-space anchor and axis, which is how you would describe
the joint out loud. For the joints without a factory, or for a frame you want to
build yourself, Joint.FramesFromWorldAnchor does the same
calculation.
Build the assembly in its rest pose. A chain assembled already displaced has every joint violated on the first step and snaps — give it angular velocity instead.
Limits and motors
Most joints take limits, a motor, or both. The pattern is the same everywhere:
an …Enabled flag, the range, and a maximum force or torque the motor may
spend.
// A lift that travels four metres straight up.
PrismaticJoint lift = world.CreatePrismaticJoint(
PrismaticJointDefinition.Slider(shaft, platform, basePoint, Vector3.UnitY) with
{
LimitsEnabled = true,
LowerTranslation = 0.0f,
UpperTranslation = 4.0f,
MotorEnabled = true,
MotorSpeed = 1.0f,
MaxMotorForce = 5000.0f,
});
| Joint | Limits | Motor |
|---|---|---|
Revolute |
LowerAngle, UpperAngle |
MotorSpeed, MaxMotorTorque |
Prismatic |
LowerTranslation, UpperTranslation |
MotorSpeed, MaxMotorForce |
Distance |
MinLength, MaxLength |
MotorSpeed, MaxMotorForce |
Spherical |
ConeAngle, LowerTwistAngle, UpperTwistAngle |
MotorVelocity, MaxMotorTorque |
Wheel |
suspension and steering, each with its own pair | SpinSpeed/MaxSpinTorque, TargetSteeringAngle/MaxSteeringTorque |
A motor with an unlimited maximum will hold anything, including things it should not. The maximum is what makes a door closer stop when you push against it.
Springs
Several joints can be soft rather than rigid, described in hertz and a damping ratio rather than in stiffness:
var rope = DistanceJointDefinition.Between(anchor, load, top, hook) with
{
SpringEnabled = true,
Hertz = 4.0f, // how fast it oscillates
DampingRatio = 0.5f, // 1.0 is critically damped
LimitsEnabled = true,
MinLength = 0.1f,
MaxLength = 3.0f,
};
A wheel is the same idea twice under different names: SuspensionHertz and
SuspensionDampingRatio for the travel, SteeringHertz and
SteeringDampingRatio for how sharply it turns to a target angle. Parallel
uses a spring to keep two frames' z axes aligned, which is how you keep a body
upright without locking its rotation outright.
Reading a joint back
Vector3 force = hinge.AsJoint.ConstraintForce;
float drift = hinge.AsJoint.LinearSeparation;
if (force.Length() > BreakingForce)
{
hinge.AsJoint.Destroy();
}
ForceThreshold and TorqueThreshold on the base definition make the engine
report when a joint is overloaded, which is the ingredient for something that
breaks under load.
Bodies that should not collide
Connected bodies do not collide by default. Set CollideConnected on the base
definition when they should — a wheel that must still hit the ground it sits on.
For two bodies that are not jointed at all but must not collide, use a filter joint rather than spending a category bit:
world.CreateFilterJoint(FilterJointDefinition.Between(turret, shell));
Tuning
Every joint carries ConstraintHertz and ConstraintDampingRatio on its base
definition, which control how hard the solver works to hold it together. Raise
the hertz for an assembly that visibly stretches under load; lower it for one
that jitters.
joint.SetConstraintTuning(hertz: 60.0f, dampingRatio: 2.0f);
DrawScale decides how large the joint's markers are when
drawn. Joints are one of the things worth seeing before you
believe them.