Skip to main content

Articulated Actors

Articulated actors represent multi-body systems composed of rigid links connected by joints. They are the primary building block for simulating robots, characters, mechanisms, and any structure where rigid parts move relative to each other through well-defined kinematic constraints.

Unlike independent rigid bodies, an articulated actor has a set of generalized joint coordinates that determine all link poses through forward kinematics. These coordinates include joint angles and displacements, but rotational joints make their combined configuration a nonlinear manifold.

Formulation

Configuration and Kinematics

The overall structure of the articulation is a rooted tree, where rigid bodies called links form the vertices of the tree graph, while the edges are joints. There is also a root joint, connecting the root link to the articulation's root frame, defined by the actor's worldFromRoot transform. Different types of joints have different configurations, e.g., a revolute joint has a single scalar angle parameterizing its possible configurations, while a spherical joint's configuration is defined by a general rotation in SO(3)\mathrm{SO}(3). The complete articulated configuration qq is then in the product manifold Q\mathcal{Q} of all the joint configuration spaces:

q=(q1,,qn)Q ,Q=j=1nQj ,q = (q_1, \ldots, q_n) \in \mathcal Q~, \qquad \mathcal Q = \prod_{j=1}^{n} \mathcal Q_j~,

where qjQjq_j\in\mathcal{Q}_j is the configuration of joint jj and nn is the number of tree joints. Generalized velocities and solver increments belong to the tangent space TqQT_q\mathcal Q; the distinction is nontrivial because of the rotational configurations of spherical and free joints. Pose updates, differences, and state combinations use manifold-aware operations that are natural extrapolations of those described for rigid-rotation reconstruction.

To simulate articulations with non-tree topologies, SuperDex Physics introduces compliant penalty constraints for cycle joints, with a formulation analogous to other constraints. As such, they become part of the energy formulation determining the equations of motion, not the kinematic description of the configuration.

Forward kinematics composes the fixed worldFromRoot placement with the joint transforms along the tree, mapping qq to the world-space rigid transform Fi(q)F_i(q) of each link ii. The corresponding articulation Jacobian Ji(q)J_i(q) maps generalized velocity vTqQv \in T_q\mathcal Q to link velocity ViV_i:

Vi=Ji(q)v ,Vi=(c˙i,ωi) .V_i = J_i(q)v~, \qquad V_i = (\dot{\mathbf c}_i,\boldsymbol{\omega}_i)~.

Here c˙i\dot{\mathbf c}_i and ωi\boldsymbol{\omega}_i are the link's world-space center-of-mass and angular velocities, respectively, using the notation defined for rigid actors. The transpose Ji(q)TJ_i(q)^T pulls link-space forces and residuals back to the cotangent space TqQT_q^*\mathcal Q. This lets each link use the same rigid-body mechanics as an independent rigid actor, while the articulation is solved in its generalized variables.

Dynamics

Articulated actors specialize the shared Lagrangian dynamics by composing each rigid link's mechanics with forward kinematics. If TiT_i and UiU_i are the rigid-body kinetic and potential energies of link ii, the per-link contributions have the form

Tlinks(q,v)=iTi ⁣(Fi(q),Ji(q)v) ,Ulinks(q)=iUi ⁣(Fi(q)) .T_{\mathrm{links}}(q,v) = \sum_i T_i\!\left(F_i(q), J_i(q)v\right)~, \qquad U_{\mathrm{links}}(q) = \sum_i U_i\!\left(F_i(q)\right)~.

The discrete incremental potential for the kinetic energy uses the rigid-actor center-of-mass and rotation discretizations computed from each link's ci(q)\mathbf c_i(q) and Ri(q)\mathbf R_i(q), so Ji(q)J_i(q) enters only when differentiating with respect to qq for the residual.

Aside from these link contributions, several other sources can contribute energy and dissipation to an articulation's dynamics. Joint inertia adds kinetic energy, joint limits and cycle joints add constraint potentials, and joint friction and damping add dissipation. Controllers, direct generalized loads, and transmissions contribute through the conservative potential UU, dissipation potential RR, or other generalized forces QotherQ_{\mathrm{other}} according to their models. Soft skinned actors strongly couple an articulation to a soft body through skinning. Contact and external constraints may couple the articulation to other actors, so their energies and residuals depend on the coupled system state rather than on qq alone. SuperDex Physics assembles all contributions and advances the coupled system with the implicit stages described in Dynamics.

Joint Types

SuperDex Physics supports six joint types:

Joint TypeEnum ValueDoFsDescription
FreeFree6Unrestricted motion (3 translational + 3 rotational). Typically used for the root link of a floating-base system.
SphericalSpherical3Three rotational degrees of freedom (ball-and-socket joint). Rotation is parameterized as a rotation vector.
RevoluteRevolute1Single rotational degree of freedom around a specified axis (hinge joint).
PrismaticPrismatic1Single translational degree of freedom along a specified axis (slider joint).
HardHard0Rigidly fuses the child link to its parent. No relative motion. Useful for fixing the root to the world or combining geometry.
CycleCycle--Creates a closed kinematic loop by connecting two links that are not in a direct parent-child relationship. Enforced as a soft spherical constraint. Declared via cycles, not joints.
note

For joint types with an axis (Revolute and Prismatic), the axis field (C++, Python) specifies the axis in the joint's local frame. It is ignored for other joint types.

Creating Articulated Actors

An articulated actor is created in a single call from parallel joints and links arrays:

  • joints[i] is the inbound joint connecting links[i] to its parent.
  • Links are listed parent-first: the root is at index 0 with parentLink = -1, and every other link's parentLink is smaller than its own index.
  • joint.parentLinkFromJoint places the joint frame in its parent link's frame; link.parentJointFromLink places the link body relative to its inbound joint frame.
  • Cycle-closing joints are declared separately in cycles (see Closed Kinematic Chains).
#include <mochi_physics/mochi_physics.h>
using namespace mochi;

// A 2-link arm: Free root + Revolute child (hinge about Z).
ArticulatedActorParams params;
params.name = "arm";
params.worldFromRoot = TransformRT{Real3{0, 0.2, 0}};
params.joints = {
{.type = ArticulatedJointType::Free},
{.type = ArticulatedJointType::Revolute,
.parentLinkFromJoint = TransformRT{Real3{0.1, 0, 0}},
.axis = Real3{0, 0, 1}},
};
params.links = {
{.parentLink = -1, .shape = rootShape, .colliderType = ColliderType::Box, .density = 1000_r},
{.parentLink = 0, .shape = childShape, .colliderType = ColliderType::Box, .density = 1000_r},
};

Actor* actor = scene->CreateArticulatedActor(params, error);

Each link becomes a queryable rigid sub-actor named "actorName/linkName"; retrieve them with GetNestedLinkActors / get_nested_link_actors.

Declarative alternative

Scenes — including articulated actors and URDF-imported skeletons — can also be authored declaratively as prefabs (.mochi_scene JSON) and loaded with prefab::AddToScene / mochi.prefab.add_to_scene.

ArticulatedJointParams Reference

ArticulatedJointParams (C++, Python) describes a single joint. joints[i] is the inbound joint of links[i].

FieldC++ TypePython NameDescription
nameDynamicStringnameJoint name (unique per actor). Auto-generated as "joint_0", ... if empty.
typeArticulatedJointTypetypeJoint type (Free, Spherical, Revolute, Prismatic, Hard). Required.
parentLinkFromJointTransformRTparent_link_from_jointJoint frame relative to the parent link's frame.
axisReal3axisAxis of motion in the joint's local frame. Revolute/Prismatic only; auto-normalized.
frictionArticulatedJointFrictionParamsfrictionPer-joint friction/damping. Ignored for Free/Hard.
inertiaoptional<real>inertiaJoint inertia coefficient [kg or kg·m²]. Ignored for Free/Hard. Default: none (0).
minLimit / maxLimitoptional<Real3>min_limit / max_limitPer-DoF limits [m or rad]. For 1-DoF joints, encode as scalar · axis.
limitStiffnessreallimit_stiffnessStiffness [N/m or N·m/rad] for limit constraints. Default: 100.
limitDampingreallimit_dampingDamping [N·s/m or N·m·s/rad] for limit constraints. Default: 0.

ArticulatedLinkParams Reference

ArticulatedLinkParams (C++, Python) describes a single rigid link. The type mirrors RigidActorParams for the per-link rigid-body properties, plus the tree-structure fields.

FieldC++ TypePython NameDescription
nameDynamicStringnameLink name (unique per actor). Auto-generated as "link_0", ... if empty.
parentLinkintparent_linkParent link index; -1 for the root. Must satisfy parentLink < i.
parentJointFromLinkTransformRTparent_joint_from_linkLink frame relative to its inbound joint frame. Rotation must be identity.
shapeShapeHandleshapeLink collision/visual geometry.
layerDynamicStringlayerContact layer name for contact filtering.
colliderTypeColliderTypecollider_typeCollision geometry type. Default: Auto.
contactContactParamscontactContact mechanics parameters.
hasGravityboolhas_gravityWhether the link is affected by gravity. Default: true.
densityoptional<real>densityUniform density [kg/m³]. Specify either density or mass.
massoptional<real>massTotal mass [kg]. Mutually exclusive with density.
centerOfMassoptional<Real3>center_of_massCoM in the link frame. Computed from geometry if unset.
momentOfInertiaoptional<Real6>moment_of_inertiaInertia tensor [ixx, ixy, ixz, iyy, iyz, izz]. Computed from geometry if unset.

ArticulatedActorParams Reference

The top-level parameters use ArticulatedActorParams (C++, Python).

FieldC++ TypePython NameDescription
nameDynamicStringnameActor name. Link actors are named "name/linkName".
worldFromRootTransformRTworld_from_rootInitial world-space transform of the actor's root frame.
jointsDynamicArray<ArticulatedJointParams>jointsPer-joint parameters. Size must equal links.
linksDynamicArray<ArticulatedLinkParams>linksPer-link parameters. Parent-first order; at least one link.
cyclesDynamicArray<ArticulatedCycleJointParams>cyclesOptional cycle-closing joints for closed loops.
skinoptional<ArticulatedSkinParams>skinOptional skinned mesh for surface collision/rendering.
jointVelocitiesoptional<DynamicArray<real>>joint_velocitiesInitial per-DoF joint velocities [m/s or rad/s]. Zero if unset.

Joint Configuration

Joint Friction and Damping

Joint friction is configured per joint via ArticulatedJointParams::friction (ArticulatedJointFrictionParams), which supports viscous damping, Coulomb (dry) friction, and an experimental Stribeck effect.

FieldDefaultDescription
viscous0.0Viscous friction coefficient [Ns/m or Nm*s/rad]. Produces a force proportional to joint velocity.
coulomb0.0Coulomb friction coefficient [N or N*m]. Constant opposing force once the joint is moving.
falloffVel1e-3Velocity threshold [m/s or rad/s] for dry friction smoothing. Smaller values are more physical but may reduce stability.
stictionExtra0.0(Experimental) Extra stiction force [N or N*m], representing the difference between peak static and dynamic friction.
stribeckVel0.0(Experimental) Stribeck velocity [m/s or rad/s] governing the static-to-dynamic friction transition sharpness.

For a one-DoF joint, let vjv_j be its tangent velocity, μv\mu_v its viscous coefficient, μc\mu_c its coulomb force, μs\mu_s its stictionExtra, vfv_f its falloffVel, and vsv_s its stribeckVel. When μ0=μc+μs>0\mu_0=\mu_c+\mu_s>0, define r=μc/μ0r=\mu_c/\mu_0 and

ϕ(x)={x2vf(1x3vf),x<vf ,2vf3+r(xvf)+(1r)vsπ2erf ⁣(xvfvs),xvf, vs>0 ,2vf3+r(xvf),xvf, vs=0 .\phi(x)= \begin{cases} \dfrac{x^2}{v_f}\left(1-\dfrac{x}{3v_f}\right), & x<v_f~, \\ \dfrac{2v_f}{3}+r(x-v_f)+(1-r)v_s\dfrac{\sqrt{\pi}}{2} \operatorname{erf}\!\left(\dfrac{x-v_f}{v_s}\right), & x\ge v_f,\ v_s>0~, \\ \dfrac{2v_f}{3}+r(x-v_f), & x\ge v_f,\ v_s=0~. \end{cases}

The joint's dissipation potential is

Rj(vj)=12μvvj2+μ0ϕ(vj) .R_j(v_j)=\frac12\mu_v v_j^2+\mu_0\phi(|v_j|)~.

The first branch smoothly increases the dry-friction magnitude from zero to the peak static value μ0\mu_0; above vfv_f, it approaches the dynamic value μc\mu_c according to the Gaussian Stribeck model. If μ0=0\mu_0=0, the dry-friction term is omitted. For a spherical joint, the same construction uses the norm of its rotational tangent velocity. Its discretization follows the shared incremental-potential formulation. Friction is ignored for Free and Hard joints.

Joint Limits

Joint limits are defined per joint on ArticulatedJointParams via minLimit / maxLimit, and enforced as spring-damper constraints whose stiffness and damping are set on the same joint (limitStiffness, limitDamping):

  • Stiffness (limitStiffness): how aggressively limits push the joint back. Default: 100 N·m/rad.
  • Damping (limitDamping): how quickly oscillations at the limit boundary are suppressed. Default: 0.

For 1-DoF joints (Revolute, Prismatic), the limit is the scalar limit value multiplied by the joint axis vector. For example, a revolute joint about Z with limits [-1, 1] rad:

min_limit = [0, 0, -1]
max_limit = [0, 0, 1]

Leave minLimit / maxLimit unset (in Python, None) for an unconstrained joint. Spherical joint limits are complex to specify; it is often easier to model them as three co-located revolute joints.

The limits of a live actor are readable via GetArticulatedDofLimits / get_articulated_dof_limits, and the limit constraints themselves via GetArticulatedJointLimitConstraints / get_articulated_joint_limit_constraints.

Joint Inertia

inertia (C++, Python) adds a scalar joint inertia coefficient aja_j. A reflected motor-rotor inertia is one possible use. For a one-DoF prismatic or revolute joint, its kinetic-energy contribution is

Tjoint,j(vj)=12ajvj2 .T_{\mathrm{joint},j}(v_j)=\frac12 a_jv_j^2~.

This kinetic contribution is discretized using the shared incremental-potential formulation.

For a spherical joint, the continuous contribution is 12ajωj22\tfrac12a_j\Vert\omega_j\Vert_2^2, where ωj\omega_j is the rotational tangent velocity. Its discrete contribution uses the same manifold-aware rotational incremental-potential treatment as a rigid body with isotropic moment-of-inertia tensor aj1a_j\mathbf{1}; the scalar parameter does not specify a general 3×33\times3 tensor. Units are [kg] for translation DoFs and [kg·m²] for rotation DoFs. Joint inertia is ignored for Free/Hard joints and can be changed live (see below).

Closed Kinematic Chains

To create a closed loop (e.g., a four-bar linkage), define the tree topology as usual and add cycle joints in cycles. A cycle joint connects a child link to a parent link that is not its tree-parent, enforced as a soft spherical constraint at a pivot in the child link's frame.

ArticulatedActorParams params;
// 4 revolute-jointed links forming an open chain ...
params.joints = {
{.type = ArticulatedJointType::Revolute, .axis = Real3{0, 0, 1}},
{.type = ArticulatedJointType::Revolute, .axis = Real3{0, 0, 1}},
{.type = ArticulatedJointType::Revolute, .axis = Real3{0, 0, 1}},
{.type = ArticulatedJointType::Revolute, .axis = Real3{0, 0, 1}},
};
params.links = {
{.parentLink = -1, .shape = barShape, .density = 1000_r},
{.parentLink = 0, .shape = barShape, .density = 1000_r},
{.parentLink = 1, .shape = barShape, .density = 1000_r},
{.parentLink = 2, .shape = barShape, .density = 1000_r},
};
// ... closed into a loop by tying link 3 back to link 0.
params.cycles = {
{.parentLink = 0, .childLink = 3, .jointFromChildLink = TransformRT{Real3{0.1_r, 0, 0}}},
};

ArticulatedCycleJointParams (C++, Python) has parentLink, childLink, jointFromChildLink (the pivot in the child link's frame), and stiffness (default 50000).

note

Contact is automatically disabled between links that are (a) directly adjacent, (b) connected via hard joints, or (c) connected via shapeless (dummy) links. Use EnableActorContactSymmetric / enable_actor_contact_symmetric to override this for specific link pairs.

Skinned Surface

An articulated actor can carry an optional skinned surface: a single triangle mesh that is deformed by the underlying links via linear blend skinning. This gives the articulation one continuous surface for collision and rendering, instead of the separate per-link collision shapes.

The skin is a colliding-only surface — it detects contact against other actors, but others do not detect against it (the per-link shapes remain the colliders). Attach it by setting skin on ArticulatedActorParams before creating the actor.

ArticulatedActorParams params;
// ... joints and links as above ...
params.skin = ArticulatedSkinParams{
.shape = skinShape, // a triangle-mesh shape with skinning weights and link indices
.layer = "Skin",
};

Actor* actor = scene->CreateArticulatedActor(params, error);

ArticulatedSkinParams Reference

The skin is configured with ArticulatedSkinParams (C++, Python).

FieldC++ TypePython NameDescription
shapeShapeHandleshapeTriangle-mesh shape for the skinned surface.
layerDynamicStringlayerContact layer name for the skin (for contact filtering).
contactContactParamscontactContact mechanics parameters for the skin.
boundaryElementTypeActorBoundaryElementTypeboundary_element_typeFinite-element type for the skin's boundary/contact integrals. Default: Default.
boundarySubsamplingoptional<BoundarySubsamplingParams>boundary_subsamplingOptional subsampling to reduce contact-integral cost. Best combined with boundaryElementType = P1Q1.
note

This surface follows the links via skinning; it is not itself physically deformable. A deformable skin is possible, but it requires integrating the articulated actor with soft actors into a soft skinned actor.

Working with Articulations at Runtime

The sections above describe how to define an articulated actor at creation time. A live actor also exposes a rich runtime API for introspection, reading and manipulating state, actuation, and live re-modeling. The Articulations examples tour this whole interface as worked examples. (Controller-based actuation is covered separately under Controllers.)

Lifecycle and Identity

CreateArticulatedActor returns an Actor* and a stable ActorHandle. Each link is a nested rigid sub-actor; enumerate them with GetNestedLinkActors and look them up with GetActor. Destroying the articulated actor destroys its links (and any constraints on them).

Actor* actor = scene->CreateArticulatedActor(params, error);
Span<ActorHandle const> links = actor->GetNestedLinkActors(error);
Actor* endEffector = scene->GetActor(links.back());
scene->DestroyActor(actor->GetHandle());

Introspection

GetArticulatedShapeInfo returns ArticulatedShapeInfo, a one-stop dump of the topology (linkNames, jointNames, jointTypes, parents, joint axes and DoF layout). GetNumDofs returns the total DoF count. Per-DoF limits are available via GetArticulatedDofLimits, and the limit constraints themselves via GetArticulatedJointLimitConstraints.

ArticulatedShapeInfo info = actor->GetArticulatedShapeInfo(error);
int numDofs = actor->GetNumDofs();
DynamicArray<Real2> dofLimits(numDofs);
actor->GetArticulatedDofLimits(dofLimits, error); // [min, max] per DoF
Span<Constraint* const> limits = actor->GetArticulatedJointLimitConstraints(error);

Reading State (Forward Kinematics)

Read the joint-space pose, the world transforms of every link, and the joint velocities into pre-sized output containers.

DynamicArray<real> pose(actor->GetNumDofs());
actor->GetArticulatedPose(pose, error);
DynamicArray<TransformRT> linkTransforms(numLinks);
actor->GetArticulatedLinkTransforms(linkTransforms, error);
DynamicArray<real> velocities(actor->GetNumDofs());
actor->GetArticulatedJointVelocities(velocities, error);

Manipulating State Directly

Set the pose from joint-space DoFs, from link transforms (IK-style), or set joint velocities. Pose-space math composes deltas in the tangent space (so spherical DoFs behave correctly), and the end-effector Jacobian is read from a nested link sub-actor.

actor->SetArticulatedPoseFromJoints(pose, error);            // joint-space DoFs
actor->SetArticulatedPoseFromLinks(worldFromLinks, error); // IK-style
actor->SetArticulatedJointVelocities(velocities, error);
actor->AddArticulatedDeltaToPose(pose, delta, outPose, error);
actor->ComputeArticulatedPoseDelta(poseA, poseB, outDelta, error);
Span<real const> jacobian = endEffector->GetArticulatedJacobian(error); // per nested link

Actuating without a Controller

Apply generalized forces to specific DoFs, or pin DoFs with a boundary condition (e.g. freeze a slider).

actor->SetExternalForcesOnDofs(dofIndices, forceValues, error);
actor->ClearExternalForces();
actor->AddBoundaryConditionDofsWorld(dofIndices, dofValues, error); // pin DoFs
actor->ClearBoundaryConditions();

Live Joint Modeling

Per-joint friction and joint inertia can be read and changed mid-simulation (one entry per joint).

Span<ArticulatedJointFrictionParams const> friction = actor->GetArticulatedJointFrictionParams(error);
actor->SetArticulatedJointFrictionParams(friction, error);
Span<real const> inertia = actor->GetArticulatedJointInertiaParams(error);
actor->SetArticulatedJointInertiaParams(inertia, error);

Controlling Contact

Contact is filtered coarsely by string layers and finely by per-actor overrides. The nested link sub-actors let you toggle contact for an individual link (e.g. only an end-effector collides with a target).

scene->EnableLayerContactSymmetric("Pendulum", "Ball", false, error);
scene->EnableActorContactSymmetric(
linkHandle,
ballHandle,
true,
IncludeNestedActors::No,
error);
bool enabled = scene->IsLayerContactEnabled("EndEffector", "Ball");
int numLayers = scene->GetNumContactLayers();

Mass, Root, and Center of Mass

GetMass and the root transform are whole-articulation queries. Center of mass and linear/angular velocity are per-rigid-body queries, so read them from a nested link sub-actor rather than the top-level articulated actor. (The articulated equivalent of SetVelocity is SetArticulatedJointVelocities.)

real mass = actor->GetMass(error);
TransformRT root = actor->GetRootTransform();
actor->SetRootTransform(root, error); // teleports the whole actor
TransformRT com = endEffector->GetCenterOfMassTransform(error); // per nested link

Controllers

The Pose Controller adds implicit PD constraints to an articulated actor after creation. It can track joint-space targets, Cartesian link positions and rotations, or a combination of all three. See the Pose Controller example for a worked hybrid, joint-only, and link-only example.

Examples

  • Plain — Double Pendulum on Rail: builds a double pendulum on a rail in code and tours the runtime API described above.

    • Python - uv run --no-project superdex_physics/examples/example_articulations_double_pendulum_on_rail.py
    • Prefab — superdex_physics/assets/samples/articulations_double_pendulum_on_rail.mochi_scene
  • Skinned — Skinned Double Pendulum: adds a linear-blend-skinned surface as a colliding-only skin.

    • Python - uv run --no-project superdex_physics/examples/example_articulations_skinned_double_pendulum.py
    • Prefab — superdex_physics/assets/samples/articulations_skinned_double_pendulum.mochi_scene
  • Inverse Kinematics — Quasistatic IK solver that computes joint configurations for target end-effector positions.
  • Pose Controller — Implicit PD controller for joint-space and Cartesian link tracking during dynamic simulation.
  • Shapes — Collision geometry types available for link shapes.
  • Rigid Actors — The underlying rigid body type used for individual links.
  • Soft Skinned Actors — Articulated skeletons coupled with deformable FEM skin.