| name | configure-joint-articulation |
| description | Configure Unity ArticulationBody joints from SDF joint definitions. Use when: adding a new joint type, debugging joint axis alignment, tuning spring/damping/limits, fixing revolute or prismatic joint behavior, setting up parent-child articulation hierarchy. |
Configure Joint Articulation
Reference and procedure for configuring Unity ArticulationBody joints from SDF <joint> definitions. Covers the full import → implement flow, joint type dispatch, axis alignment, drive configuration, and limit handling.
When to Use
- Adding support for a new SDF joint type (e.g., gearbox, screw)
- Debugging incorrect joint axis orientation or motion direction
- Tuning spring stiffness, damping, friction, or force limits
- Fixing revolute joint limit inversion issues
- Setting up parent-child
ArticulationBody hierarchy for a new robot model
- Understanding how SDF joint parameters map to Unity
ArticulationDrive
Architecture
SDF <joint>
│
├─ Import.Joint.cs: ImportJoint()
│ ├─ Resolve parent/child links by name
│ ├─ Create ArticulationBody on child link
│ ├─ Call Implement.Joint methods
│ └─ Store joint metadata on Helper.Link
│
└─ Implement.Joint.cs: MakeJoint()
├─ SetArticulationBodyRelationship() → parent-child hierarchy + anchor pose
├─ SetAnchor() → anchor position/rotation
└─ Type dispatch:
├─ Revolute/Continuous → MakeRevoluteJoint()
├─ Prismatic → MakePrismaticJoint()
├─ Ball → MakeBallJoint()
├─ Universal/Revolute2 → MakeRevoluteJoint2()
└─ Fixed → MakeFixedJoint()
Joint Type Reference
| SDF Type | Unity Type | DOF | Notes |
|---|
revolute | SphericalJoint (1-DOF locked) | 1 rotational | Limits via CurveOrientation swap |
continuous | SphericalJoint (1-DOF free) | 1 rotational | No limits, free rotation |
prismatic | PrismaticJoint | 1 linear | Direct limit mapping (no swap) |
ball | SphericalJoint (3-DOF free) | 3 rotational | All axes free |
universal / revolute2 | SphericalJoint (2-DOF) | 2 rotational | Two-axis via MakeRevoluteJoint2() |
fixed | FixedJoint | 0 | Zero solver iterations |
Procedure
1. Import Phase — Link Resolution and Hierarchy
Import.Joint.cs resolves parent/child links by name and establishes the ArticulationBody hierarchy:
var linkObjectParent = targetObject.FindTransformByName(joint.ParentName);
var linkObjectChild = targetObject.FindTransformByName(joint.ChildName);
var articulationBodyChild = linkObjectChild.GetComponent<ArticulationBody>();
if (articulationBodyChild == null)
articulationBodyChild = CreateArticulationBody(linkObjectChild.gameObject);
var anchorPose = Implement.Joint.SetArticulationBodyRelationship(
joint, linkObjectParent, linkObjectChild);
articulationBodyChild.SetAnchor(anchorPose);
articulationBodyChild.MakeJoint(joint);
2. Parent-Child Hierarchy Setup
SetArticulationBodyRelationship() decides hierarchy based on model scope:
linkChild.SetParent(linkParent);
modelTransformChild.SetParent(linkParent);
var (jointPos, jointRot) = joint.RawPose.ToUnity();
anchorPose.position += jointPos;
anchorPose.rotation *= jointRot;
3. Joint Type Configuration
Revolute Joint
Uses SphericalJoint with two axes locked. Key pattern: limit values are swapped through SDF2Unity.CurveOrientation():
private static void MakeRevoluteJoint(this ArticulationBody body, in JointAxis axis)
{
body.jointType = ArticulationJointType.SphericalJoint;
body.linearDamping = 1.5f;
body.angularDamping = 2f;
var drive = new ArticulationDrive();
if (axis.HasJointLimits())
{
drive.lowerLimit = SDF2Unity.CurveOrientation((float)axis.Upper);
drive.upperLimit = SDF2Unity.CurveOrientation((float)axis.Lower);
}
drive.forceLimit = double.IsInfinity(axis.Effort)
? float.MaxValue : (float)axis.Effort;
drive.stiffness = (float)axis.SpringStiffness;
drive.target = SDF2Unity.CurveOrientation((float)axis.SpringReference);
drive.damping = (float)axis.Damping;
body.jointFriction = (float)axis.Friction;
body.maxJointVelocity = (float)axis.MaxVelocity;
var jointAxis = axis.Xyz.ToUnity().normalized;
}
Axis Alignment Pattern
The dominant axis component determines which drive and DOF lock to use:
var absX = Mathf.Abs(jointAxis.x);
var absY = Mathf.Abs(jointAxis.y);
var absZ = Mathf.Abs(jointAxis.z);
if (absX >= absY && absX >= absZ)
{
body.anchorRotation *= Quaternion.FromToRotation(Vector3.right, jointAxis);
body.xDrive = drive;
body.twistLock = hasLimits
? ArticulationDofLock.LimitedMotion : ArticulationDofLock.FreeMotion;
body.swingYLock = ArticulationDofLock.LockedMotion;
body.swingZLock = ArticulationDofLock.LockedMotion;
}
else if (absY >= absX && absY >= absZ)
{
body.anchorRotation *= Quaternion.FromToRotation(Vector3.up, jointAxis);
body.yDrive = drive;
body.twistLock = ArticulationDofLock.LockedMotion;
body.swingYLock = hasLimits
? ArticulationDofLock.LimitedMotion : ArticulationDofLock.FreeMotion;
body.swingZLock = ArticulationDofLock.LockedMotion;
}
else
{
body.anchorRotation *= Quaternion.FromToRotation(Vector3.forward, jointAxis);
body.zDrive = drive;
body.twistLock = ArticulationDofLock.LockedMotion;
body.swingYLock = ArticulationDofLock.LockedMotion;
body.swingZLock = hasLimits
? ArticulationDofLock.LimitedMotion : ArticulationDofLock.FreeMotion;
}
Prismatic Joint
Linear motion — limits are not swapped (unlike revolute):
private static void MakePrismaticJoint(this ArticulationBody body, in JointAxis axis)
{
body.jointType = ArticulationJointType.PrismaticJoint;
var drive = new ArticulationDrive();
if (axis.HasJointLimits())
{
drive.lowerLimit = (float)axis.Lower;
drive.upperLimit = (float)axis.Upper;
}
drive.target = (float)axis.SpringReference;
if (absX >= absY && absX >= absZ)
{
body.xDrive = drive;
body.linearLockX = hasLimits
? ArticulationDofLock.LimitedMotion : ArticulationDofLock.FreeMotion;
body.linearLockY = ArticulationDofLock.LockedMotion;
body.linearLockZ = ArticulationDofLock.LockedMotion;
}
}
Other Joint Types
body.jointType = ArticulationJointType.SphericalJoint;
body.swingYLock = ArticulationDofLock.FreeMotion;
body.swingZLock = ArticulationDofLock.FreeMotion;
body.twistLock = ArticulationDofLock.FreeMotion;
body.jointType = ArticulationJointType.FixedJoint;
body.solverIterations = 0;
body.solverVelocityIterations = 0;
MakeRevoluteJoint(body, axis1);
4. Helper.Link Metadata
After joint creation, the importer stores metadata on Helper.Link for runtime use:
var linkHelper = linkObjectChild.GetComponent<Helper.Link>();
linkHelper.JointName = joint.Name;
linkHelper.JointParentLinkName = joint.ParentName;
linkHelper.JointChildLinkName = joint.ChildName;
if (joint.Type == JointType.Prismatic)
axisSpringReference = (float)joint.Axis.SpringReference;
else
axisSpringReference = SDF2Unity.CurveOrientation(...);
linkHelper.SetJointPoseTarget(axis1xyz, axisSpringReference,
axis2xyz, axis2SpringReference);
5. Adding a New Joint Type
To add support for an unsupported joint type (e.g., screw, gearbox):
-
Add a case in MakeJoint() dispatch:
case JointType.Screw:
body.MakeScrewJoint(joint.Axis);
break;
-
Create the implementation method:
private static void MakeScrewJoint(this ArticulationBody body, in JointAxis axis)
{
}
-
Decide limit semantics: revolute-style (swap via CurveOrientation) or prismatic-style (direct).
Critical Rules
- Revolute limits are always swapped —
lowerLimit = CurveOrientation(Upper), upperLimit = CurveOrientation(Lower). This accounts for the SDF→Unity coordinate handedness change.
- Prismatic limits are never swapped — direct mapping from SDF values.
- Axis alignment — always use
axis.Xyz.ToUnity().normalized for coordinate conversion, then select drive by dominant component.
- Anchor rotation —
Quaternion.FromToRotation() aligns the drive axis to the joint axis direction.
- Continuous joints have no limits — use
ArticulationDofLock.FreeMotion instead of LimitedMotion.
- Force limits — check
double.IsInfinity(axis.Effort) and map to float.MaxValue.
Common Issues
| Symptom | Cause | Fix |
|---|
| Joint rotates on wrong axis | Dominant axis detection chose wrong component | Check axis.Xyz in SDF, verify .ToUnity() conversion |
| Joint hits limits at wrong angles | Limit swap not applied (or applied to prismatic) | Revolute: must swap. Prismatic: must not swap |
| Joint doesn't move | All DOFs locked | Verify at least one lock is FreeMotion or LimitedMotion |
| Child link flies away | Missing ArticulationBody on parent chain | Ensure all links from root to child have ArticulationBody |
| Spring target wrong direction | CurveOrientation not applied to spring reference | Revolute targets need CurveOrientation; prismatic do not |
| Cross-model joint breaks hierarchy | Wrong parent chosen in SetArticulationBodyRelationship | Check model scope logic in import phase |