Physics3D
Table of Contents
The Physics3D bundle is a collection of abstract models designed as a foundation for modeling a wide range of engineering physics.
Using the OpenPLX language, core types such as RigidBody and Mate can be specialized into components like links and joints for robotics, booms and hydraulic cylinders for heavy machinery, or tires and suspensions for vehicle modeling.
Mate interactions, including Hinge and Prismatic, provide the building blocks for simulating complex mechanical systems.
A central feature of the Physics3D bundle is the automatic assembly of the entire System along with its subsystems. As long as the included rigid bodies and mates define an unambiguous configuration, mechanisms and machines are assembled automatically through the implemented SNAP algorithm.
The Physics Tree
The intended way of instantiating an OpenPLX Physics3D model is to load a Physics3D.System.
A system may include any number of rigid bodies, geometries, mate connectors, interactions, and child systems—each of which may contain more of the same. The size of the physics graph is not restricted, which means OpenPLX can represent both simple models and arbitrarily large and deep physical systems.
Semantics of System Composition
Adding a subsystem or a rigid body to a system is straightforward: simply list a typed attribute in the system.
Similarly, you may add Physics3D.Interactions.ContactGeometry or Physics3D.Interactions.MateConnector to a rigid body or a system.
One might argue for all types to have predefined container-type attributes for every supported type. However, this would either:
- require redundant work (re-listing objects that are already defined), or
- force all objects to be declared at the same time (to preserve order within the container), which would also prevent attributes from having unique names.
Types Inserted into the Physics Graph
The following types are automatically inserted into the physics graph when listed in an owning model.
Each type has a local transform relative to its owner.
| Type | Supported ownership of |
|---|---|
| System | System, Body, ContactGeometry, MateConnector |
| Body | ContactGeometry, MateConnector |
| ContactGeometry | – |
| MateConnector | – |
Specializations
- RigidBody is a specialization of Body
- RedirectedMateConnector is a specialization of MateConnector
- Environment is a specialization of System
Any specialization of these base types is supported and becomes part of the model, thereby extending the physics tree.
Example: Physics Tree
In the example below, a model of a system with nested bodies and systems is illustrated.
Each local transform of the objects is shown with a small frame.

The root_system is the model intended for instantiation.
It includes:
- two child systems:
child_system_aandchild_system_b, - one rigid body,
- one static contact geometry,
- one MateConnector,
- one RedirectedMateConnector, and
- three Mates.
Ownership and Local Transforms
To clarify ownership and the relationships between the various local_transforms, the physics tree is shown below:

Runtime Assembly
When the model is instantiated in a runtime environment, bodies are assembled from the Mate definitions.
The figure below illustrates how the three Mate definitions create a constrained chain between the world, body, body_a, and body_b.

body_bb, on the other hand, is not part of that chain and will receive a transform directly from its explicitly defined local_transform.
Note: Automatic assembly using SNAP only applies to systems and rigid bodies without an explicitly defined local transform.
System
A System has a local_transform attribute, which defines its transform relative to its owner (if it has one).
MateConnector
- A MateConnector owned by a system is considered world-static.
- When used in a Mate, it constrains the body of the other connector to the world.
- A Mate with two world-static MateConnectors has no semantic meaning and is ignored.
- The
local_transformof a MateConnector is defined by:- the
positionattribute, and - two orthonormal vectors:
main_axisandnormal.
All three attributes are local to its owner.
- the
- A MateConnector owned by a RigidBody defines the local frame of attachment of a Mate.
- If one RigidBody has a specified transform, the other RigidBody (with an unset
local_transform) can be SNAP’ed so the two MateConnectors align. - If the two bodies do not share the same parent, the closest common ancestor is located, and any unset
local_transformalong the path may be adjusted to align the bodies.
- If one RigidBody has a specified transform, the other RigidBody (with an unset
RedirectedMateConnector
A RedirectedMateConnector is used to define a non-world-static MateConnector relative to a System.
- The dynamics are redirected to a RigidBody that is assumed to have been assembled beforehand.
- Example:
rmcis defined relative to theroot_system, but redirected tobody.- The
body_to_worldMate will then use the position ofrmcrelative to an explicitly chosen position ofbody.
ContactGeometry
A ContactGeometry has a local_transform attribute relative to its owner.
- If owned by a System, the ContactGeometry is static.
- If owned by a RigidBody, it contributes to the inertia and mass of the body.
- An explicitly specified inertia or mass on the RigidBody will override any geometry-derived values.
Body
The Body model has a kinematics attribute, which defines its kinematic data.
- The
local_transformis contained within this attribute. - RigidBody is a specialization of Body. A result of future development could be specializations such as:
FlexibleBody is BodyBeam is BodyCloth is Body
For non-rigid types, the kinematics attribute must be specialized to describe the internal kinematics specific to that type.
Mates
A Mate is an interaction that constrains two rigid bodies.
Each Mate requires two MateConnectors—one for each body—defined as body-relative coordinate systems.
A Mate is considered at rest when the two coordinate systems overlap.
The SNAP algorithm operates on Mates, automatically assembling bodies by resolving all implicitly defined relative positions within a system. Its purpose is to align MateConnectors wherever possible, ensuring consistent and correct assembly of the model.
Specialization through Traits
As described in the nodes_readme document, OpenPLX supports specialization of types through both single inheritance and traits.
- With single inheritance, once a type inherits from a base type, no other specializations that inherit from the same base can be applied.
- With traits, however, a type can be specialized with multiple independent behaviors. For example, a RigidBody may be extended with several traits without losing its identity as a RigidBody. The main type remains RigidBody.
When to Use Traits vs. Inheritance
In general, it is recommended to use traits instead of single inheritance.
- Single inheritance should be reserved for domain specialization.
- Example: Defining a Link or a Boom as a specialization of RigidBody, since these are inherently rigid.
- Traits are best used to add attributes or behaviors that are independent of the core domain type.
- Example: Defining Link- or Boom-specific attributes as traits allows flexibility—later, a Link or Boom could instead be defined as a FlexibleBody or even a System.
Example: Domain Specialization by Single Inheritance
A robot is inherently a system, so single inheritance is appropriate:
Robotics.Robot is Physics3D.System
Traits: Avoiding Type Explosion Consider the Robotics.Joint, which may be:
- flexible or rigid, and
- position-, velocity-, or torque-controlled.
If only single inheritance were used, one abstract type would be required for each combination, quickly leading to an explosion of types.
Example: Domain Specialization with Traits
Instead, traits provide a cleaner solution. For example:
trait FlexibleJointTrait:
drive_train is FlexibleJointDriveTrain
This trait can be applied to any of the three control types, avoiding redundancy while keeping the model flexible.
Example: Shared interface with Traits
Traits also allow for shared Interfaces. Two models could define a model with very different level of detail, but still share the interface from composition. For example the DriveTrain bundle includes two different engine models—EmpiricalEngine and MeanValueEngine. Both inherit from Physics.Interactions.Interaction1DOF, and both share four traits:
EmpiricalEngine is Physics.Interactions.Interaction1DOF:
with DriveTrain.Traits.RotationalPowerGenerator
with Physics.Signals.FractionInputTrait
with Physics.Signals.RpmOutputTrait
with Physics.Signals.Torque1DOutputTrait
MeanValueEngine is Physics.Interactions.Interaction1DOF:
with DriveTrain.Traits.RotationalPowerGenerator
with Physics.Signals.FractionInputTrait
with Physics.Signals.RpmOutputTrait
with Physics.Signals.Torque1DOutputTrait
Although the implementations of the two engines differ completely, the interfaces remain the same thanks to the shared traits. This ensures that different models can share a consistent interface while retaining distinct internal behavior.
Component based modeling
With OpenPLX Physics3D, we address the challenge of building advanced mechanical systems while minimizing the need for hardcoded internal positions and rotations of bodies.
Instead of defining these manually, we rely on SNAP to compute any undefined positions and orientations.
A key difficulty lies in designing the building blocks of a mechanical system so that they closely resemble their real-world counterparts. To specify the body-relative position and orientation of a MateConnector, users provide:
- a position vector (
Vec3), and - two direction vectors (
main_axisandnormal).
The main_axis is mandatory and typically represents the hinge axis for a hinge joint or the normal axis of a planar joint. The normal vector is expected to be orthogonal to the main axis. If it is not, only the orthogonal component is used.
By default, OpenPLX automatically computes a normal vector orthogonal to the main axis. If a user specifies a normal vector that is parallel to the main axis, an error is reported.
Clock Example
To illustrate how MateConnectors work in practice, let’s model a simple clock consisting of just the clock face and a single hand.
We imagine these components as if they were designed by two different suppliers, yet we still want a common framework to assemble their digital twins into one coherent system.
In the real world, this would be similar to mounting the clock hand onto a small pin or screw.

In this model, both the clock and the hand are represented as a OpenPLX.Physics3D.RigidBody, and each includes a single MateConnector that defines how they fit together:
- On the clock, the MateConnector is positioned at the center, slightly above the clock face.
- On the hand, the MateConnector is placed at its pivot point—the location around which it should rotate.
To define rotation, we assign a main_axis:
- For the clock, the axis points into the face of the clock.
- For the hand, the axis is set normal to its back side so the front side faces the viewer.
This ensures that a positive rotation of the hand (following the right-hand rule) corresponds to moving forward in time.

To further control orientation, two local directions are defined:
- the upward direction toward twelve o’clock, and
- the direction along the length of the hand.
Together, these vectors fully define the assembly.
This method is quite different from hardcoding explicit positions and rotations. Traditional modeling requires defining a fixed initial condition, which can be difficult to modify later. By instead using local vectors, the setup becomes clearer, more flexible, and easier to reason about.
It also enables the initial angle of the hand around the clock’s normal axis to be expressed as a simple scalar value, making it straightforward to place the hand at any desired starting position.
SNAP
The SNAP algorithm automatically computes local transforms for Body and System objects, based on the Mate definitions that connect bodies to each other or to the world.
SNAP does not resolve ambiguities:
- If the system contains redundant constraints, SNAP will force the corresponding transforms to be the identity.
- If the model is under-constrained, the undefined transforms remain unresolved.
Three-Level SNAP Example
Consider the following example from snap_parent.openplx:
snap_parent.openplx
```js Rod is Physics3D.Bodies.RigidBody: inertia.mass: 10 geometry is Physics3D.Geometries.Box: size: Math.Vec3.from_xyz(0.1, 0.1, 1) arrow is Physics3D.Geometries.Box: local_transform: position.z: 0.5 rotation: Math.Quat.angleAxis(Math.PI / 4, Math.Vec3.Y_AXIS()) size: Math.Vec3.from_xyz(0.071, 0.1, 0.071) connector is Physics3D.Interactions.MateConnector: position.z: -geometry.size.z * 0.6 main_axis: Math.Vec3.Y_AXIS() normal: Math.Vec3.Z_AXIS() RodParent is Physics3D.System: rod is Rod RodParentParent is Physics3D.System: rod_system is RodParent World is Physics3D.System: world_connector is Physics3D.Interactions.MateConnector: position.x: 0.5 main_axis: Math.Vec3.Y_AXIS() normal: Math.Vec3.X_AXIS() rod_system is RodParentParent world_lock is Physics3D.Interactions.Lock: connectors: [rod_system.rod_system.rod.connector, world_connector] SnapRod is World: rod_system.local_transform.rotation: Math.Quat.fromTo(Math.Vec3.from_xyz(1,0,0), Math.Vec3.from_xyz(1,1,1).normal()) rod_system.rod_system.local_transform.rotation: Math.Quat.fromTo(Math.Vec3.from_xyz(0,1,0), Math.Vec3.from_xyz(1,1,1).normal()) SnapRodParent is World: rod_system.local_transform.rotation: Math.Quat.fromTo(Math.Vec3.from_xyz(1,0,0), Math.Vec3.from_xyz(1,1,1).normal()) rod_system.rod_system.rod.kinematics.local_transform.rotation: Math.Quat.fromTo(Math.Vec3.from_xyz(0,1,0), Math.Vec3.from_xyz(1,1,1).normal()) SnapRodParentParent is World: rod_system.rod_system.local_transform.rotation: Math.Quat.fromTo(Math.Vec3.from_xyz(1,0,0), Math.Vec3.from_xyz(1,1,1).normal()) rod_system.rod_system.rod.kinematics.local_transform.rotation: Math.Quat.fromTo(Math.Vec3.from_xyz(0,1,0), Math.Vec3.from_xyz(1,1,1).normal()) ```The example defines a single Rod model, which is a RigidBody, contained within RodParent, a System.
To make the setup more complex, RodParent itself is nested inside another parent system named RodParentParent.
This file contains three different scenarios for SNAP to handle.
There is one body with a single MateConnector that must be positioned at the world_connector. However, three different transforms are involved, and the user must either explicitly define the redundant ones or allow SNAP to force two of them to the identity transform.
In this example, we highlight how SNAP computes transforms in three different cases:
- SNAP computes the
RodParentParent.local_transformwhen bothRodParentandRodlocal transforms are specified (not default). - SNAP computes the
RodParent.local_transformwhen bothRodParentParentandRodlocal transforms are specified (not default). - SNAP computes the
Rod.local_transformwhen bothRodParentParentandRodParentlocal transforms are specified (not default).
To position the Rod body at the world_connector, the following affine matrix equation must hold:
$A = S_1 S_2 R M$
Where:
- $A$ = the
world_connectortransform - $S_1$ = local transform of
RodParentParentrelative to its parent (world frame) - $S_2$ = local transform of
RodParentrelative to $S_1$ - $R$ = local transform of
Rodrelative to $S_2$ - $M$ = local transform of the
Rod.connectorrelative to theRod
When leaving the computation to SNAP, it is undefined which of the three transforms ($S_1$, $S_2$, $R$) will be solved and which will be set to the identity.
The formulas for each computation are as follows:
$S_1 = A M^{-1} R^{-1} S_2^{-1}$
$S_2 = S_1^{-1} A M^{-1} R^{-1}$
$R = S_2^{-1} S_1^{-1} A M^{-1}$
Generic Transform Computation
Consider the computation of $S_1$ in the SnapRodParent example above.
This case illustrates how the generic SNAP computation works in practice.
The tree of transforms is shown in the diagram below. Each node represents a SnapFrame containing a local transform. The Ancestor is the SnapFrame corresponding to the closest common ancestor (a System) of $M_f$ and $M_t$.
- M = MateConnector
- R = RigidBody
- S = System

In this example, world_lock is a Mate that connects two MateConnectors: rod.connector and world_connector.
Here:
- $M_t$ =
world_connector(the target) - $M_f$ =
rod.connector(the frame being snapped)
The algorithm identifies $S_2$ as the target SnapFrame for the world_lock Mate.
The notation <ancestor>_<child>_reduced represents a combined transform from an upper coordinate system to a child coordinate system.
The generic formula for computing the target local transform $S$ is derived from:
\[NSP = A\]This equation means that the product of transforms $N S P$ must equal $A$ for the SnapFrames to align.
Solving for $S$, the unknown, gives:
This is referred to as the SNAP transform.
Algorithm
Conceptually, SNAP is simple: it assembles a mechanical system the way a person might assemble a set of Lego pieces—starting from one part and incrementally attaching others until the entire model is complete.
However, in practice, systems defined with mates, bodies, and subsystems in OpenPLX Physics3D may present challenges:
- Two mates may attempt to snap in ambiguous or conflicting ways.
- Some systems may include redundant transforms.
- Certain bodies may not be connected to any mate.
In these cases, SNAP attempts to resolve as much as possible:
- It computes local transforms for systems and bodies whenever they can be defined unambiguously.
- If redundancy is detected, SNAP forces one of the redundant transforms to the identity.
- Ambiguities that cannot be resolved are left undefined.
The guiding principle is that any model which can be assembled in the real world should also be successfully assembled by SNAP—just like building with Lego.
To trace SNAP’s execution step-by-step, you can add the flag --loglevel trace to your command line.
Pseudocode
The pseudocode below summarizes the SNAP algorithm:
S ← Set of all mates
D ← Degrees of freedom left from mates being snapped
A, B ← mate connectors
C ← A coordinate system with an undefined local transform relative to its parent
Snap(A, B, C) ← Function that computes a local transform for C which moves A to B, using the SNAP transform.
while there are unused mates in S:
N = current_num_used_mates(S)
for each mate M with mate connectors A and B:
G ← closest common ancestor of A and B
[AG[ ← sequence of local transforms from A to G
[BG[ ← sequence of local transforms from B to G
if num_undefined_transforms([AG[) == 1 && num_undefined_transforms([BG[) == 0:
C ← get_first_undefined_transform([AG[)
T ← Snap(A, B, C)
C.set_local_transform(T)
D.insert(generate_degrees_of_freedom(M, C))
else if num_undefined_transforms([AG[) == 0 && num_undefined_transforms([BG[) == 1:
C ← get_first_undefined_transform([BG[)
T ← Snap(A, B, C)
C.set_local_transform(T)
D.insert(generate_degrees_of_freedom(M, C))
else if num_undefined_transforms([AG[) == 0 && num_undefined_transforms([BG[) == 0:
T ← find_solution_using_degrees_of_freedom(M, D)
# No frame was positioned in the previous loop
# SNAP will force a transform to be the identity
num_dof ← current_num_unused_degrees_of_freedom(D)
if N == current_num_used_mates(S):
if num_dof > 0:
kill_first_degree_of_freedom(D)
continue # Retry all mates; ambiguity may have been resolved
M ← first_unused_mate_with_undefined_transform_chain(S)
A, B ← get_mate_connectors(M)
G ← closest common ancestor of A and B
[AG[ ← sequence of local transforms from A to G
[BG[ ← sequence of local transforms from B to G
if num_undefined_transforms([AG[) > 0:
C ← get_first_undefined_transform([AG[)
C.set_local_transform(I)
continue
else if num_undefined_transforms([BG[) > 0:
C ← get_first_undefined_transform([BG[)
C.set_local_transform(I)
continue
else:
# ERROR
Mate M did not have an undefined transform chain
break the while loop