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 [10]. 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 [8] and [19].
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.