Skip to main content

Constraints

Constraints in SuperDex Physics model interactions and attachments between actors. They are the primary mechanism for coupling degrees of freedom (DoFs) across different entities in a simulation, enabling joints, springs, attachments, and kinematic targets.

Constraints vs. articulated actors

Modeling a jointed structure (such as a double pendulum) with constraints is the preferred approach when joints are added or removed at runtime. For a fixed joint structure, prefer an articulated actor instead — it is more efficient and robust.

Mathematical Formulation

Constraints are compliant: they contribute an elastic potential and a dissipation potential to the incremental potential of each implicit stage, rather than enforcing an algebraic condition exactly. The notation below follows the dynamics page: qq is the generalized configuration, qi0q_i^0 the stage-start configuration, and Δti\Delta t_i the stage duration.

Constraint Value

Each constraint defines a constraint value gRmg\in\mathbb R^m that vanishes when the constraint is satisfied, together with its Jacobian

J=gqRm×n ,J=\frac{\partial g}{\partial q}\in\mathbb R^{m\times n}~,

taken with respect to the nn degrees of freedom of the involved actors. The Constraint Types below give gg for each type; GetDeviation (get_deviation in Python) returns its current value.

Bilateral constraints (joints, attachments, kinematic targets) have a smooth gg that may take either sign componentwise. Range constraints instead measure a one-sided violation. Written for a single bounded DoF qjq_j,

gj=max(qjuj,  ljqj,  0) ,g_j=\max\left(q_j-u_j,\;l_j-q_j,\;0\right)~,

for bounds ljqjujl_j\leq q_j\leq u_j, so gjg_j and the corresponding row of JJ vanish inside the bounds. Each range type below bounds its own scalar measure — a joint angle, a rotation-vector component, a slide offset — in place of qjq_j. A range constraint is inactive, and contributes nothing, when every component is within its bounds both in the current configuration and at the stage start; the stage-start condition matters because the damping term below differences against g(qi0,p^i0)g(q_i^0,\hat p_i^0).

Target-bearing constraints depend on a prescribed target in addition to the configuration, g=g(q,p^)g=g(q,\hat p). The target is fixed data within a stage.

Constraint Rate

The rate of the constraint value is discretized as a stage-local difference,

g˙    ΔgΔti ,Δg=g ⁣(q,p^)g ⁣(qi0,p^i0) .\dot g\;\approx\;\frac{\Delta g}{\Delta t_i}~, \qquad \Delta g = g\!\left(q,\hat p\right)-g\!\left(q_i^0,\hat p_i^0\right)~.

Two properties follow. First, the difference is taken on gg itself rather than as JvJv, which makes the discrete dissipation term exactly integrable in qq and therefore admits an incremental potential. Second, Δg\Delta g also differences the target: moving a target between stages produces a damping force. Use UpdateOldTarget (update_old_target in Python) to reset p^i0\hat p_i^0 when teleporting a target, so that no spurious damping transient is generated.

Incremental Potential

A constraint with stiffness kk and damping dd contributes

Φic(q)=k2g22contribution to U+d2ΔtiΔg22ΔtiR\Phi^{\mathrm{c}}_i(q) =\underbrace{\frac{k}{2}\left\Vert g\right\Vert_2^2}_{\text{contribution to }U} +\underbrace{\frac{d}{2\Delta t_i}\left\Vert\Delta g\right\Vert_2^2}_{\Delta t_i R}

to the stage incremental potential, corresponding to the elastic potential Uc=k2g22U^{\mathrm{c}}=\tfrac{k}{2}\Vert g\Vert_2^2 and the dissipation potential Rc=d2g˙22R^{\mathrm{c}}=\tfrac{d}{2}\Vert\dot g\Vert_2^2 of the continuous system.

Saturation

A positive saturation gˉ\bar g replaces the quadratic elastic term with

Uc=kgˉρ ⁣(g2) ,ρ(x)={x2gˉx33gˉ2,0x<gˉ ,xgˉ3,xgˉ ,U^{\mathrm{c}}=k\,\bar g\,\rho\!\left(\left\Vert g\right\Vert_2\right)~, \qquad \rho(x)= \begin{cases} \displaystyle \frac{x^2}{\bar g}-\frac{x^3}{3\bar g^2}, & 0\leq x<\bar g~, \\ \displaystyle x-\frac{\bar g}{3}, & x\geq\bar g~, \end{cases}

using the same C1C^1 ramp as the regularized Coulomb friction in contact. Since ρ[0,1]\rho'\in[0,1], the elastic force magnitude is smoothly capped at kgˉk\bar g, reached once g2gˉ\Vert g\Vert_2\geq\bar g. The damping term is unaffected by saturation.

note

Saturation is not a clamp applied to the linear spring: ρ(x)2x/gˉ\rho'(x)\approx 2x/\bar g for small xx, so the effective stiffness near g=0g=0 is 2k2k rather than kk. Saturation is better understood as a force of nearly constant magnitude kgˉk\bar g acting along gg, regularized over the length scale gˉ\bar g.

Gradient and Approximate Hessian

The stage residual contributed by a constraint is qΦic\nabla_q\Phi^{\mathrm{c}}_i,

rc=JT(κg+dΔtiΔg) ,κ={k,saturation disabled ,kgˉρ ⁣(g2)g2,saturation enabled ,r^{\mathrm{c}}=J^T\left(\kappa\,g+\frac{d}{\Delta t_i}\Delta g\right)~, \qquad \kappa= \begin{cases} k, & \text{saturation disabled}~, \\ \displaystyle k\,\bar g\,\frac{\rho'\!\left(\Vert g\Vert_2\right)}{\Vert g\Vert_2}, & \text{saturation enabled}~, \end{cases}

where the ratio ρ(x)/x\rho'(x)/x is evaluated in a form that stays regular as x0x\to0. The generalized constraint force is rc-r^{\mathrm{c}}, which is what GetForce (get_force in Python) reports.

The residual derivative drops the term involving the second derivative of gg, which is indefinite in general, yielding the Gauss–Newton approximation

Kc(κ+dΔti)JTJ0 .K^{\mathrm{c}}\approx\left(\kappa+\frac{d}{\Delta t_i}\right)J^TJ\succeq 0~.

Constraint Types

The table below summarizes every constraint type. mm is the dimension of the constraint value gg, and nn is the number of degrees of freedom it couples. Kind distinguishes bilateral constraints, whose gg is smooth and signed, from unilateral ones, whose gg is the one-sided violation defined in the Constraint Value section and which contribute nothing while satisfied. Target marks the types that carry a runtime-settable kinematic target.

TypeActorsmmnnKindTarget
RigidSphericalJoint2312Bilateral
RigidPrismaticJoint239Mixed
JointRotationRange236Unilateral
JointRotationTracking236Bilateral
RigidPivotPosition136Bilateral
RigidPivotToRigidTarget136Bilateral
RigidPivotRotation133Bilateral
DeformableNodePosition133Bilateral
DeformableNodeToDeformableNode236Bilateral
DeformableNodeToRigid239Bilateral
ArticulatedSingleDofTarget111Bilateral
ArticulatedSingleDofRange111Unilateral
Articulated3dRotationTarget133Bilateral
Articulated3dRotationRange133Unilateral
RodElementRotationToRigid2311Bilateral

The formulas below use the following notation. TAT_A is the world transform of body AA, with translation tA\mathbf t_A and rotation RAR_A, so TA(r)T_A(\mathbf r) is the world position of a point r\mathbf r given in AA's local frame. Deformable nodes have reference position X\mathbf X and displacement u\mathbf u. Log(R)\mathrm{Log}(R) is the rotation vector of a rotation RR. Hats denote prescribed targets.

Componentwise rotation bounds

The rotational range constraints bound each component of a rotation vector independently. Because rotations do not commute, these components are not independent rotations about fixed axes, and the bounds are only an intuitive description of a range of motion for moderate angles.

Rigid-Rigid Joints

RigidSphericalJoint

Spherical (ball-and-socket) joint between two rigid bodies. The constraint value is the world-space displacement between two pivot points, each rigidly attached to its respective body:

g=TA(rA)TB(rB) ,g = T_A(\mathbf{r}_A) - T_B(\mathbf{r}_B)~,

where rA\mathbf{r}_A and rB\mathbf{r}_B are the local pivot positions.

RigidPrismaticJoint

Constrains the relative translation of two rigid bodies transverse to a sliding axis, and optionally limits travel along it. Let R0R_0 be the joint frame expressed in body AA's local coordinates: it is specified in world coordinates at creation and converted to AA-local then, so it is constant thereafter. Its zz axis is the free axis. The relative translation in that frame is

s=R01RA1(tBtA)sref .\mathbf s = R_0^{-1}R_A^{-1}\left(\mathbf t_B-\mathbf t_A\right)-\mathbf s_{\mathrm{ref}}~.

The two transverse components are constrained bilaterally, g1=s1g_1=s_1 and g2=s2g_2=s_2, while g3g_3 is the signed violation of s3s_3 against the optional travel limits: s3us_3-u above the upper limit, s3ls_3-l below the lower one, and zero in between.

warning

This constraint does not restrict relative rotation, and its travel limits are not reported by GetLimitMinValues / GetLimitMaxValues. Combine it with a JointRotationTracking constraint to obtain a full prismatic joint.

JointRotationRange

Limits the relative rotation between two rigid bodies, with independent lower and upper bounds per component. The relative rotation is expressed in the joint reference frame R0R_0, again stored in body AA's local coordinates, and measured against a reference rotation RrefR_{\mathrm{ref}}:

rAB=Log ⁣(R01RA1RBR0Rref1) .\mathbf{r}_{AB} =\mathrm{Log}\!\left(R_0^{-1}R_A^{-1}R_B R_0 R_{\mathrm{ref}}^{-1}\right)~.

Each gjg_j is the one-sided violation of rAB,j\mathbf{r}_{AB,j} against its bounds. By default (rangeAroundRest), RrefR_{\mathrm{ref}} is the relative rotation observed at creation, so the bounds are relative to the rest pose rather than absolute in the joint frame.

JointRotationTracking

Drives the relative rotation between two rigid bodies toward a target rotation R^\hat R, which is the orientation of BB relative to AA expressed in AA's local frame:

g=Log ⁣((RAR^R0)1RBR0) .g=\mathrm{Log}\!\left(\left(R_A\hat R R_0\right)^{-1}R_B R_0\right)~.

The target is initialized to the relative rotation observed at creation, so the constraint starts satisfied, and is updated at runtime with SetTargetRotation. It is used both to track measured joint angles, such as finger joints driven by a hand tracking system, and to supply the missing rotational restriction of a RigidPrismaticJoint.

Rigid Pivots to Kinematic Targets

Each of these constrains a single rigid body against prescribed target data; none of them couples two actors.

RigidPivotPosition

Constrains a pivot point on a rigid body to a target position:

g=T(r)p^ ,g = T(\mathbf{r}) - \hat{\mathbf{p}}~,

where T(r)T(\mathbf{r}) is the world-space position of the pivot and p^\hat{\mathbf{p}} is the prescribed target position.

RigidPivotToRigidTarget

Constrains a pivot point on a rigid body to the position that the same local point would occupy under a prescribed target transform T^\hat T:

g=T(r)T^(r) .g = T(\mathbf{r}) - \hat T(\mathbf{r})~.

This differs from RigidPivotPosition only in that the target is a full rigid motion, so the target point also moves with the target's rotation. Despite the name, it involves a single actor: T^\hat T is prescribed target data, not the state of a second body. Use RigidSphericalJoint to couple a point on one body to a point on another.

RigidPivotRotation

Constrains a pivot frame on a rigid body to a target orientation:

g=Log ⁣(RRR^1) ,g = \mathrm{Log}\!\left(R\,R_\ell\,\hat R^{-1}\right)~,

where RR is the body rotation, RR_\ell is the pivot frame in the body's local coordinates, and R^\hat R is the prescribed target rotation in world coordinates.

Deformable Couplings

DeformableNodePosition

Fixes a deformable-actor node to a prescribed world-space position:

g=T(X+u)p^ .g = T(\mathbf{X} + \mathbf{u}) - \hat{\mathbf{p}}~.

This is used for boundary conditions such as pinning a shell corner or fixing the base of a deformable object.

DeformableNodeToDeformableNode

A spring connecting nodes on two deformable actors:

g=TA(XA+uA)TB(XB+uB) .g = T_A(\mathbf{X}_A + \mathbf{u}_A) - T_B(\mathbf{X}_B + \mathbf{u}_B)~.

DeformableNodeToRigid

Attaches a deformable-actor node to a pivot point on a rigid body:

g=TR(r)TD(X+u) .g = T_R(\mathbf{r}) - T_D(\mathbf{X} + \mathbf{u})~.

By default (fixToDeformablePos), the rigid-frame pivot r\mathbf{r} is computed at creation so that the constraint starts at zero deviation, rather than being supplied by the caller.

Articulated Constraints

These act directly on the reduced pose of an articulated actor, so they involve no world-space frames.

ArticulatedSingleDofTarget

Drives a single joint DoF toward a target value, g=θθ^g = \theta - \hat\theta. Used for articulated actor control.

note

The single-DoF types apply only to translational DoFs and to single-DoF rotational (revolute) joints. For the rotation of a spherical or free joint, use the 3D variants below.

ArticulatedSingleDofRange

Limits a single joint DoF to [l,u][l, u], giving the one-sided violation g=max(θu,  lθ,  0)g = \max\left(\theta - u,\; l - \theta,\; 0\right). Used to enforce mechanical joint limits.

Articulated3dRotationTarget

Drives the rotation of a spherical or free joint toward a target orientation:

g=Log ⁣(RjR^1) ,g = \mathrm{Log}\!\left(R_j\hat R^{-1}\right)~,

where RjR_j is the joint rotation from the reduced pose. The target is expressed in the joint's local frame relative to its rest orientation, so the identity corresponds to the rest pose.

Articulated3dRotationRange

Limits the rotation of a spherical or free joint, applying independent bounds to each component of Log(Rj)\mathrm{Log}(R_j) and returning the one-sided violation componentwise.

Rod Couplings

RodElementRotationToRigid

Drives the rotation of a rod element relative to a rigid body toward a target rotation R^\hat R, which is the orientation of the rod element relative to the body expressed in the body's local frame. The reference frame R0R_0 is stored in those same local coordinates:

g=Log ⁣((RRR^R0)1ReR0) ,g=\mathrm{Log}\!\left(\left(R_R\hat R R_0\right)^{-1}R_e R_0\right)~,

where RRR_R is the rigid body rotation and ReR_e is the world-space rotation of the rod element's material frame. As with JointRotationTracking, the target is initialized to the relative rotation observed at creation and updated with SetTargetRotation; the default behavior therefore preserves the creation-time relative rotation rather than aligning the rod element with the body frame. The n=11n=11 coupled DoFs are the 3 rigid rotational DoFs and the 8 DoFs of the rod element's two nodes.

Code Examples

Pinning a Soft Body Node (DeformableNodePosition)

Pin a specific node of a deformable actor to a fixed position in world space. This is useful for boundary conditions such as fixing a shell corner or anchoring the base of a soft body.

DeformableNodePositionConstraintParams params;
params.actor = softActor->GetHandle();
params.nodeIndex = 0; // pin the first node
params.position = Real3{0_r, 1_r, 0_r}; // target world position
params.stiffness = 1e6_r; // penalty stiffness
Constraint* constraint = scene->CreateDeformableNodePositionConstraint(params, error);

Connecting Two Rigid Bodies (RigidSphericalJoint)

Create a ball-and-socket joint between two rigid bodies. Each body specifies a local pivot point; the constraint penalizes the distance between those two pivots in world space.

RigidSphericalJointConstraintParams params;
params.actorA = bodyA->GetHandle();
params.actorB = bodyB->GetHandle();
params.localPosA = Real3{0.5_r, 0_r, 0_r}; // pivot on body A
params.localPosB = Real3{-0.5_r, 0_r, 0_r}; // pivot on body B
params.stiffness = 1e6_r;
Constraint* constraint = scene->CreateRigidSphericalJointConstraint(params, error);

Attaching a Deformable Actor to a Rigid Body (DeformableNodeToRigid)

Couple a deformable mesh node to a point on a rigid body. This is commonly used to attach a deformable actor to a rigid handle.

DeformableNodeToRigidConstraintParams params;
params.deformableActor = softActor->GetHandle();
params.rigidActor = rigidActor->GetHandle();
params.deformableNodeIndex = 10; // which deformable-actor node
params.fixToDeformablePos = true; // use the node's current position
params.stiffness = 1e4_r;
Constraint* constraint = scene->CreateDeformableNodeToRigidConstraint(params, error);

Working with Constraints at Runtime

The Constraint Types above describe how to define a constraint at creation time. Every Constraint also exposes a uniform runtime interface: the same methods work on all types, but many are type-specific and report a graceful Error (in Python, methods on Constraint raise mochi.Error) when they don't apply. The constraints page tours this whole interface as a worked example.

Lifecycle and Identity

C++ Scene constraint factory methods return a Constraint* and a stable ConstraintHandle. Look a constraint back up with GetConstraint, iterate all of them with ForEachConstraint, and remove one with DestroyConstraint (by pointer or handle). Python provides the corresponding get_constraint, for_each_constraint, and destroy_constraint methods. A constraint is auto-destroyed when any actor it references is destroyed.

ConstraintHandle handle = constraint->GetHandle();
Constraint* same = scene->GetConstraint(handle); // look up by handle
scene->ForEachConstraint([](Constraint* c) { /* ... */ }); // enumerate
scene->DestroyConstraint(handle); // explicit removal
// scene->DestroyActor(actorHandle) also removes every constraint on that actor

Introspection

The generic interface reports how a constraint is wired up, regardless of its type. GetNumActors (get_num_actors in Python) is a useful discriminator — a world-anchored RigidPivotPosition involves one actor, while a body-to-body RigidSphericalJoint involves two.

ConstraintType type = constraint->GetType();
int numActors = constraint->GetNumActors();
for (int i = 0; i < numActors; ++i) {
Actor* actor = constraint->GetActor(i);
Span<int const> dofs = constraint->GetDofIndicesForActor(i); // affected DoFs
}

Parameters

Stiffness, damping, and saturation can be read and changed at any time. See Mathematical Formulation for how each enters the stage problem; in short, saturation caps the elastic force magnitude at stiffness * saturation, and a negative value disables the cap.

constraint->SetStiffness(1e5_r, error);   // [N/m] (or [N*m/rad] for rotational constraints)
constraint->SetDamping(20_r, error); // [N*s/m] (or [N*m*s/rad])
constraint->SetSaturation(-1_r, error); // force cap = stiffness * saturation; negative disables

Kinematic Targets

Target-bearing constraints track a prescribed target. Updating the target each step animates it — for example, driving a RigidPivotPosition anchor along a path to create a moving base. Use UpdateOldTarget to teleport the target without producing a spurious damping transient.

pivot->SetTargetPosition(Real3{x, y, z}, error);  // RigidPivotPosition
// SetTargetRotation / SetTargetDof apply to rotation- / DoF-target constraints

Diagnostics and Queries

GetDeviation returns the current constraint value gg (the position/rotation error) and needs no setup. GetForce returns the constraint force but requires a ConstraintForce query to be registered before stepping. The query is identified by the C++ QueryType::ConstraintForce or corresponding Python QueryType value. IsQuerySupported reports whether a query type applies.

constraint->RegisterQuery(QueryType::ConstraintForce, error);  // once, before Step
scene->Step(dt, error);
DynamicArray<real> deviation = constraint->GetDeviation(); // constraint value g
Span<real const> force = constraint->GetForce(error); // needs the query above

Limits

Range constraints (e.g. JointRotationRange, ArticulatedSingleDofRange) expose their bounds via GetLimitMinValues / GetLimitMaxValues; unconstrained components report ±\pm\infty. Python provides get_limit_min_values / get_limit_max_values.