User Manual - xissburg/edyn Wiki

Original URL: https://github.com/xissburg/edyn/wiki/User-Manual

This document shows how to use Edyn in practice.

Table of Contents

Introduction

Edyn is a real-time physics simulation engine focused on constrained rigid body simulation. On top of that, it seeks to have the capacity to simulate very large worlds leveraging multithreading and distributed simulation.

It operates in conjunction with an entt::registry which makes it ideal for any ECS-oriented project that uses EnTT. One of the great advantages is that no weird conversions must be done to and from the registry and the usually object-oriented physics engine. All the relevant physics information is associated with the entity, i.e. the edyn::present_position and edyn::present_orientation can be obtained from the registry and used to render the entity or to do any other computations.

Edyn builds a graph where rigid bodies are nodes and constraints are edges and finds the connected components in this graph to split up the rigid bodies and constraints in subsets which can be simulated in isolation because they do not interact with entities in other connected components. These connected components are also known as islands, and each island is simulated in a worker thread, inside an island worker. As constraints are created and destroyed (especially contact constraints), it checks if any connected components have merged or split and proceeds to destroy or create new islands. As such, no physics simulation is done in the main thread, as it seeks to offload as much work to the worker threads as possible. In the main thread, the island coordinator is responsible for managing all islands. Each island worker has its own private entt::registry and after every step of the simulation, it crafts an island delta which contains relevant information about entities and components that were created, updated and destroyed and sends it to the coordinator via a thread-safe message queue. In the main thread, the coordinator merges the island deltas into the main registry.

The physics simulation step happens in each island worker independently. This is the order of major updates:

  1. Process messages and island deltas, thus synchronizing state with the main registry, which might create new entities in the local registry.
  2. Detect collision for new contact manifolds that have been imported from the main registry.
  3. Create contact constraints for new contact points. This includes new points generated in the collision detection of imported contact manifolds and contact points that were created in the previous physics step.
  4. Apply external forces such as gravity.
  5. Solve constraints.
  6. Apply constraint impulses.
  7. Integrate velocities to obtain new positions and orientations.
  8. Now that rigid bodies have moved, run the broad-phase collision detection to create/destroy collision pairs (i.e. new contact manifolds).
  9. Perform narrow-phase collision detection to generate new contact points. Contact constraints are not created yet.
  10. Send island delta to coordinator.

The main features include:

Creating a scene and running a simulation

Before setting up the physics scene, edyn::init() must be called to initialize some internals such as the worker threads. Then, edyn::attach(registry) must be called to make an entt::registry ready to be used for physics simulation. Finally, rigid bodies and constraints can be created in the registry. In the main loop of the application, edyn::update(registry) must be called to keep the registry updated with the latest physics state. Due to its multithreaded nature, the update function does not actually do any physics simulation, it basically does island management, and the islands actually run the simulation in separate threads.

// Initialization.
entt::registry registry;
edyn::init();
edyn::attach(registry);

// Create floor
auto floor_def = edyn::rigidbody_def();
floor_def.kind = edyn::rigidbody_kind::rb_static;
floor_def.material->restitution = 1;
floor_def.material->friction = 0.5;
// Plane at the origin with normal pointing up.
floor_def.shape = edyn::plane_shape{{0, 1, 0}, 0};
edyn::make_rigidbody(registry, floor_def);

// Create dynamic rigid body.
auto def = edyn::rigidbody_def();
def.material->friction = 0.8;
def.mass = 10;
def.restitution = 0;
// Box with 0.2 half-length in all dimensions.
def.shape = edyn::box_shape{0.2, 0.2, 0.2};
// Set moment of inertia from mass and shape assigned previously.
def.update_inertia();
edyn::make_rigidbody(registry, def);

// Main loop.
while (true) {
    edyn::update(registry);
    // Do something with the physics components such as rendering.
}

// Finish.
edyn::detach(registry);
edyn::deinit();

All rigid bodies have the edyn::position and edyn::orientation components, however these should not be used for rendering or else the result will have noticeable stutter. That is because the simulation always runs in steps of fixed delta time and the data coming from different island workers might not be exactly synchronized to the same point in time. To ameliorate these issues, interpolation/extrapolation is done to align the state of all rigid bodies to the same point in time, and that state is available in the edyn::present_position and edyn::present_orientation components.

Rigid bodies can be of three types:

Some shapes can only be used with static rigid bodies, i.e. edyn::plane_shape, edyn::mesh_shape and edyn::paged_mesh_shape. There's no moment of inertia calculation for these shapes and there's no collision detection algorithm implemented between them. They also ignore any transforms applied to the rigid body (i.e. edyn::position and edyn::orientation) for performance and complexity reasons.

Shapes are optional. Rigid bodies without a shape obviously do not take part in collision detection. They can be constrained with other rigid bodies however and can be used as internal components of a machine such as drivetrain components of a vehicle.

The material is optional. It is non-empty by default. If reset (i.e. def.material.reset()), the rigid body will not respond to collisions, i.e. collisions will be detected and contact points will be generated but no edyn::contact_constraint will be created. These kinds of bodies are known as sensors because they can be used with collision events to detect when another body enters a certain volume in the world.

Creating constraints

Constraints can be created between a pair of rigid bodies to tie their motion in some way. Use the edyn::make_constraint function to create constraints.

auto def = edyn::rigidbody_def();
// Configure rigid body...
auto bodyA = edyn::make_rigidbody(registry, def);
auto bodyB = edyn::make_rigidbody(registry, def);
// Create a point pivot. It returns an (entity, component) pair.
auto [point_entity, point_constraint] = edyn::make_constraint<edyn::point_constraint>(registry, bodyA, bodyB);
// Set pivots.
point_constraint.pivot[0] = {0, 0.5, 0};
point_constraint.pivot[1] = {0, -0.5, 0};
// Not necessary to mark it as dirty because it's a new entity and it will be
// sent to the island worker in the next update.

Do not create edyn::contact_constraint manually.

Modifying components

Due to its multithreaded nature, changes to components in the main registry will not be automatically propagated to the island workers. Whenever a relevant component changes, it's important to notify Edyn about it. One way to do it is using the edyn::dirty component to mark one of more components as dirty for one entity.

// Set position of a kinematic entity.
registry.get<edyn::position>(kinematic_entity) = new_pos;
// Mark position as dirty.
registry.get_or_emplace<edyn::dirty>(kinematic_entity).updated<edyn::position>();

In every edyn::update(registry), all edyn::dirty components are consumed and the requested changes are propagated to the correct island workers.

An alternative is edyn::refresh:

// Set position of a kinematic entity.
registry.get<edyn::position>(kinematic_entity) = new_pos;
// Request position to be updated in all islands where this entity resides.
edyn::refresh<edyn::position>(registry, kinematic_entity);

External elements

You may want to run logic alongside the physics simulation which happens in background threads. Edyn offers facilities which allow custom code to be run in every simulation step.

External systems

In most cases, running the logic that pertains to dynamic entities in the main thread and pushing changes to their islands is a rather wasteful choice. It also does not operate on the latest data nor it has the chance to run in every step of the simulation since the main thread can be updated at a lower rate than the island worker (e.g. 60 Hz in the main thread vs 120 Hz in the island worker). For that reason, Edyn allows external systems to run in the island workers, before and after every step of the simulation. These external systems have access to the worker's private registry and can modify the components directly.

void update_external_systems(entt::registry &registry) {
    // This is the required signature for an external system function.
    // This function will be called concurrently, each time with a different
    // registry, since it is called in all island workers.
}
// Assign function to run before every simulation step.
edyn::set_external_system_pre_step(registry, &update_external_systems);

External components

It may be desirable to have external components be present in the worker's registry as well. Components that are not Edyn's own will not be shared with island workers automatically, since Edyn has no knowledge of them and will only share relevant internal components by default. These components can be declared to be shared explicitly.

struct ext_component {};
// Make Edyn aware of external component.
edyn::register_external_components<ext_component>(registry);
// Now the external components can be obtained from the registry
// in an external system function.

The entities in the island worker's registry do not match up with the entities in the main registry, since in EnTT there's no possibility of creating an entity with a specific value. Thus, entity mapping is done, i.e. for every new entity in the main registry, a corresponding new entity is created in the island worker's private registry and a bidirectional map is established. For that reason, components that contain entt::entity values need to be transformed into island worker's space when imported, and vice-versa. If your external component has an entt::entity in it, it is required to provide a template specialization of the edyn::merge function in the edyn namespace.

struct ext_component {
    entt::entity entity;
};

namespace edyn {
    // This function takes the old and the new component and a context which has the entity map.
    // `old_comp` will be nullptr if this component was just created, but that's not relevant
    // in this case.
    template<> inline
    void merge(const ext_component *old_comp, ext_component &new_comp, merge_context &ctx) {
        // Map entity from remote to local.
        new_comp.entity = ctx.map->remloc(new_comp.entity);
    }
}

// Register external component later.
edyn::register_external_components<ext_component>(registry);

Now, when components of this type are imported into the island worker's private registry, it will point to the correct entity.

External entities

It might be necessary to have an entity which is not a rigid body to be present in the island where a rigid body resides so it can be accessed in an external system. That can be achieved by tagging it as an external entity using edyn::tag_external_entity and connecting it to a rigid body using a edyn::null_constraint. Tagging it as external, causes a node to be inserted into the entity graph for that entity and the null constraint creates an edge in the entity graph connecting the external entity to the rigid body. Now, whenever the rigid body is inserted into a new island, the external entity and its components will follow.

Collision filtering

Collisions can be filtered using collision groups and masks, which are a pair of 64-bit unsigned ints used a bit flags. A high bit in the group signals presence in that group. A high bit in the mask indicates participation in collisions with the group in that bit's position. For example, if one rigid body has a collision group of 1 and another has a collision mask of ~1 (bitwise not) then they will not collide.

The collision group and masks can be set in the rigid body definition:

auto def1 = edyn::rigidbody_def();
//...
def1.collision_group = 1ULL; // This rigid body is in the first group...
def1.collision_mask = ~2ULL; // ...and it does not collide with rigid bodies in the second group.
//...
edyn::make_rigidbody(registry, def1);
// Make a rigid body which does not collide with the one created above.
auto def2 = edyn::rigidbody_def();
//...
def2.collision_group = 2ULL; // This rigid body is in the second group...
def2.collision_mask = ~1ULL; // ...and it does not collide with rigid bodies in the first group.
//...
edyn::make_rigidbody(registry, def2);

The edyn::collision_filter component holds that information, which can be updated at any time.

Collision exclusion lists can be used for fine grained collision filtering. The list of entities a rigid body should not collide with is stored in a edyn::collision_exclusion component. Call edyn::exclude_collision to add a collision exclusion pair.

The collision filtering logic can be overridden by replacing the should_collide function, which can be done calling edyn::set_should_collide. It takes a const registry and two entities as parameters and returns a boolean. If the default behavior is desired as part of the new logic, edyn::should_collide_default can be called directly.

Observing collision events

Collision events can be observed using EnTT's signals, such as:

void contact_started(entt::registry &registry, entt::entity entity) {
    auto &cp = registry.get<edyn::contact_point>(entity);
    // Do something with contact point.
}

void contact_ended(entt::registry &registry, entt::entity entity) {
    // Contact was destroyed.
}

registry.on_construct<edyn::contact_point>().connect<&contact_started>();
registry.on_destroy<edyn::contact_point>().connect<&contact_ended>();

Contact points are not updated in the main registry continuously since that would be wasteful by default. In some cases it's desirable to have information such as the contact position, normal vector and applied normal and friction impulse in every frame, which can be used to play sounds or to drive a particle system, for example. If updated contact information is necessary, set the continuous_contacts property to true in the edyn::rigidbody_def.

The collision impulse is available in a separate component edyn::constraint_impulse. It contains an array with the applied impulse for each constraint row. What is a row is beyond the scope of this document but for a contact constraint, the first element is the normal impulse and the second and third holds the friction impulse in two orthogonal directions tangent to the surface. As observed earlier, contact points are generated at the end of the step, but contact constraints are not created just yet. That means, when a edyn::contact_point construction event is triggered, the edyn::constraint_impulse will not be there yet. For that reason, if the collision impulse is needed, listen to construction of edyn::contact_constraint instead.

Two constraints are used to produce friction, each applying impulses in two orthogonal directions tangent to the surface, thus both being orthogonal to the contact normal as well. These directions are not cached but since they're calculated using edyn::plane_space, they can be obtained using the same function and passing the contact normal as argument.

void contact_started(entt::registry &registry, entt::entity entity) {
    // `constraint_impulse` is guaranteed to be available after construction of a `contact_constraint`.
    auto &imp = registry.get<edyn::constraint_impulse>(entity);
    auto normal_impulse = imp.values[0];
    auto friction_impulse0 = imp.values[1];
    auto friction_impulse1 = imp.values[2];
    // Obtain friction directions.
    auto &cp = registry.get<edyn::contact_point>(entity);
    edyn::vector3 tangent[2];
    edyn::plane_space(cp.normal, tangent[0], tangent[1]);
    // Since the friction impulses are the components of two orthogonal directions,
    // the total friction impulse magnitude is the length of a 2D vector with the
    // impulses as coordinates.
    auto total_friction_impulse = edyn::length(edyn::vector2{friction_impulse0, friction_impulse1});
    // Do something with the impulses...
}

registry.on_construct<edyn::contact_constraint>().connect<&contact_started>();

Materials

The rigid body's material hold surface properties such as friction and restitution. Individual base properties are assigned to the material of each rigid body and when they collide, these properties must be combined into one for the collision response. Default functions exist for the combination of these values in material_mixing.hpp but it might be desirable to provide specific values for certain pairs of materials. A numerical id can be assigned to materials and then their combined properties can be specified via edyn::insert_material_mixing, which takes as arguments a pair of material ids and the desired material properties for that combination. When two bodies collide, the combined material properties will be used, otherwise the base property values will be mixed according to the default mixing functions.

// Specifying material properties for a pair of materials.
auto ball_mat_id = 0;
auto table_mat_id = 1;
// Material for ball-ball collision.
auto ball_ball_mat = edyn::material_base{};
ball_ball_mat.friction = 0.05;
ball_ball_mat.restitution = 0.95;
edyn::insert_material_mixing(registry, ball_mat_id, ball_mat_id, ball_ball_mat);
// Material for ball-table collision.
auto table_ball_mat = edyn::material_base{};
table_ball_mat.friction = 0.2;
table_ball_mat.restitution = 0.5;
edyn::insert_material_mixing(registry, ball_mat_id, table_mat_id, table_ball_mat);

Friction

As mentioned earlier, sliding friction is solved using two orthogonal directions which are tangent to the contact surface and its impulse can be found in the second and third entries in the edyn::constraint_impulse component.

Rolling friction works similarly to sliding friction, where it uses the same orthogonal tangential vectors to apply torque and remove angular velocity from the rolling bodies. Rolling friction is only applied to shapes that can roll, such as spheres, capsules and cylinders. For cylinders specifically, it is only applied along the cylinder's main axis. Its impulse can be found in the fifth and sixth entries in edyn::constraint_impulse. In the literature, the coefficient of rolling resistance often represents the magnitude of force that's applied at the center of mass of a rolling body per unit of weight, whereas in Edyn, the coefficient at edyn::material::roll_friction represents an amount of torque that is applied per unit of weight. Thus, a real world coefficient of rolling resistance should be multiplied by the radius of the rolling shape before assigning to the material.

Spinning friction applies torque along the contact normal. Its impulse is found in the fourth entry in edyn::constraint_impulse.

Restitution

The coefficient of restitution represents the elasticity or bounciness of a rigid body. To properly propagate the shock of a collision on a chain of connected rigid bodies with non-zero restitution, Edyn employs a restitution solver which runs before the constraint solver. The restitution solver solves small groups of contact constraints in isolation over a few iterations until the normal relative velocity of all contact points is positive, i.e. they're separating (within a threshold).

The number of iterations can be set via edyn::set_solver_restitution_iterations and edyn::set_solver_individual_restitution_iterations, where the latter represents the number of iterations used when solving each subgroup of constraints. If the number of iterations is set to zero, the restitution solver is deactivated. Restitution will still work in this case, though in limited fashion, where only the contacts with non-zero restitution which have an initial penetration velocity will actually be treated as having non-zero restitution and existing contacts which have zero normal relative velocity will be treated as if their restitution was zero, which is a limitation of the sequential impulses constraint solver which is solved by the restitution solver. The restitution solver can be quite expensive to run so you might want to deactivate it if you don't need this effect.

Shifting the center of mass

By default, the center of mass is the centroid of a shape. The center of mass can be specified in the edyn::rigidbody_def and it can also be changed later using edyn::set_center_of_mass. The center of mass is specified as edyn::vector3 which is an offset from the centroid in object space. The moment of inertia about the new center of mass can be calculated using the parallel axis theorem by calling edyn::shift_moment_of_inertia.

auto def = edyn::rigidbody_def();
def.mass = 100;
def.shape = edyn::box_shape{0.2, 0.2, 0.2};
def.center_of_mass = {0.1, 0.1, 0.1};
// Calculate moment of inertia using the parallel axis theorem.
def.update_inertia();
auto entity = edyn::make_rigidbody(registry, def);

// It can also be changed dynamically later.
edyn::set_center_of_mass(registry, entity, {0.05, -0.1, 0});

It is very important to note that edyn::position represents the position of the center of mass and edyn::linvel represents the velocity of the center of mass. For that reason, whenever the center of mass is changed, those values are updated to reflect the state at the new center of mass. edyn::get_rigidbody_origin can be used to get the position of the origin in world space (the origin is always zero in object space).

All constraint pivots are relative to the origin. That includes contact points. Thus it is important to remember to use the position of the rigid body origin when converting these pivots into world space, and not the edyn::position (unless the center of mass offset is zero, of course). This makes it possible to change the center of mass dynamically without having to modify the pivots of all constraints that connect to a rigid body.

Closest point calculation

The collision detection APIs can be used independently. The edyn::collide function is overloaded for every pair of shape types and it calculates the closest points between them.

// Calculate closest points between two boxes.
auto box = edyn::box_shape{edyn::vector3{0.5, 0.5, 0.5}};
auto ctx = edyn::collision_context{};
ctx.posA = edyn::vector3{0,0,0};
ctx.ornA = edyn::quaternion_identity;
ctx.posB = edyn::vector3{0, 2 * box.half_extents.y, 0};
ctx.ornB = edyn::quaternion_identity;
// If the shapes are farther away than `ctx.threshold`, no contact points will be generated.
ctx.threshold = 0.2;
auto result = edyn::collision_result{};
edyn::collide(box, box, ctx, result);
// `result` now contains the closest points.

Ray-casting

Ray-casting can be done using the edyn::raycast function, such as:

// Given points `p0` and `p1` for the ray:
auto result = edyn::raycast(registry, p0, p1);

if (result.entity != entt::null) {
    // Calculate point of intersection.
    auto intersection = edyn::lerp(p0, p1, result.fraction);
    // Access extra info.
    if (registry.has<edyn::box_shape>(result.entity)) {
        // Since the hit entity has a `box_shape`, it means the info is a `box_raycast_info`.
        auto info = std::get<edyn::box_raycast_info>(result.info_var);
        // Get the index of the face that was hit...
        auto face_idx = info.face_index;
        // ...then do something with it...
    }
}

The result contains the entity that was hit first by the ray, or it may be entt::null if nothing was hit. It also contains the fraction between p0 and p1 where the intersection occurs, which allows the intersection point to be calculated using linear interpolation. It also contains the normal vector at the point of intersection and extra info in the info_var member. The contents of the variant info_var depends on the type of shape that was hit. It is empty for a edyn::sphere_shape since there's no extra information for a sphere intersection, or in case a edyn::polyhedron_shape is hit, it has a edyn::polyhedron_raycast_info which contains the index of the face that was hit.