DiscoverLearnDocumentationGet OpenPLXSearch Contact

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.

physics example

The root_system is the model intended for instantiation.
It includes:

  • two child systems: child_system_a and child_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:

physics tree

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.

runtime view

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_transform of a MateConnector is defined by:
    • the position attribute, and
    • two orthonormal vectors: main_axis and normal.
      All three attributes are local to its owner.
  • 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_transform along the path may be adjusted to align the bodies.

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:
    • rmc is defined relative to the root_system, but redirected to body.
    • The body_to_world Mate will then use the position of rmc relative to an explicitly chosen position of body.

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_transform is contained within this attribute.
  • RigidBody is a specialization of Body. A result of future development could be specializations such as:
    • FlexibleBody is Body
    • Beam is Body
    • Cloth 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_axis and normal).

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.

Clock modelling example

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.

Clock modelling example with Mate Connectors

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_transform when both RodParent and Rod local transforms are specified (not default).
  • SNAP computes the RodParent.local_transform when both RodParentParent and Rod local transforms are specified (not default).
  • SNAP computes the Rod.local_transform when both RodParentParent and RodParent local 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_connector transform
  • $S_1$ = local transform of RodParentParent relative to its parent (world frame)
  • $S_2$ = local transform of RodParent relative to $S_1$
  • $R$ = local transform of Rod relative to $S_2$
  • $M$ = local transform of the Rod.connector relative to the Rod

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

Snap hierarchical transforms

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:

\[S = N^{-1} A P^{-1}\]

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:

  1. Two mates may attempt to snap in ambiguous or conflicting ways.
  2. Some systems may include redundant transforms.
  3. 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
OpenPLX is a work in progress. This draft version will evolve with user feedback and experience. We welcome your input and collaboration.
X