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 builds, for each body \(k \in \{i,j\}\), an orthonormal basis \((\hat{\mathbf{t}}_k, \hat{\mathbf{n}}_k, \hat{\mathbf{b}}_k)\) aligned with the joint axis at setup, where

\[ \hat{\mathbf{t}}_k = \frac{\mathbf{x}^1_k - \mathbf{x}^0_k}{\|\mathbf{x}^1_k - \mathbf{x}^0_k\|}, \quad \hat{\mathbf{b}}_k = \hat{\mathbf{t}}_k \times \hat{\mathbf{n}}_k, \]

and the \((\hat{\mathbf{n}}, \hat{\mathbf{b}})\) pair is stored per body in the layout \([\hat{\mathbf{n}}_k, \hat{\mathbf{b}}_k]\). The current relative angle between the two bodies is extracted as

\[ \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 around \(+\hat{\mathbf{t}}\) (counterclockwise when viewed along \(+\hat{\mathbf{t}}\)).

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).