Constraints
Constraints restrict degrees of freedom of rigid bodies. In Jitter2, constraints always act between two bodies. A joint is a collection of multiple constraints, so it may act on multiple bodies.
Degrees of Freedom
A rigid body has 6 degrees of freedom (DOF): 3 translational and 3 rotational. Constraints remove one or more of these degrees of freedom.
| Constraint | Removed DOF | Description |
|---|---|---|
BallSocket |
3 translational | Anchors a point on each body together |
FixedAngle |
3 rotational | Locks relative orientation between bodies |
HingeAngle |
2 rotational (3 with limit) | Allows rotation around a single axis only, optionally enforces angular limits |
PointOnLine |
2 translational (3 with limit) | Constrains a point to a line, optionally limits distance along the axis |
PointOnPlane |
1 translational (when limit active) | Constrains a point to a plane, only enforced when the limit is violated |
DistanceLimit |
1 translational (when limit active) | Constrains the distance between anchor points, only enforced when outside the limit range |
TwistAngle |
1 rotational (when limit active) | Limits relative twist around an axis, only enforced when outside the limit range |
ConeLimit |
1 rotational (when limit active) | Limits angular tilt between two axes |
EllipticalConeLimit |
1 rotational (when limit active) | Limits swing between inner and outer elliptical cones; leaves twist free |
AngularMotor |
— | Drives angular velocity (does not remove DOF) |
LinearMotor |
— | Drives linear velocity (does not remove DOF) |
SpringConstraint |
1 translational | Spring-like force along the anchor connection |
Joints
Joints are combinations of constraints for common use cases:
| Joint | Constraints used | Remaining DOF |
|---|---|---|
HingeJoint |
BallSocket + HingeAngle (+ optional AngularMotor or TwistServo) |
1 rotational |
PrismaticJoint |
PointOnLine + FixedAngle or HingeAngle (+ optional LinearMotor or LinearAxisServo) |
1 translational |
UniversalJoint |
BallSocket + TwistAngle (+ optional AngularMotor or AngularServo) |
2 rotational |
WeldJoint |
BallSocket + FixedAngle |
0 (fully locked) |
Joint drives
HingeJoint, PrismaticJoint, and UniversalJoint can create an optional motor or servo.
Select JointDrive.Motor for velocity control or JointDrive.Servo for position control.
Omitting the drive creates the mechanical joint only (JointDrive.None).
var elbow = new HingeJoint(
world, upperArm, lowerArm, center, axis,
angle: AngularLimit.FromDegree(0, 135),
drive: JointDrive.Servo);
elbow.Servo!.TargetAngle = JAngle.FromDegree(90);
elbow.Servo.MaximumTorque = 20;
The hinge exposes a TwistServo; its target angle uses the same zero and direction as
HingeAngle.Angle. The prismatic joint exposes a LinearAxisServo using the same coordinate
as Slider.Distance:
var slider = new PrismaticJoint(
world, body1, body2, center, axis,
limit: new LinearLimit(0, 2),
drive: JointDrive.Servo);
slider.Servo!.TargetPosition = 1.5f;
slider.Servo.MaximumForce = 100;
The universal joint exposes an AngularServo with a target orientation for body 2 relative to
body 1. Its initial target is the current relative orientation:
var universal = new UniversalJoint(
world, body1, body2, center, axis1, axis2,
drive: JointDrive.Servo);
universal.Servo!.TargetRelativeOrientation = targetRelativeOrientation;
universal.Servo.MaximumTorque = 20;
The twist constraint remains active, so only orientations allowed by the joint can be reached.
Drives start with zero maximum force or torque. Mechanical limits remain independent of
the drive. The joint includes its drive when enabled, disabled, removed, or drawn for debugging.
Motor is available when selecting JointDrive.Motor; Servo is available when selecting
JointDrive.Servo. Otherwise the corresponding property is null.
The older hasMotor constructor overloads are obsolete and remain supported. Replace
hasMotor: true with drive: JointDrive.Motor, or omit the drive when no motor is needed.
Creating constraints
Constraints are instantiated through a create method in the world class:
var constraint = world.CreateConstraint<ConstraintType>(body1, body2);
Jitter2 allocates and assigns unmanaged memory to constraints internally, allowing them to be used like regular classes.
The solver interacts with unmanaged memory structures of type ConstraintData (256 bytes) or SmallConstraintData (128 bytes), depending on whether constraint.IsSmallConstraint is set.
Small constraints are designed for simpler constraints with a smaller memory footprint, such as spring constraints for soft bodies.
Caution
Constraint Initialization
For all default constraints, constraint.Initialize must be called once after world.CreateConstraint.
World space initialization
Constraints are initialized using world space coordinates (anchor points, axes).
The bodies should be positioned in a valid configuration before calling Initialize.
The constraint then computes and stores local reference frames for each body based on their current poses.
This means the constraint "remembers" the initial setup and will try to maintain it during simulation.
Example: Hinge joint
A swinging door can be implemented by combining two constraints: A HingeAngle constraint which removes two degrees of angular freedom (the bodies can only rotate around a single axis), and a BallSocket constraint removing all three translational degrees of freedom:
var hingeAngle = world.CreateConstraint<HingeAngle>(body1, body2);
hingeAngle.Initialize(hingeAxis, AngularLimit.Full);
var ballSocket = world.CreateConstraint<BallSocket>(body1, body2);
ballSocket.Initialize(hingeCenter);
Alternatively, the HingeJoint can be used for convenience:
var hinge = new HingeJoint(world, body1, body2, hingeCenter, hingeAxis);
Cone limits
ConeLimit restricts the angle between two axes to a range from 0° to 180°.
Ranges such as [0°, 45°], [135°, 180°], and [0°, 180°] are valid.
Equal limits fix the angle when it lies strictly between 0° and 180°, for example [30°, 30°].
The fixed ranges [0°, 0°] and [180°, 180°] are rejected by Initialize and the Limit property.
To keep axes parallel or antiparallel while allowing free twist, use HingeAngle with AngularLimit.Full:
var hingeAngle = world.CreateConstraint<HingeAngle>(body1, body2);
hingeAngle.Initialize(hingeAxis, AngularLimit.Full);
Position the bodies with the intended axes aligned or antiparallel before initialization.
HingeAngle stores their current relative orientation as its reference; it does not pull two
arbitrarily misaligned axes into alignment during initialization.
Elliptical cone limits
EllipticalConeLimit allows different swing limits in two perpendicular directions. Body 1 owns
the cone frame; body 2 owns the axis being constrained. All initialization directions are supplied
in world space and stored relative to their respective bodies.
var cone = world.CreateConstraint<EllipticalConeLimit>(body1, body2);
cone.Initialize(
axis: JVector.UnitY,
referenceDirection: JVector.UnitX,
limitAlongReference: AngularLimit.FromDegree(15, 60),
limitAlongPerpendicular: AngularLimit.FromDegree(5, 25));
The lower bounds define the inner ellipse (15° and 5°); the upper bounds define the outer ellipse (60° and 25°). Swing is allowed between these boundaries. Setting both lower bounds to zero removes the inner boundary. Setting each lower bound equal to its upper bound constrains swing to one elliptical cone surface. Identical ranges in both directions give a circular cone.
The ellipses are defined in swing-angle coordinates: the shortest tilt angle resolved along
the reference direction and axis cross referenceDirection. Each boundary satisfies
(swing1 / angle1)^2 + (swing2 / angle2)^2 = 1. These are coupled boundaries, not independent
clamps on two angles. The reference direction is projected perpendicular to the cone axis;
parallel directions are rejected. Rotation of body 1 rotates the entire ellipse frame.
Each range must satisfy 0 <= From <= To < 180°, with a positive upper bound. The lower bounds
must either both be zero or both be positive. Fixed zero and 180° tilt are unsupported. Change
the ranges together using SetLimits; inspect the current coordinates using GetSwing.
The two-axis Initialize overload supports an initially tilted body 2. The single-axis overload
starts at zero tilt, which violates a positive inner boundary and will be corrected by the solver.
Translation and twist of body 2 about its constrained axis remain free. Combine this constraint
with BallSocket for a joint, and optionally TwistAngle to limit twist.
Servos
Servos drive bodies toward a target configuration. They can start outside that configuration, and their targets can change between simulation steps. Changing a target wakes sleeping connected dynamic bodies on the next step. Motors control velocity; servos control position or orientation.
| Servo | Controls | Target |
|---|---|---|
AngularServo |
3 angular directions | TargetRelativeOrientation: body 2's orientation in body 1's frame |
TwistServo |
1 angular direction | TargetAngle: twist relative to the initial zero-twist pose |
LinearServo |
3 linear directions | TargetPosition: offset from anchor 1 to anchor 2, in body 1's frame |
LinearAxisServo |
1 linear direction | TargetPosition: signed anchor separation along the axis attached to body 1 |
Maximum force or torque defaults to zero, as with motors. Set MaximumForce or
MaximumTorque to enable the drive. Positive infinity allows unlimited force or torque.
Bias controls how aggressively the servo approaches its target; Softness allows compliance.
For example, drive two robot arm segments toward a relative orientation:
var servo = world.CreateConstraint<AngularServo>(upperArm, lowerArm);
servo.Initialize(JQuaternion.CreateRotationX((Real)0.5));
servo.MaximumTorque = 20;
// Choose a different configuration later without moving the bodies directly.
servo.TargetRelativeOrientation = JQuaternion.CreateRotationX((Real)1.0);
The parameterless AngularServo.Initialize() captures the current relative orientation instead.
LinearServo.Initialize(anchor1, anchor2) accepts separate world-space attachment points;
its default target of zero pulls them toward coincidence. LinearAxisServo.Initialize also
accepts the desired signed separation. TwistServo.Initialize accepts a target angle relative
to its initial reference, so a nonzero target begins with an angular error.
TwistServo uses signed joint angles from -180° to +180°. Moving from -170° to +170°
travels 340° through zero. Targets outside this interval are rejected; revolutions are not
tracked. Use mechanical limits to keep the joint's motion inside the angular interval.
AngularServo targets an orientation and uses the shortest rotation to reach it, so
equivalent orientations represent the same target.
Servos act alongside mechanical constraints. For an elbow, use a hinge to keep the arm segments
connected and enforce the joint's angle limits, then add a TwistServo around the same axis.
Its torque limit controls the actuator's strength while the hinge retains its mechanical limits.
Fixed constraints
To constrain a rigid body relative to world space, use world.NullBody as one of the bodies.
For example, to keep a capsule upright:
var upright = world.CreateConstraint<HingeAngle>(capsule, world.NullBody);
upright.Initialize(JVector.UnitY, AngularLimit.Full);
Softness and Bias
For most constraints, softness and bias values can be set. These values define how strictly the constraint limits are enforced. Softer constraints may improve simulation stability but do not fully enforce the constraint limits. The softness and bias parameters can be tweaked for optimal results. Better constraint behavior can also be achieved by sub-stepping, see World.
Enable/Disable constraints
Constraints can be temporarily enabled or disabled using constraint.IsEnabled.
Debug drawing
Constraints and joints can draw their anchors, axes, and limits through an IDebugDrawer:
constraint.DebugDraw(drawer);
joint.DebugDraw(drawer);
See setting up a debug drawer to connect this to your renderer. Call these methods after the physics step, while the world is not being modified by another thread.
Green shows reference guides and upper or fixed limits, red shows lower limits, and white shows current anchors and directions. Coordinate frames use red for X, green for Y, and blue for Z.
| Constraint | Reading the drawing |
|---|---|
BallSocket |
The two anchor markers should coincide. The line between them shows separation. |
DistanceLimit |
Spheres around the first anchor show the allowed distances to the second anchor. |
PointOnLine |
Crosses mark stops along the line; arrows mark unbounded ends. A white line shows displacement away from the line. |
PointOnPlane |
Grids show the limiting planes. A white projection line appears when the anchor is outside the allowed region. The planes extend beyond the grids. |
ConeLimit, EllipticalConeLimit |
The white axis must stay within the outer cone and outside any red inner cone. Equal limits allow motion along one cone surface. |
HingeAngle |
An arc shows the allowed angles, or a green ray marks a fixed angle. The white ray shows the current angle. Short axis arrows reveal hinge misalignment. |
TwistAngle |
The dial shows the current twist and its limits. The white axis shows the current body direction; swing remains free. |
FixedAngle |
An RGB frame on each body accounts for the initial relative orientation. Matching axes stay parallel when the target is met. No frame is drawn for World.NullBody. |
LinearMotor |
Green arrows show body 1's contribution and white arrows show body 2's contribution to the requested relative motion. These show target direction, not measured velocity; a static body stays still. |
AngularMotor |
A green circle and white arrow show the target rotation of body 2, or body 1 when body 2 is static or kinematic. Short lines show both motor axes. The arrow shows target direction, not measured speed; a zero target has no arrow. |
AngularServo |
A green target frame and an RGB current frame share an origin. Corresponding axes align when the target is reached. |
TwistServo |
The dial marks the target twist and the current twist angle. |
LinearServo |
Green and white markers show the target and current anchor positions; the connecting line shows the position error. |
LinearAxisServo |
Markers show target and current position projected onto the reference axis. Perpendicular motion is uncontrolled. |
SpringConstraint |
The coil joins the anchors; the green cross marks the rest length along the spring direction. |
Pass DebugDrawSettings to change the appearance:
constraint.DebugDraw(drawer, new DebugDrawSettings
{
GuideSize = 2,
FrameSize = 1.5f
});
GuideSize changes the size of guides. FrameSize sets the length of coordinate-frame
axes in world units, and PointRadius sets the radius of anchor spheres. Arrowhead size and
shaft thickness are adjusted separately using ArrowHeadSize and ArrowShaftRadius.
Physical distances and angular limits keep their actual values.
See DebugDrawSettings for color, arrow, and detail settings.
Arrows are solid by default. Set SolidArrows = false for wire arrows. ArrowSegments controls
arrow smoothness. Wire arrowheads connect every vertex around the base to the tip.
Your drawer's DrawTriangle implementation controls whether solid arrows and anchor markers
appear filled or as triangle edges. When drawing your own guides, DrawArrow and DrawFrame
use the same defaults; DrawWireArrow and DrawWireFrame explicitly draw wire geometry.
Constraint lifetime
Constraints are attached to their two rigid bodies. They can become invalid without an explicit
call to world.Remove(constraint) when:
- either connected body is removed;
- changing a body's motion type leaves both connected bodies non-dynamic;
world.Clear()removes the connected bodies; or- the world is disposed.
Use constraint.IsValid before accessing a retained constraint reference when another operation
may have removed it:
world.Remove(body1); // also removes constraints attached to body1
if (constraint.IsValid)
{
constraint.IsEnabled = false;
}
Once IsValid is false, the constraint's unmanaged data is no longer available and its
data-backed properties and methods must not be accessed. Rigid bodies provide the equivalent
body.IsValid check after body removal or world disposal.
For reactive cleanup, World.ConstraintRemoved is raised when a constraint is removed explicitly
or automatically:
world.ConstraintRemoved += removedConstraint =>
{
// Drop any application-side state associated with removedConstraint.
};
The constraint is already invalid when the callback runs. Disabling a constraint and disposing the world do not raise this event.