Rigid-Soft Coupling and Contact
Rigid-soft coupling does not require a special coupling object. Put ABD and
FEM geometries in the same Scene, label their collision surfaces, and enable
their material pair in ContactTabular. The global nonlinear solve advances
both DOF systems and the IPC barrier transfers forces between them.
Complete mixed scene
This asset-free program places one deformable tetrahedron, one affine body, and an implicit plane in a single scene. The soft body has its own contact element so the rigid-soft pair can use different friction and resistance from the default rigid-rigid/rigid-ground model.
#include <cmath>
#include <filesystem>
#include <iostream>
#include <uipc/uipc.h>
#include <uipc/constitution/affine_body_constitution.h>
#include <uipc/constitution/stable_neo_hookean.h>
int main()
{
using namespace uipc;
using namespace uipc::constitution;
using namespace uipc::core;
using namespace uipc::geometry;
const std::string workspace = "output/docs/rigid_soft_cpp";
std::filesystem::create_directories(workspace);
Engine engine{"cuda", workspace};
World world{engine};
auto config = Scene::default_config();
config["dt"] = 0.01;
config["gravity"] = Vector3{0.0, -9.8, 0.0};
Scene scene{config};
auto& table = scene.contact_tabular();
auto rigid_contact = table.default_element();
auto soft_contact = table.create("soft");
table.default_model(0.3, 1.0_GPa);
table.insert(rigid_contact, soft_contact, 0.5, 300.0_MPa);
const Float s3 = std::sqrt(3.0) / 2.0;
vector<Vector3> vertices = {Vector3{0.0, 1.0, 0.0},
Vector3{0.0, 0.0, 1.0},
Vector3{-s3, 0.0, -0.5},
Vector3{s3, 0.0, -0.5}};
for(auto& p : vertices)
p *= 0.3;
vector<Vector4i> tetrahedra = {Vector4i{0, 1, 2, 3}};
auto base = tetmesh(vertices, tetrahedra);
label_surface(base);
label_triangle_orient(base);
auto soft = base;
for(auto& p : view(soft.positions()))
p += Vector3{0.0, 0.25, 0.0};
StableNeoHookean fem;
auto moduli = ElasticModuli::youngs_poisson(100.0_kPa, 0.45);
fem.apply_to(soft, moduli, 1000.0);
soft_contact.apply_to(soft);
scene.objects().create("soft_body")->geometries().create(soft);
auto rigid = base;
AffineBodyConstitution abd;
abd.apply_to(rigid, 100.0_MPa, 1000.0);
rigid_contact.apply_to(rigid);
Transform transform = Transform::Identity();
transform.translation() = Vector3{0.0, 1.0, 0.0};
view(rigid.transforms())[0] = transform.matrix();
scene.objects().create("rigid_body")->geometries().create(rigid);
auto floor = ground(0.0);
rigid_contact.apply_to(floor);
scene.objects().create("floor")->geometries().create(floor);
world.init(scene);
if(!world.is_valid())
return 1;
while(world.frame() < 15)
{
world.advance();
world.retrieve();
}
std::cout << "rigid-soft scene reached frame " << world.frame() << '\n';
return world.is_valid() ? 0 : 1;
}
"""Minimal ABD/FEM coupled-contact scene used by the coupling tutorial."""
from pathlib import Path
import numpy as np
from uipc import Logger, Transform, Vector3, view
from uipc.core import Engine, Scene, World
from uipc.geometry import ground, label_surface, label_triangle_orient, tetmesh
from uipc.constitution import AffineBodyConstitution, ElasticModuli, StableNeoHookean
from uipc.unit import GPa, MPa, kPa
Logger.set_level(Logger.Level.Warn)
workspace = Path("output/docs/rigid_soft")
workspace.mkdir(parents=True, exist_ok=True)
engine = Engine("cuda", str(workspace))
world = World(engine)
config = Scene.default_config()
config["dt"] = 0.01
config["gravity"] = [[0.0], [-9.8], [0.0]]
scene = Scene(config)
table = scene.contact_tabular()
rigid_contact = table.default_element()
soft_contact = table.create("soft")
table.default_model(0.3, 1.0 * GPa)
table.insert(rigid_contact, soft_contact, 0.5, 300.0 * MPa)
s3 = np.sqrt(3.0) / 2.0
vertices = np.array(
[[0.0, 1.0, 0.0], [0.0, 0.0, 1.0], [-s3, 0.0, -0.5], [s3, 0.0, -0.5]]
) * 0.3
tetrahedra = np.array([[0, 1, 2, 3]])
base = tetmesh(vertices, tetrahedra)
label_surface(base)
label_triangle_orient(base)
# Deformable body: every vertex owns three FEM degrees of freedom.
soft = base.copy()
view(soft.positions())[:] += Vector3.Values([0.0, 0.25, 0.0])
moduli = ElasticModuli.youngs_poisson(100.0 * kPa, 0.45)
StableNeoHookean().apply_to(soft, moduli, mass_density=1000.0)
soft_contact.apply_to(soft)
scene.objects().create("soft_body").geometries().create(soft)
# Affine body: the mesh is local geometry and its instance owns 12 ABD DOFs.
rigid = base.copy()
AffineBodyConstitution().apply_to(rigid, 100.0 * MPa, mass_density=1000.0)
rigid_contact.apply_to(rigid)
transform = Transform.Identity()
transform.translate(Vector3.Values([0.0, 1.0, 0.0]))
view(rigid.transforms())[0] = transform.matrix()
scene.objects().create("rigid_body").geometries().create(rigid)
floor = ground(0.0)
rigid_contact.apply_to(floor)
scene.objects().create("floor").geometries().create(floor)
world.init(scene)
assert world.is_valid()
for _ in range(15):
world.advance()
world.retrieve()
assert world.is_valid()
print(f"rigid-soft scene reached frame {world.frame()}")
The two bodies keep different state models:
| Geometry | Constitution | Dynamic state | Fixed state |
|---|---|---|---|
| Soft tetrahedron | StableNeoHookean |
Three position DOFs per vertex | vertices()/is_fixed |
| Affine body | AffineBodyConstitution |
Twelve DOFs per instance | instances()/is_fixed |
| Ground | implicit geometry | none | static by construction |
Grouping geometries under one object would not couple them mechanically. Contact couples them because their surfaces share an enabled table entry.
How the contact table is resolved
ContactTabular is symmetric: inserting (rigid, soft) defines both lookup
directions. Every geometry has one contact-element ID. An untagged geometry
uses the default element, while create(name) allocates a new element.
The example resolves these pairs:
| Pair | Model used |
|---|---|
| rigid-ground | default: friction 0.3, resistance 1 GPa |
| rigid-soft | explicit: friction 0.5, resistance 300 MPa |
| soft-ground | table fallback/default behavior unless explicitly inserted |
If a pair needs precise behavior, insert it explicitly rather than relying on fallback. For example:
Apply each element to its geometry before the geometry is copied into a scene object. Changing the local mesh afterward does not retroactively retag the scene copy.
Contact setup checklist
For every expected pair, verify this sequence:
config["contact"]["enable"]istrue(the default).- Explicit meshes have a collision surface from
label_surface. - Tetrahedral surfaces are oriented with
label_triangle_orient. - Each geometry has the intended
ContactElement. - The pair entry is enabled and has non-negative friction and a sensible resistance in scene units.
- Initial geometry is not intersecting; keep the sanity check enabled while building a scene.
d_hat, thickness, mesh scale, and initial separation are mutually consistent.
Friction additionally requires contact/friction/enable = true, which is the
default. A pairwise friction coefficient of zero remains frictionless.
Resistance, ABD stiffness, and FEM modulus are different
- ABD
kappapenalizes non-rigid affine deformation of one body. - FEM Young's modulus controls deformation inside a soft body.
- Contact resistance controls the pairwise normal barrier.
They use stiffness-like units but act on different energies. Increasing all three to solve an overlap usually hides a scale or initialization error and makes Newton/PCG convergence harder.
The backend computes a scene-adaptive contact-stiffness corridor when possible and clamps non-negative user resistance into it. Negative contact stiffness is the opt-in marker for adaptive kappa. See the full Contact and Collision reference for the exact defaults and precedence rules.
Solver behavior in mixed scenes
ABD and FEM Hessian blocks enter one global linear system. The FEM
preconditioner selector affects FEM blocks only; ABD keeps its own block
preconditioner. With linear_system/fem_preconditioner = "mas", all non-empty
FEM meshes are partitioned automatically using backend-compatible clusters.
Use it as a measured performance option, especially for stiff deformables,
not as a requirement for physical coupling.
Deeper examples and implementation evidence
- ABD/FEM stack:
90_abd_fem_cube_stack - rigid wall with cloth/FEM:
93_cube_wall_cloth - material-dependent rigid contact:
10_ramp_sliding - C++ mixed-contact regressions:
18_abd_fem_contact.cppand41_abd_fem_mesh_d_hat.cpp - contact masks and multiple models:
31_contact_mask.cppand8_abd_multi_contact_model.cpp - public table API:
contact_tabular.h