In a typical mechanical model, many of the rigid bodies are interconnected, either using spring-type components that exert binding forces on the bodies, or through joints and connectors that enforce the connection using hard constraints. This section describes the latter. While the discussion focuses on rigid bodies, joints and connectors can be used more generally with any body that implements the ConnectableBody interface. In particular, this allows joints to also interconnect finite element models, as described in Section 6.6.2.
Consider two rigid bodies A and B. The pose of body B with respect to
body A can be described by the 6 DOF rigid transform . If A
and B are unconnected,
may assume any possible value and has
a full six degrees of freedom. A joint between A and B
constrains the set of poses that are possible between the two bodies
and reduces the degrees of freedom available to
. For ease
of use, the constraining action of a joint is described with respect
to a pair of local coordinate frames C and D that are connected to
frames A and B, respectively, by auxiliary transformations. This
allows joints to be placed at locations that do not correspond
directly to frames A or B.
The joint frames C and D move with respect to each other as the joint
moves. The allowed joint motions therefore correspond to the allowed
values of the joint transform . Although both frames
typically move with their attached bodies, D is considered the base frame and C the motion frame (this is because when a joint
is used to connect a single body to ground, body B is set to null and the world frame takes its place). As an example of a
joint’s constraining effect, consider a hinge joint (Figure
3.8), which allows C to move with respect to D only by
rotating about the
axis while the origins of C and D remain
coincident. Other motions are prohibited. If we let
describe
the counter-clockwise rotation angle of C about the
axis, then
should always have the form
![]() |
(3.7) |
When a joint is attached to bodies A and B, frame C is fixed to body A
and frame D is fixed to body B. Except in special cases, the joint
frames C and D are not coincident with the body frames A
and B. Instead, they are located relative to A and B by the
transforms and
, respectively
(Figure 3.9). Since
and
are
both fixed, the joint constraints on
constrain the relative
poses of A and B, with
determined from
![]() |
(3.8) |
(See Section A.2 for a discussion of determining transforms between related coordinate frames).
Each different joint and connector type restricts the motion between
two bodies to degrees of freedom, for some
. Sometimes,
the joint also defines a set of
coordinates that parameterize
these
DOFs. For example, the hinge joint described above is
parameterized by
. Other examples are given in
Section 3.4: a 2 DOF cylindrical has coordinates
and
, a 3 DOF gimbal joint is parameterized by the
roll-pitch-yaw angles
,
, and
, etc. When
(where
is the identity transform), the coordinates
are usually all equal to zero, and the joint is said to be in the zero state.
As explained in Section 1.2, ArtiSynth uses a
full coordinate formulation for dynamic simulation. That means that
instead of using joint coordinates to describe system state, it uses
the combined full coordinates of all dynamic components. For
example, a model consisting of a single rigid body connected to ground
by a hinge joint will have 6 DOF (corresponding to the 6 DOF of the
body), rather than the 1 DOF implied by the hinge joint. The DOF
restrictions imposed by the joints are then enforced by a set of
linearized constraint relationships
![]() |
(3.9) |
that restrict the body velocities computed at each simulation
step, usually by solving an MLCP like (1.6). As
explained in Section 1.2, the right side
vectors
and
in (3.9) contain time
derivative terms, which for simplicity much of the following
presentation will assume to be 0.
Each joint contributes its own set of constraint equations to (3.9). Typically these take the form of bilateral, or equality, constraints
![]() |
(3.10) |
which are added to the system’s global bilateral constraint matrix
.
contains
rows providing
individual
constraints
.
During simulation, these give rise to
constraint
forces (corresponding to
in (1.8))
which enforce the constraints.
In some cases, the joint also maintains unilateral, or inequality
constraints, to keep out of inadmissible regions. These take
the form
![]() |
(3.11) |
and are added to the system’s global unilateral constraint matrix
. They give rise to constraint forces corresponding to
in
(1.8). A common use of unilateral constraints
is to enforce range limits of the joint coordinates (Section
3.3.5), such as
![]() |
(3.12) |
A specific unilateral constraint is added to only when
is on or within the boundary of the inadmissible region
associated with that constraint. The constraint is then said to be
engaged. The combined number of bilateral and engaged unilateral
constraints for a particular joint should not exceed 6; otherwise, the
joint would be overconstrained.
Joint coordinates, when supported for a particular joint, can be both
read and set. Setting a coordinate causes the joint transform
to change. To accommodate this, the system adjusts the poses
of one or both bodies connected to the joint, along with adjacent
bodies connected to them, with preference given to bodies that are not
attached to “ground”. However, if this is done during simulation,
and particularly if one or both of the bodies connected to the joint
are moving dynamically, the results will be unpredictable and will
likely conflict with the simulation.
Joint coordinates are also often exported as properties. For example,
the
HingeJoint
class (Section 3.4) exports its coordinate
as the property theta, which can be accessed in the GUI, or via
the accessor methods
Since joint constraints are generally nonlinear, their linearized
enforcement at the velocity level by (3.9) will
usually produce small errors as the simulation proceeds. These errors
are reduced using a position correction step described in
Section 4.9.1 and [11].
Errors can also be caused by joint compliance
(Section 3.3.8). Both effects mean that the joint
transform may deviate from the allowed values dictated by
the joint type. In ArtiSynth, this is accounted for by introducing an
additional constraint frame G between D and C
(Figure 3.10). G is computed to be the nearest frame
to C that lies exactly in the joint constraint space.
is
therefore a valid joint transform,
accommodates the error,
and the whole joint transform is given by the composition
![]() |
(3.13) |
If there is no compliance or joint error, then frames G and C are
identical, , and
. Because
describes the joint error, we sometimes refer to it as
.
Joint and connector components in ArtiSynth are both derived from the superclass BodyConnector, with joints being further derived from JointBase, which provides support for coordinates. Some of the commonly used joints and connectors are described in Section 3.4.
An application creates a joint by constructing it and adding it to a MechModel. Many joints have constructors of the form
which specifies the bodies A and B which the joint connects,
along with the transform giving the pose of the joint base
frame D in world coordinates. The constructor then assumes that the
joint is in the zero state, so that C and D are the same and
and
, and then computes
and
from
![]() |
![]() |
(3.14) | ||
![]() |
![]() |
(3.15) |
where and
are the current poses of A and B.
After the joint is created, it should be added to the system’s MechModel using addBodyConnector(), as shown in the following code fragment:
It is also possible to create a joint using its default constructor and attach it to the bodies afterward, using the method setBodies(bodyA,bodyB,TDW), as in the following:
One reason for doing this is that it allows the joint transform
to be modified (by setting coordinate values) before
setBodies() is called; this is discussed further in
Section 3.3.4.
Joints usually offer a number of other constructors that let its world location and body relationships to be specified in different ways. These may include:
The first, which is restricted to rigid bodies, allows the application
to explicitly specify transforms and
connecting
frames C and D to the body frames A and B, and is useful when
and
are explicitly known, or the initial value of
is not the identity. Likewise, the second constructor
allows
and
to be explicitly specified, with
if
. For instance, suppose
and
are both known. Then we can use the
relationship
![]() |
(3.16) |
to create the joint as in the following code fragment:
As an alternative to specifying or its equivalents, some
joint types provide constructors that let the application locate
specific joint features. These may be easier to use in some cases. For
instance, HingeJoint provides a
constructor
that specifies origin of D and its axis (which is the rotation
axis), with the remaining orientation of D aligned as closely as
possible with the world.
SphericalJoint provides a
constructor
that specifies origin of D and aligns its orientation with the world. Users should consult the source code or API documentation for specific joints to see what special constructors may be available.
Finally, it is possible to use joints to connect a single body to ground (by convention, this is the A body). Most joints provide a constructor of the form
which allows this to be done explicitly. Alternatively, most joint constructors which supply body B will allow this to be specified as null, so that body A will be connected to ground by default.
As mentioned in Section 3.3.2, some joints
support coordinates that parameterize the valid motions within the
joint transform . All such joints are subclasses of
JointBase,
which provides some generic methods for querying and setting
coordinate values (JointBase is in turn a subclass of
BodyConnector).
The number of coordinates is returned by the method numCoordinates(); if this returns 0, then coordinates are not
supported. Each coordinate has an index in the range ,
where
is the number of coordinates. Coordinate values can be
queried or set using the following methods:
Specific joint types usually also provide names for their joint
coordinates, along with integer constants describing their indices
and methods for accessing their values. For example,
CylindricalJoint
supports two coordinates, and
, along with
the following:
The coordinate values are also exported as the properties z and theta, allowing them to be set in the GUI. For convenience, particularly in GUI applications, the properties and methods for controlling specific angular coordinates generally use degrees instead of radians.
As discussed in Section 3.3.2, unlike in some multibody simulation systems (such as OpenSim), joint coordinates are not fundamental quantities that describe system state. As such, then, coordinates can usually only be set in specific circumstances that avoid simulation conflicts. In general, when joint coordinates are set, the system adjusts the poses of one or both bodies connected to this joint, along with adjacent bodies connected to them, with preference given to bodies that are not attached to “ground”. However, if this is done during simulation, and particularly if one or both of the bodies connected to the joint are moving dynamically, the results will be unpredictable and will likely conflict with the simulation.
If a joint has been created with its default constructor and not yet
attached to any bodies, then setting joint values will simply set the
joint transform . This can be useful in situations where one
needs to initialize a joint’s
to a non-identity value
corresponding to a particular set of joint coordinates:
This can also be done in vector form:
In either of these cases, setBodies() will not use but instead use the value determined by the initial coordinate
values.
To determine the corresponding to a particular set of
coordinates, one may use the method
In some cases, within a model’s build() method, one may wish to set initial coordinates after a joint has been attached to its bodies, in order to move those bodies (along with the bodies attached to them) into an initial configuration without having to explicitly calculate the poses from the joint coordinates. As mentioned above, the system will make a decision about which attached bodies are most “free” and adjust their poses accordingly. This is done in the example of the next section.
It is possible to set limits on a joint coordinate’s range, and also to lock a coordinate in place at its current value.
When a joint coordinate hits either an upper or lower range limit, a unilateral constraint is invoked to prevent it from violating the limit, and remains engaged until the joint moves away from the limit. Each range constraint that is engaged reduces the number of joint DOFs by one.
By default, joint range limits are usually disabled (i.e., they are
set to ). They can be queried and set, for a given
joint with index idx, using the methods:
where range limits for angular coordinates are specified in radians. For convenience, the following methods are also provided which use degrees instead of radians for angular coordinates:
Range checking can be disabled by setting the range to
, or by specifying rng as null, which
implicitly does the same thing.
Ranges for angular coordinates are not limited to
but can instead be set to larger values; the joint will continue to wrap until the limit is reached.
Joint coordinates can also be locked, so that they hold their current value and don’t move. A joint is locked using a bilateral constraint that prevents motion in either direction and reduces the joint’s DOF count by one. The following methods are available for querying or setting a coordinate’s locked status:
As with coordinate values, specific joint types usually provide methods for controlling the ranges and locking status of individual coordinates, with ranges for angular coordinates specified in degrees instead of radians. For example, CylindricalJoint supplies the methods
The range and locking information is also exported as the properties zRange, thetaRange, zLocked, and thetaLocked, allowing them to be set in the GUI.
A simple model showing two rigid bodies connected by a joint is defined in
artisynth.demos.tutorial.RigidBodyJoint
The build method for this model is given below:
A MechModel is created as usual at line 4. However, in this
example, we also set some parameters for it:
setGravity() is
used to set the gravity acceleration vector to instead
of the default value of
, and the frameDamping
and rotaryDamping properties (Section
3.2.7) are set to provide appropriate damping.
Each of the two rigid bodies are created from a mesh and a density. The meshes themselves are created using the factory methods MeshFactory.createRoundedBox() and MeshFactory.createRoundedCylinder() (lines 13 and 22), and then RigidBody.createFromMesh() is used to turn these into rigid bodies with a density of 0.2 (lines 17 and 25). The pose of the two bodies is set using RigidTransform3d objects created with x, y, z translation and axis-angle orientation values (lines 18 and 26).
The hinge joint is implemented using HingeJoint, which is constructed at line 32 with the joint coordinate frame D being located in world coordinates by TDW as described in Section 3.3.3.
Once the joint is created and added to the MechModel, the method
setTheta() is
used to explicitly set the joint parameter to 35 degrees. The joint
transform is then set appropriately and bodyA is moved
to accommodate this (bodyA being chosen since it is the most
free to move).
Finally, joint rendering properties are set starting at line 42. We render the joint as a cylindrical shaft about the rotation axis, using its shaftLength and shaftRadius properties. Joint rendering is discussed in more detail in Section 3.3.10).
During each simulation solve step, the joint velocity constraints
described by (3.10) and (3.11) are
enforced by bilateral and unilateral constraint forces and
:
![]() |
(3.17) |
Here, and
are spatial forces (or wrenches, Section
A.5) acting in the joint coordinate
frame C, and
and
are the Lagrange multipliers
computed as part of the mechanical system solve (see
(1.6) and (1.8)). The sizes
of
and
equal the number of bilateral and engaged unilateral constraints in the joint; these numbers can be
queried for a particular joint using the methods
numBilateralConstraints()
and
numEngagedUnilateralConstraints().
(The number of engaged unilateral constraints may be less than the
total number of unilateral constraints; the latter may be queried with
numUnilateralConstraints(),
while the total number of
constraints is returned by
numConstraints().
Applications may sometimes need to query the current constraint force values, typically from within a controller or monitor (Section 5.3). The Lagrange multipliers themselves may be obtained with
which load the multipliers into lam or the and set their sizes to the number of bilateral or engaged unilateral constraints. Alternatively, one can retrieve the individual multiplier for the constraint indexed by idx using
Typically, it is more useful to find the spatial constraint forces
and
, which can be obtained with respect to frame C:
If the attached bodies A and B are rigid bodies, it is also possible to obtain the constraint wrenches experienced by those bodies:
Constraint wrenches obtained for bodies A or B are given in world
coordinates, which is consistent with the forces reported by rigid
bodies via their getForce() method. To orient the forces into
body coordinates, one may use the inverse of the rotation matrix
of the body’s pose. For example:
By default, the constraints used to implement joints and couplings are treated as hard, so that the system tries to respect the constraint conditions (3.9) as exactly as possible as the simulation proceeds. Sometimes, however, it is desirable to introduce some “softness” into the constraints, whereby constraint forces are determined as a linear function of their distance from the constraint. Adding compliance also allows an application to regularize a system of joint constraints that would otherwise be overconstrained, as illustrated in Section 3.3.9.
To describe compliance precisely, consider the bilateral constraint
portion of the MLCP in (1.6), which solves for the
updated system velocities at each time step:
![]() |
(3.18) |
Here is the system’s bilateral constraint matrix,
denotes the constraint impulses (from which the constraint forces
can be determined by
), and for
simplicity we have assumed that
is constant and so the
term
on the lower right side is
.
Solving (3.18) results in constraint forces that satisfy
precisely, corresponding to hard constraints. To
implement soft constraints, start by defining a function
that defines the distances from each constraint, where
is the
vector of system positions; these distances are
the local translational and rotational deviations from each
constraint’s correct position and are discussed in more detail in
Section 4.9.1. Then assume that the constraint
forces are a linear function of these distances:
![]() |
(3.19) |
where is a diagonal compliance matrix that is equivalent to
an inverse stiffness matrix. We also note that
will be time
varying, and that we can approximate its change between time steps as
![]() |
(3.20) |
Next, assume that in using (3.19) to determine
for a particular time step, we use the average value of
over the step, represented by
.
Substituting this and (3.20) into
(3.19), multiplying by
, and rearranging yields:
![]() |
(3.21) |
Then noting that , we obtain
a revised form of (3.18),
![]() |
(3.22) |
in the which the zeros in the matrix and right hand side have been replaced by compliance terms. The resulting constraint behavior is different from that of (3.18) in two important ways:
The joint now allows 6 DOF, with motion along the constrained directions
limited by restoring spring constants given by the reciprocals of the
diagonal entries of .
Unilateral constraints can be regularized using the same approach,
with a distance function defined such that .
The reason for specifying soft constraints using compliance instead of
stiffness is that by setting we can easily handle the case of
infinite stiffness where the constraints are strictly enforced.
The ArtiSynth compliance implementation uses a slightly more complex
version of (3.22) that accounts for non-constant
and
also allows for a damping term
, where
is again a
diagonal matrix. For more details, see [9]
and [21].
When using compliance, damping is often needed for stability, and, in the case of unilateral constraints, to prevent “bouncing”. A good choice for damping is usually critical damping, which is discussed further below.
Any joint which is a subclass of
BodyConnector allows
individual compliance values and damping values
to be set
for each of the joint’s
constraints. These values comprise the
diagonal entries in the compliance and damping matrices
and
,
and can be queried and set using the methods
The vectors supplied to the above set methods contain the
requested compliance or damping values. If their size is less than
numConstraints(), then compliance or damping will be set for the
first
constraints. Damping for a specific constraint only has an
effect if the compliance for that constraint is nonzero.
What compliance and damping values should be specified? Compliance is
usually relatively easy to figure out. Each of the joint’s individual
constraints corresponds to a row in its bilateral constraint matrix
or unilateral constraint matrix
, and represents a
specific 6 DOF direction along which the spatial velocity
(of frame C with respect to D) is restricted (more
details on this are given in Section 4.9.1).
Each of these constraint directions is usually predominantly linear or
rotational; specific descriptions for the constraints of different
joints are provided in Section 3.4. To determine
compliance for a constraint
, estimate the typical force
likely
to act along its direction, decide how much displacement
(translational or rotational) along that constraint is desirable, and
then set the compliance
to the associated inverse stiffness:
![]() |
(3.23) |
Once is determined, the damping
can be estimated based on
the desired damping ratio
, using the formula
![]() |
(3.24) |
where is total mass of the bodies attached to the joint.
Typically, the desired damping will be close to critical damping, for
which
.
Constraints associated with linear motion will typically require different compliance values from those associated with rotation. To make this process easier, joint components allow the setting of collective compliance values for their linear and rotary constraints, using the methods
The set() methods will set a uniform compliance for all linear or rotary constraints, except for unilateral constraints associated with coordinate limits. At the same time, they will also set an automatically computed critical damping value. Likewise, the get() methods query these linear or rotary constraints for uniform compliance values (with the corresponding critical damping), and return either that value, or -1 if it does not exist.
Most of the demonstration models for the joints described in Section 3.4 allow these linear and rotary compliance settings to be adjusted interactively using a control panel, enabling users to experimentally gain a feel for their behavior.
To determine programmatically whether a particular constraint is linear or rotary, one can use the joint method
which returns a vector of information flags for all its constraints. Linear and rotary constraints are indicated by the flags LINEAR and ROTARY, defined in RigidBodyConstraint.
Situations may occasionally arise in which a model is overconstrained, which means that the rows of the bilateral
constraint matrix in (3.9) are not all
linearly dependent, or in other words,
does not have full
row rank. At present, the ArtiSynth solver has difficultly handling
overconstrained models, but these situations can often be handled by
adding a small amount of compliance to the
constraints. (Overconstraining is not a problem with unilateral
constraints
, because of the way they are handled by the solver.)
One possible symptom of an overconstrained system is a error message in the application’s terminal output, such as
Pardiso: num perturbed pivots=12
Overconstraining frequently occurs in closed-chain linkages, involving loops in which a jointed sequence of links is connected back on itself. Depending on how the constraints are configured and how redundant they are, the system may still be able to move. A classical example is the four-bar linkage, a common version of which consists of four links, or “bars”, arranged as a parallelogram and connected by hinge joints at the corners. One link is usually connected to ground, and so the remaining three links together have 18 DOF, while the four hinge joints together remove 20 DOF, overconstraining the system. However, the constraints are redundant in such as way that the linkage still actually has 1 DOF.
To model a four-bar in ArtiSynth presently requires adding compliance to the hinge joints. An example of this is defined by the demo program
artisynth.demos.tutorial.FourBarLinkage
shown in Figure 3.12. The code for the build() method and a couple of supporting methods is given below:
Two helper methods are used to construct the model: createLink()
(lines 6-17), and createJoint() (lines 23-36). createLink() makes the individual rigid bodies used to build the
linkage: a mesh is produced defining the body’s shape (a box with
rounded ends), and then passed to the
RigidBody
createFromMesh() method which creates the body and sets its
inertia according to a specified density. The body’s pose is then set
so as to center it at while rotating it about the
axis
by the angle deg (in degrees). The completed body is then added to the
MechModel mech and returned.
The second helper method, createJoint(), connects two rigid
bodies (link0 and link1) together using a HingeJoint. Because we know the location of the joint in
body-relative coordinates, it is easier to create the joint using the
transforms and
instead of
:
locates the joint at the top end of link0, at
,
with the
axis parallel to the body’s
axis, while
similarly locates the joint at the bottom of link1. After the
joint is created and added to the MechModel, its render
properties are set so that its axis drawn as a blue cylinder.
The build() method itself begins by creating a MechModel
and setting damping parameters for the rigid bodies (lines
40-43). Next, createLink() is used to create and store the four
links (lines 46-50), and the left bar is attached to ground by making
it non-dynamic (line 52). The links are then connected together using
joints created by createJoint() (lines 55-59). Finally, uniform
compliance and damping values are set for each of the joint’s
bilateral constraints, using the setCompliance() and setDamping() methods (lines 63-72). Values are set for the first five
constraints, since for a HingeJoint these are the bilateral
constraints. The compliance value of was found
experimentally to be low enough so as to not cause noticeable
deflections in the joints. Given
and an
average mass of around
for each link pair,
(3.24) suggests the damping factor of
. Note that for this example, very similar settings could be
achieved by simply calling
In principle, we only need to set compliance for the constraints that are redundant, but it can sometimes be difficult to determine exactly which these are. Also, different values are often needed for linear and rotary constraints; that is not necessary here because the links have unit length and so the linear and rotary units have similar scales.
Most joints provide a means to render themselves in order to provide a graphical representation of their position and configuration. Control over this is achieved by setting various properties in the joint component, including both specialized properties and the standard render properties (Section 4.3) used by all renderable components.
All joints which are subclasses of JointBase support rendering of both their C and D coordinate frames, through the properties drawFrameC, drawFrameD, and axisLength. The first two properties are of the type Renderer.AxisDrawStyle (described in detail in Section 3.2.8), and can be set to LINE or ARROW to enable the coordinate axes to be drawn either as lines or solid arrows. The axisLength property has type double and specifies the length with which the axes are drawn. As with all properties, these properties can be set either in the GUI, or in code using accessor methods supplied by the joint:
Another pair of properties used by several joints is shaftLength and shaftRadius, which specify the length and radius used to draw shaft or axis structures associated with the joint. These are rendered as solid cylinders, using the color indicated by the faceColor rendering property. The default value of both properties is 0; if shaftLength is 0, then the structures are not drawn, while if shaftRadius is 0, a default value proportional to shaftLength is used. For example, to enable rendering of a blue shaft along the rotation axis of a hinge joint, one may use the code fragment
As another example, to enable rendering of a green ball about the center of a spherical joint, one may use the fragment
Specific joints may define additional properties to control how they are rendered.