33. agx::MergedBody – many bodies simulated as one

A merged body is can consist of several agx::RigidBody objects merged together, handled as one single agx::RigidBody in the solver. This functionality can be used to dramatically reduce the system size, as seen from the solver, but preserving the mass properties/inertia of a merged system. It can be seen as an extension of AutoSleep functionality, but with the huge benefit, that it support merging/sleeping on moving/dynamics bodies. Whereas AutoSleep has the big limitation that it only support sleeping on non moving bodies (zero velocity in world frame).

The functionality of agx::MergedBody can be used in two ways:

  • Explicitly/manual: By explicitly creating EdgeInteractions (Section 33.3) between bodies which are added to an agx::MergedBody the user has full control over how bodies are merged and split.

  • Automatic: Section 34 describes functionality for automatic merge/split based on relative velocities, contacts and force measurements.

// Create a new merged body.
agx::MergedBodyRef mergedBody = new agx::MergedBody();
// Active the merged body by adding it to the simulation.
simulation->add( mergedBody );
// Deactivating the merged body by removing it from the simulation.
simulation->remove( mergedBody );

33.1. Applications

Let’s have a look at same sample cases. A good start is to have a distinct parent body. This parent body can for example be a ship, the ground, a harbor, a flatbed, etc.

33.1.1. Hydraulic cranes on an offshore vessel

A set of hydraulic cranes on an offshore vessel. Each crane may consist of around 15 bodies and 15-20 constraints. The weight of the crane is significant, affecting the motion of the ship, so when the crane isn’t in use, it cannot just be removed (e.g., only rendered instead).

15 bodies and 15-20 constraints are in general fast to solve but imagine three cranes per vessel and 10 vessels. It’s now 450 extra rigid bodies and about 500 constraints that aren’t explicitly in use.

Now, merging the bodies in the cranes to the ship, the constraints will become inactive (since no relative motion), and the solver will only have to solve for ONE rigid body instead of 45.

33.1.2. Logs or rocks on a flatbed

A few hundred logs or rocks on a moving flatbed can in general take a significant amount of time in the solver. For this to be a reasonable scenario, the vehicle pulling the flatbed has to feel the weight and inertia of the load, so the load cannot simply be removed to reduce the time in the solver.

If the stacked logs or rocks can be considered steady, i.e., not moving relative to each other, it’s possible to merge the objects to the flatbed. Instead of a few hundred objects + thousands of contacts to solve, the solver only has one single body, attached to the vehicle to solve for.

33.2. The dynamics of merged bodies

The motion control of the final merged body is determined by the motion controls of the set of rigid bodies merged, with the following priority: 1. (if one is) KINEMATICS results in KINEMATICS 2. (if at least one is) STATIC and none KINEMATICS, the result is STATIC 3. (if all are) DYNAMICS and none STATIC or KINEMATICS, the result is DYNAMICS

Note

Only one rigid body with KINEMATICS motion control may occur in a merged body at the time! Having more than one is considered undefined since it’s not possible to determine the final velocity of the bodies involved.

If the final motion control is DYNAMICS, the total mass becomes

\[m^{tot} = \sum_{i} m_i\]

the center of mass

\[p^{cm} = \frac{1}{m^{tot}} \sum_{i} m_i p_i^{cm}\]

and finally the inertia

\[I^{tot} = \sum_{i} I_i - m_i \left[p^{cm} - p_i^{cm}\right]^2\]

where

\[\begin{split}[r] = \begin{bmatrix} 0 & -r_z & r_y \\ r_z & 0 & -r_x \\ -r_y & r_x & 0 \end{bmatrix}\end{split}\]

If the motion control is different from DYNAMICS, the calculations of center of mass, mass and inertia are ignored.

In the case of KINEMATICS, the linear- and angular velocity will be the one of the kinematic body. In the DYNAMICS case, the final linear- and angular velocity becomes:

\[v^{tot} = \frac{1}{m^{tot}} \sum_{i} m_iv_i\]
\[\omega^{tot} = \frac{1}{m^{tot}} \sum_{i} m_i \omega_i\]

33.2.1. Advanced mass properties

agx::MergedBody supports rigid bodies with advanced mass properties features. Examples of advanced features are damping (Section 55.2), added mass (Section 55.1).

33.3. Edge interactions – merging objects together

The internal structure of an agx::MergedBody is a graph where the nodes in the graph is an agx::RigidBody and the edges, connecting the nodes, are so called edge interactions.

An edge interaction defines how the merged rigid bodies interacts and what happens when an edge is removed.

33.3.1. Empty edge interaction

An empty edge interaction is a pure logical connection between two rigid bodies.

// Merge rb1 <-> rb2 given an empty edge interaction.
mergedBody->add( new agx::MergedBody::EmptyEdgeInteraction( rb1, rb2 ) );

33.3.2. Contact generator edge interaction

A contact generator edge interaction is an edge that, depending on when the edge is removed, can perform collision detection between the geometries in the first and the second rigid body.

If the contact generator edge is removed before the collision detection is executed, the edge does nothing. I.e., it acts as an empty edge interaction, assuming the objects will participate in the collision detection given the usual update.

If the contact generator edge is removed after the collision detection pass has been run and before the solver, the edge will check for overlaps between the two bodies and add agxCollide::GeometryContact’s to the system. This prevents the two objects to “fall into” each other when the solver solves the system.

// Merge rb1 <-> rb2 and generate geometry contacts
// between them if we split in a PRE_STEP callback.
mergedBody->add( new agx::MergedBody::ContactGeneratorEdgeInteraction( rb1, rb2 ) );

33.3.3. Geometry contact edge interaction

An edge interaction, constructed given an agxCollide::GeometryContact, taking advantage of the contact information, normal and friction forces.

To gain full advantage of this edge, the solver should have seen the geometry contact, i.e., normal and friction forces are available in the contact.

// Post-step: If the objects in the geometry contact
// are at rest, merge them.
for ( auto geometryContact : simulation->getSpace()->getGeometryContacts() ) {
  if ( isRestingContact( geometryContact ) )
    mergedBody->add( new agx::MergedBody::GeometryContactEdgeInteraction( *geometryContact ) );
}

Similar to agx::MergedBody::ContactGeneratorEdgeInteraction, the contact points and normal are added back, as a new agxCollide::GeometryContact, if the edge is removed after collision detection but before the solver executes.

33.3.4. Binary constraint edge interaction

A binary constraint edge interaction is an edge constructed given a one or two (hence binary) body constraint.

This edge does nothing when removed from an agx::MergedBody.

// If the hinge motor isn't enabled - merge.
if ( !hinge->getMotor1D()->getEnable() )
  mergedBody->add( new agx::MergedBody::BinaryConstraintEdgeInteraction( hinge ) );

33.4. Data and state of the merged rigid bodies

If an agx::RigidBody is merged, it’s possible to access the agx::MergedBody it belongs to by doing:

// Check whether rb belongs to a merged body.
agx::MergedBody* mergedBody = agx::MergedBody::get( rb );
if ( mergedBody != nullptr )
  std::cout << rb->getName() << " is merged." << std::endl;

Having access to the agx::MergedBody object, it’s possible to traverse/inspect/collect the nodes and edges:

// Collect edges connected to 'rb'.
agx::MergedBody::EdgeInteractionRefContainer rbEdges;
agx::MergedBody::EdgeInteractionVisitor visitor = [ &rbEdges ]( agx::MergedBody::EdgeInteraction* edge )
{
  rbEdges.push_back( edge );
};
// 'Visit all edges associated to rb'.
mergedBody->traverse( rb, visitor );

// Print the name of the bodies 'rb' is merged to.
for ( auto edge : rbEdges ) {
  const agx::RigidBody* otherRb = edge->getRigidBody1() == rb ? edge->getRigidBody2() :
                                                                edge->getRigidBody1();
  std::cout << "rb is merged to: " << otherRb->getName() << std::endl;
}

A similar example, visiting all neighboring nodes instead of collecting all edges (there may be an arbitrarily number of edges between two bodies):

agx::RigidBodyPtrVector otherBodies;
agx::MergedBody::RigidBodyVisitor visitor = [ &otherBodies ]( agx::RigidBody* otherRb )
{
  otherBodies.push_back( otherRb );
};
// 'Visit all neigboring nodes to rb'.
mergedBody->traverse( rb, visitor );

// Print the name of the bodies 'rb' is merged to.
for ( auto otherRb : otherBodies )
  std::cout << "rb is merged to: " << otherRb->getName() << std::endl;

Or one can simply do:

const auto* otherBodies = mergedBody->getNeighbors( rb );
if ( otherBodies == nullptr )
  std::cout << "rb is not merged in mergedBody" << std::endl;
else
  for ( auto otherRb : *otherBodies )
    std::cout << "rb is merged to: " << otherRb->getName() << std::endl;

33.4.1. Listeners

It’s possible to add listeners to an agx::MergedBody. These listeners are passive, i.e., it’s not valid to change the state and not possible to for example prevent a merge, from within a listener.

Callbacks: - clone() Create a clone of your listener. It’s valid to return the same instance. The clone method is called when the merged body, the listener belongs to, is being split into several agx::MergedBody instances (see: agx::MergedBody::splitIslands). - onAdd( agx::RigidBody* rb, const agx::MergedBody* mb ) Called when a rigid body is added to the merged body. - onRemove( agx::RigidBody* rb, const agx::MergedBody* mb ) Called when a rigid body is being removed from the merged body. - onAdded( EdgeInteraction* edge, const agx::MergedBody* mb ) Called when an edge has been added to the merged body. - onRemoved( const EdgeInteractionRefContainer& edges, const agx::MergedBody* mb ) Called when a set of edges has been removed from the merged body. - onMovedFromTo( const EdgeInteractionRefContainer& edges, const agx::MergedBody* fromMergedBody, const agx::MergedBody* toMergedBody ) Called when a set of edges has been moved from one merged body to another.

33.5. Splitting merged rigid bodies

If a rigid body is merged, there are several ways to split it. The most convenient way is to do:

// Split method finds the merged body associated to 'rb'
// and removes 'rb' from it.
const agx::Bool success = agx::MergedBody::split( rb );
if ( success )
  std::cout << rb->getName() << " successfully removed from its merged body." << std::endl;
else
  std::cout << rb->getName() << " failed to split. Was it merged in the first place?" << std::endl;

For more control, use the remove method:

// Find if 'rb' is merged.
agx::MergedBody* mergedBody = agx::MergedBody::get( rb );
// If merged, remove it (split).
if ( mergedBody != nullptr )
  mergedBody->remove( rb );

The remove method guarantees to remove any references to the agx::MergedBody as long as rb is present in mergedBody.

Note

that when you’re managing the agx::MergedBody objects, an instance may become empty when removing a rigid body. E.g., two bodies are merged, remove one of them will result in a single body without any edges being the only one left – so it will be removed as well. Leaving the agx::MergedBody object empty. An update to the above example:

// Find if 'rb' is merged.
agx::MergedBodyRef mergedBody = agx::MergedBody::get( rb );
// If merged, remove it (split).
if ( mergedBody != nullptr ) {
  mergedBody->remove( rb );
  // If the remove of 'rb' results in an empty 'mergedBody',
  // remove 'mergedBody' from the simulation.
  if ( mergedBody->isEmpty() )
    simulation->remove( mergedBody );
  mergedBody = nullptr;
}

It’s also possible to split a rigid body by removing all the edges to its neighboring nodes/bodies:

// Collect edges connected to 'rb'.
agx::MergedBody::EdgeInteractionRefContainer rbEdges;
agx::MergedBody::EdgeInteractionVisitor visitor = [ &rbEdges ]( agx::MergedBody::EdgeInteraction* edge )
{
  rbEdges.push_back( edge );
};
// 'Visit all edges associated to rb'.
mergedBody->traverse( rb, visitor );

// Remove all the edges associated to our 'rb'.
for ( auto edge : rbEdges )
  mergedBody->remove( edge );

// When all edges has been removed, 'rb' hasn't got
// any connections left in 'mergedBody' so it will
// be removed.
agxAssert( agx::MergedBody::get( rb ) == nullptr );

33.6. Separate islands within an agx::MergedBody

The agx::MergedBody doesn’t assume that all rigid bodies/nodes are connected. E.g, if you have four bodies rb1, rb2, rb3 and rb4, and add edge interactions rb1 <-> rb2 and rb3 <-> rb4 to the agx::MergedBody – it’s a valid, but maybe not a desired state.

A more natural example. Say we have five rigid bodies, merged like this:

../_images/agx__mergedbody_1.png

Removing rb3 will result in the following merged state:

../_images/agx__mergedbody_2.png

where there aren’t any interactions connecting island 1 to island 2. It’s not possible to query the merged body how many islands it consists of since it needs a full graph traversal to determine this. Still, if the desired behavior is there shouldn’t be separate islands in my merged bodies’ it’s possible to call agx::MergedBody::splitIslands. This method will determine separate islands, create new merged bodies and add them to the simulation.

// Checks for separate, non-interacting, islands in mergedBody.
// NOTE: mergedBody has to be in the simulation and all new
//       agx::MergedBody objects will be added to the same
//       simulation.
agx::MergedBodyRefVector newIslands = mergedBody->splitIslands();
std::cout << "Number of new islands: " << newIslands.size() << std::endl;

Resulting in:

../_images/agx__mergedbody_3.png

33.7. Merging two already merged objects

For obvious reasons (it’s rigid, it cannot move in different directions at the same time!), a rigid body may only belong to one agx::MergedBody. As a result of this constraint, an edge will be rejected by a merged body if one (or both) of the bodies included is merged to another merged body. This state has to be explicitly handled by the user.

Given two rigid bodies rb1 and rb2, merged with other objects in mergedBody1 and mergedBody2, it’s possible to merge the two agx::MergedBody objects together given an edge between rb1 and rb2:

agx::MergedBody* mergedBody1 = agx::MergedBody::get( rb1 );
  agx::MergedBody* mergedBody2 = agx::MergedBody::get( rb2 );
  // Copying all data from mergedBody2 into mergedBody1 and
  // adding the empty edge interaction to mergedBody1.
  agx::MergedBody::EmptyEdgeInteractionRef edge = new agx::MergedBody::EmptyEdgeInteraction( rb1, rb2 );
  const agx::Bool success = mergedBody1->merge( mergedBody2, edge );
  // If merge is successful, remove the empty mergedBody2
  // from the simulation.
  if ( success )
    simulation->remove( mergedBody2 );

33.8. Active and inactive

Consider the example scenario described in Hydraulic cranes on an offshore vessel. The crane is not in use so we have it merged. The state of the agx::MergedBody containing the objects, is active:

// Adding 'craneMergedBody' will active it and the containing
// rigid bodies will be merged.
simulation->add( craneMergedBody );

When the crane is ready for use, e.g., moving the boom, we want to inactivate the agx::MergedBody. This can of course be done in many different ways, but one convenient thing of the agx::MergedBody objects is that it will preserve its internal structure when removed from the simulation. The objects merged will simply not be merged anymore.

Let a prismatic be the constraint we control the boom hydraulic piston/cylinder with. When the prismatic motor is active we expect the objects to be able to move relative each other, and when the motor is inactive – we may merge the crane objects again. This can be achieved by adding and removing the craneMergedBody:

if ( prismatic->getMotor1D()->getSpeed() != 0.0 ) {
    // Motor is active, split all objects in 'craneMergedBody'
    // by removing it from the simulation.
    if ( craneMergedBody->isInSimulation() )
      simulation->remove( craneMergedBody );
  }
  else {
    // Motor isn't active, merge all objects in 'craneMergedBody'
    // by adding it to the simulation.
    if ( !craneMergedBody->isInSimulation() )
      simulation->add( craneMergedBody );
  }

This means that a rigid body can be associated to an agx::MergedBody but it’s still not actively merged. It’s possible to check whether an objects is actively merged (i.e., not seen by the solver) or passively merged (i.e., part of a merged body, but that merged body is not in a simulation):

// agx::MergedBody::get returns the merged body 'craneBoom' is part of.
const agx::Bool isPartOfAMergedBody = agx::MergedBody::get( craneBoom ) != nullptr;
// agx::MergedBody::getActive returns the merged body 'craneBoom' is part
// of IF that merged body is a part of the simulation.
const agx::Bool isActivelyMerged = agx::MergedBody::getActive( craneBoom ) != nullptr;

During normal usage there’s no need to use the getActive method.