Skip to content

Affine Body Revolute Joint

References:

A unified newton barrier method for multibody dynamics

#18 AffineBodyRevoluteJoint

The Affine Body Revolute Joint constitution constrains two affine bodies to rotate relative to each other about a shared axis. This joint allows for rotational motion while restricting translational movement between the two bodies.

Affine Body Revolute Joint

We assume 2 affine body indices \(i\) and \(j\), each with their own state vector (to be concerete, transform) \(\mathbf{q}_i\) and \(\mathbf{q}_j\) as defined in the Affine Body constitution.

The joint axis in world space is defined by 2 points \(\mathbf{x}^0\) and \(\mathbf{x}^1\). At the beginning of the simulation, the relationships are kept:

\[ \mathbf{x}^0 = \mathbf{J}^0_i \mathbf{q}_i = \mathbf{J}^0_j \mathbf{q}_j, \]

and,

\[ \mathbf{x}^1 = \mathbf{J}^1_i \mathbf{q}_i = \mathbf{J}^1_j \mathbf{q}_j, \]

intuitively \(\mathbf{J}^0\) and \(\mathbf{J}^1\) tell the local coordinates of the two points on affine bodies \(i\) and \(j\).

The energy function for the Affine Body Revolute Joint is defined as:

\[ E = \frac{K}{2} \cdot \left( \| \mathbf{J}^0_i \mathbf{q}_i - \mathbf{J}^0_j \mathbf{q}_j \|^2 + \| \mathbf{J}^1_i \mathbf{q}_i - \mathbf{J}^1_j \mathbf{q}_j \|^2 \right), \]

where \(K\) is the stiffness constant of the joint, we choose \(K=\gamma (m_i + m_j)\), where \(\gamma\) is a user defined strength_ratio parameter, and \(m_i\) and \(m_j\) are the masses of the two affine bodies.

Angle State

The revolute joint tracks a scalar joint angle \(\theta\) that extra-per-joint constitutions (driving joint, angle limit, external torque) share. The backend constructs per body \(k \in \{i,j\}\) an orthonormal basis \((\hat{\mathbf{t}}_k, \hat{\mathbf{n}}_k, \hat{\mathbf{b}}_k)\) from the hinge geometry embedded in symbolic generation and Jacobian layout for this constitution (see sym/affine_body_revolute_joint alongside deployed kernels): \(\hat{\mathbf{t}}_k\) is the unit tangent along body \(k\)’s hinge segment; \(\hat{\mathbf{b}}_k = \hat{\mathbf{t}}_k \times \hat{\mathbf{n}}_k\). Pair \([\hat{\mathbf{n}}_k, \hat{\mathbf{b}}_k]\) is the rotating perpendicular frame used downstream. Current relative angle is

\[ \cos\theta = \frac{\hat{\mathbf{n}}_i \cdot \hat{\mathbf{n}}_j + \hat{\mathbf{b}}_i \cdot \hat{\mathbf{b}}_j}{2}, \quad \sin\theta = \frac{\hat{\mathbf{b}}_i \cdot \hat{\mathbf{n}}_j - \hat{\mathbf{n}}_i \cdot \hat{\mathbf{b}}_j}{2}, \]
\[ \theta = \operatorname{atan2}(\sin\theta,\; \cos\theta) \in (-\pi, \pi]. \]

The sign follows the right-hand rule about the child tangent \(+\hat{\mathbf{t}}_{\mathrm{child}}\) (see below): counterclockwise when viewed along \(+\hat{\mathbf{t}}_{\mathrm{child}}\).

Parent vs. child along the axis

Map left geometry (l_*, body \(i\)) to parent and right geometry (r_*, body \(j\)) to child, matching typical kinematic-tree layout.

At a valid configuration, the mapped joint axis rays are anti-parallel on the shared line:

\[ \hat{\mathbf{t}}_{\mathrm{parent}} = \hat{\mathbf{t}}_i = -\,\hat{\mathbf{t}}_{\mathrm{child}} = -\,\hat{\mathbf{t}}_j, \]

because each body carries its edge from attachment point \(\mathbf{x}^0_k\) to \(\mathbf{x}^1_k\) while the pairing links endpoints across bodies along the same physical axis.

Angular state \(\theta\), angle, limits, and driving use both bodies’ bases symmetrically via the formulas above. Which ray defines positive external torque is specified only for constitution Affine Body Revolute Joint External Force (UID 668): actuator torque follows child-positive \(+\hat{\mathbf{t}}_{\mathrm{child}}\); see Parent vs. child axis and positive torque.

State Update

At the end of each time step, the backend writes back the current relative angle, wrapped to \((-\pi, \pi]\), to the angle edge attribute:

\[ \theta_{\text{current}} = \operatorname{map}_{(-\pi,\pi]}\left(\theta(\mathbf{q}) + \alpha_0\right), \]

where \(\theta(\mathbf{q})\) is extracted from the formulas above and \(\alpha_0\) is init_angle, a user-facing offset that shifts the "zero" of the reported value (default 0.0). The angle attribute is therefore available to any frontend and to all extra-per-joint constitutions built on top of this joint (driving joint, limit, external torque). When an external state write replaces the body DOF \(\mathbf{q}\) (e.g. via AffineBodyStateAccessorFeature), the backend re-syncs current_angles synchronously through the GlobalJointDofManager.

Convention for all user-facing bounds

All user-facing angle quantities on this joint (limit/lower, limit/upper on AffineBodyRevoluteJointLimit; aim_angle on AffineBodyDrivingRevoluteJoint) are expressed in the same "current_* frame" as the reported angle attribute. A user can therefore set them directly against the value they read back from angle, without having to reason about init_angle. Wherever init_angle appears inside a solver-side formula, it is subtracted from the user-facing quantity to translate it into the raw frame \(\theta(\mathbf{q})\).

Attributes

On the joint geometry (1D simplicial complex), on edges (one edge per joint):

  • l_geo_id (IndexT): scene geometry slot id for the left body \(i\)
  • r_geo_id (IndexT): scene geometry slot id for the right body \(j\)
  • l_inst_id (IndexT): instance index within the left geometry
  • r_inst_id (IndexT): instance index within the right geometry
  • strength_ratio: \(\gamma\) in \(K = \gamma(m_i + m_j)\) above
  • angle (Float): \(\theta_{\text{current}}\), current relative joint angle in radians, wrapped to \((-\pi, \pi]\). Written by the backend every time step (read-only for the user).
  • init_angle (Float): \(\alpha_0\), initial angle offset added to the extracted relative angle. Default 0.0.

When the joint is created via Local create_geometry, optional edge attributes (each Vector3) supply local joint geometry: l_position0, l_position1 (left body local frame), r_position0, r_position1 (right body local frame).